D-Link DIR-815路由器溢出漏洞分析
网上关于 D-Link DIR-815 路由器漏洞复现的文章还是蛮多的,因此第一次的复现选择了这个软柿子🤔。因为相关文章很多的缘故,所以我尽可能来写一些大多文章没有提到的点。
漏洞描述 :DIR-815 固件中的 Hedwig.cgi 脚本中,在处理 HTTP 头时,如果 Cookie 字段中含 uid= 的值则存在栈溢出漏洞,从而获得路由器远程控制权限
影响版本 :DIR-815/300/600/645等
首先下载固件,然后用 binwalk 解压出来得到文件系统,很多文章对这里进行了详细介绍,这里就不再赘述了,只记录一个关于解压后出现的软链接问题。我的 ubuntu 18.04 中 binwalk 解压文件系统时报了如下的 warning

查看 web 目录下的文件发现软链接都指向了 /dev/null (如下)
如果单纯的为了启动程序,可以去手动设置回原本的软链接(上面的 warning 中有记录原本的链接位置在哪),下文中也提到了这一点。不过在该漏洞复现完之后,我在一篇文章中发现了解决的方法。
先去下面这个目录,然后找到 extractor.py 文件(如果找不到这个目录的话也可以用 find 搜一下 extractor.py 的位置)

然后来编辑这个文件,直接翻到文件的最后一行,应该是下面这样的
就改成 if 0 and not 让这个 if 进不去即可。
最后执行命令 sudo python3 setup.py install 重新安装 binwalk 就能生效了,可以看到现在的效果是正常的(如下)

不过需要提一句,这里即使看起来软链接是正常的,但依然无法正常启动程序,如果想要运行某个程序的话(依然需要自己手动创建一个软链接,至少我的是这样)
运行时报错

这个报错说明找不到 libgcc_s.so.1 文件,解决方法是将解压固件得到的文件系统中的 /lib 目录下的 libgcc_s.so.1 文件软链接到 /lib 目录下即可

然后再次运行发现并不是原本缺少 libgcc_s.so.1 的报错了(如下)

看到这个字符串会感觉有点熟悉,将 cgibin 拖到 ida 里发现是程序里没有匹配到相应的函数(如下),因为运行的 cgibin 程序并不在这个匹配的列表中,正常情况下都是通过软链接指向的这个程序来执行的。所以要去执行 hedwig.cgi 程序

因为当初 binwalk 提取完固件,其中 hedwig.cgi 的软链接都指向了 /dev/null ,所以这里要把 hedwig.cgi 删掉,重新生成一个 cgibin 的软链接。
下图程序是成功跑起来了

分析二进制文件
main
main 函数的最开始在匹配程序名以来调用不同的函数来实现具体功能。
v3 = *argv; |
以这段代码为例,首先根据 *argv 获取程序的名字,通过 strrchr 函数来匹配程序名中最后一个 / 出现的位置, v6+1 取的是 / 的下一个字符的地址,然后来匹配是否为 phpcgi 这个字符串, 如果是的话则跳转到 phpcgi_main 函数,整个 main 函数都是在做这个事情
hedwigcgi_main
接下来逐步分析 hedwigcgi_main 函数
sprintf 是危险函数,将字符串格式化后拷贝到指定内存时没有规定长度大小从而可能存在溢出
这里需要让环境变量 REQUEST_METHOD 为 POST ,并且创建 /var/tmp/temp.xml 文件

上图中出现的一个关键函数是 sess_get_uid ,它的作用是将提取的 COOKIE 中 uid= 后面的字符串存为 v4 的 data 字段。下面来分析一下这个函数
sess_get_uid
在分析这个函数之前,还需要分析前面出现过的几个函数 sobj_new sobj_strcmp sobj_add_char sobj_get_string
sobj_new
申请了一块堆,用来存储结构体的数据,主要关注的是 max_len used_len data 这三个成员,其他几个之后逆向分析的时候没用到(这里每个字段的含义,不是一上来就知道的,这是分析其他函数时进行猜测的)
sobj_strcmp
传入的参数一个是 sobj_new 返回的结构体指针,另一个是字符串指针,判断结构体的 data 字段存储的字符串是否和传入的字符串一样
sobj_add_char
传入了 sobj_new 返回的结构体指针,另一个参数是字符。首先判断结构体指针是否存在,max_len 是否等于 used_len 。如果符合条件的话将字符 ch 写入到 data 字段中,并且让 used_len 字段加一。
sobj_get_string
该函数用于返回传入的结构体指针中 data 域的指针
现在来分析 sess_get_uid
函数最开始进行了一些初始化和判断,同时拿到了环境变量 HTTP_COOKIE 值的指针,并设置 state ( 状态位)为 0
该函数具体功能是通过逐个扫描 COOKIE 的字符,来寻找 = ,如果找到了 = 则设置 state 为 2 ,之后再扫描字符的时候因为 state 为 2 的缘故,都会进入另一个分支,去将扫描 COOKIE 的字符存储到 v4 结构体的 data 成员中。如果没有找到 = 那么 state 一直为 1 ,则始终将 COOKIE 的字符存储到 v2 结构体的 data 成员中(如下图)
当扫描完 COOKIE 的所有字符后,去判断 v2 结构体的 data 成员是否为字符串 uid ,如果是的话,就将 v4 结构体之前存储的字符串写到结构体 a1 的 data 域中。( a1 也就是 sess_get_uid 函数传入的结构体指针)
再回到 hedwigcgi_main 函数上,现在想执行到真正利用的溢出点,需要控制 haystack 的值才行(如下图)
控制 haystack
通过查看 haystack 的交叉引用(如下图),发现只有一个地方可以对 haystack 进行赋值
跳转过去到了 409A6C 函数
如果记性不错的话应该能想起来它是一个回调函数,在 hedwigcgi_main 函数中出现过 cgibin_parse_request((int)sub_409A6C, 0, 0x20000u); 因此就要去分析 cgibin_parse_request 函数,看看是何时调用了 409A6C 函数
cgibin_parse_request
这里是 cgibin_parse_request 函数的后部分,前部分要满足 CONTENT_LENGTH < 0x20000 和 REQUEST_URI 这个值要存在,这样才能走到下面这部分
这里设置 CONTENT_TYPE 为 aApplication ,最后会调用 0x42C014[2] 位置的指针,该函数指针就是 0x403B10

之后给个分析的思路吧, 实在不想写这么详细了。
进入 403B10 函数,首先 CONTENT_TYPE 在原本的 aApplication 后面要再加上字符串 x-www-form-urlencoded 才能进入主逻辑部分。 read 会读入 0xc 个数据,然后将这个输入的数据作为参数调用 402B40 函数,这个函数将刚刚读入的数据,以 = 进行分割。接着调用了函数指针 v9 (这个 v9 就是最开始所说的回调函数 409A6C ),而刚刚 = 前面的数据会被当做参数传进来,下面再看一下 409A6c 函数

因此只要走到这里,haystack 就会被赋值成 = 前面字符串的地址。从而绕过 if ( !haystack ) 这个判断。
总结一下赋值 haystack 的函数调用链 :cgibin_parse_requeset -> 403b10 -> 402b40 -> 函数指针v9 ,初学者可以自行去详细分析上述过程。
qemu 用户模式下复现
ROP 链的布置
现在是肯定能走到第二次的 sprintf 进行溢出了。现在我们来测一下溢出控制返回地址的偏移量是多少。
如何调试
先准备一个 payload 文件,里面放入 COOKIE 的值,这里直接用 cyclic 2000 > payload ,不过别忘记在最开始加一个 uid= 字符串
然后写一个启动脚本(如下),这里简单说明一下这个脚本。首先使用 chroot 命令将当前目录 squashfs-root 设置为根目录,因为程序打开的文件都是相对于这个文件系统来说的。一旦将 squashfs-root 设置为根目录,那么 qemu-mipsel 就没办法使用了,因为依赖了其他目录的库文件,因此我们使用静态链接的 qemu-mipsel-static (我的 ubuntu 18.04 上用 apt-get install 安装的 qemu-mipsel-static 会报一个错误

原因是这个 qemu-mipsel-static 版本太低,我的解决方法是在 ubuntu 22.04 上安装后,拖到了 18.04 上)
-E 用于指定要在模拟的虚拟机中设置的环境变量,而这些变量是前面分析过的,进行设置即可,剩下的就和调试 MIPS 架构的程序一样了,有需要的话可以查看这篇 文章
!/bin/bash |

发现覆盖到返回地址需要填充 1043 的垃圾数据。
通过观察函数最后返回处的汇编,这里是可以控制很多寄存器,我们接下来就是要通过这些可控的寄存器来完成 ROP

ROP-system
因为这个程序的溢出是 sprintf 导致的, \x00 可以造成字符串的截断,而 system 函数地址末尾就是 \x00 ,为了避免被截断,我们要先让 system 函数的地址减一放入一个寄存器,之后跳转到能让这个寄存器加一的 gadget 上。MIPS 架构的 ROP 是通过寄存器间的跳转实现的,而 x86 中通常是用 ret 指令根据栈中存放的数据来跳转的。
在 《揭秘家用路由器0day漏洞挖掘技术》一书中对该 ROP 链布局画的十分形象(如下),因为上面提到了我们能控制很多寄存器,就先在 $ra 寄存器布置一个让 $s0 加一的 gadget (提前控制 $s0 为 system 减一的地址),接着跳转到一段能赋值栈地址的 gadget 上(用于指向 /bin/sh ),最后跳回到 system 上

补充:
- 程序依赖的
libc是软链接libc.so.0指向的libuClibc-0.9.30.1.so,因此gadget要去这个里面找 - 找
gadget的话,用IDA插件mipsrop。以上面两段gadget为例,搜寄存器加一的指令可以这么搜mipsrop.find("addiu .*,1"),当然了可能会出现下面的报错

只需要点一下 search -> mips rop gadgets 即可

能匹配到很多个 gadget (如下),根据自己布局的需求来选择合适的就可以

如果要搜将栈地址放入某个寄存器的 gadget ,可以用 mipsrop.stackfinder() 命令(如下)

winmt 师傅提到 用户模式不支持多线程,而 system 函数会调用 fork 函数,从而导致 fork 执行失败,system 执行到这里后就会卡住。不过之后在系统模式下是没问题的
EXP
from pwn import * |
上面的 exp 是可以正常走到 system 函数的,但是 a0 是 /bin//sh/postxml ,这是因为第一次 sprintf 拼接了后面的字符串常量 postxml 。因为地址固定的原因,我们可以直接使用 libc 中的 /bin/sh 地址 EXP如下
from pwn import * |
可以发现这次是成功执行到了 system("/bin/sh") ,因为 fork 的原因,依然是拿不到 shell
ROP-ret2shellcode
明白了上面 ROP 的思想,那么布置 shellcode 也就不在话下,因为 shellcode 能直接调用 execve 从而不需要去使用 fork。不过需要注意的是 shellcode 中不能出现 \x00 还有缓存不一致性(数据缓存区和指令缓存区需要一个时间来同步),因此需要先调用一下 sleep(1) 再去执行 shellcode。
这里还需要提到一点,如果现在执行了 gadgetA ,然后跳转到了 sleep(1) 函数,等函数返回时会再跳转到了 gadgetA,因此必须要保证 gadgetA 回来后依然能去跳转到我们指定的地址,以此来保证 ROP 不间断。
画了个抽象的图(如下)
EXP
from pwn import * |

可以看到这次是拿到 shell 了。不过这里执行 execve("/bin/sh") 成功其实是一种假象,因为固件中的 /bin/sh 链接到了 busybox 上,虽然 busybox 是静态链接,但因为它是 MIPS 架构,导致了我在 x64 上直接执行是失败的。因此我上面是把原本的 sh 给删掉,换成了主机自带的 x64 架构的 sh ,同时还把相应的动态库都放到了当前的 /lib 下面,才算执行成功。不然用原本的 sh 还是执行失败,这么做的目的仅仅是为了证明这种操作理论上是可以拿到 shell 的 😎
qemu 系统模式下复现
只要在 qemu 用户模式下能复现成功,并且搞清楚原理,其实这个 qemu 系统模式搞的很快。首先实现一下 qemu 与宿主机的通信,然后把 httpd 服务启起来就可以发送数据包直接打了(在不遇到什么奇怪的报错下确实比较快…)
我这里的环境是 ubuntu 18.04 qemu-system-mipsel 7.2.0
实现宿主机与 qemu 的通信
创建一个 net.sh 脚本,我这里的网卡是 ens33 ,如果是 eth0 的话,就把出现的 ens33 换成 eth0 即可,chmod +x net.sh 给文件可执行权限,然后 ./net.sh 运行
!/bin/sh |
然后再执行如下几条命令
!/bin/sh |
再用下面这个脚本启动
sudo qemu-system-mipsel -M malta -kernel vmlinux-2.6.32-5-4kc-malta -hda debian_squeeze_mipsel_standard.qcow2 -append "root=/dev/sda1 console=tty0" -nographic -net nic -net tap,ifname=tap0,script=no,downscript=no |
这个 mips 内核还有镜像文件,之前师傅们上放的链接好像都失效了。这里是找 winmt 师傅要的一份,上传到网盘上了 链接:https://pan.baidu.com/s/1-qvt7pG0Tr91JKoH2elNdQ?pwd=l04v
提取码:l04v
如果此时 qemu 中的网卡 eth0 是有 ip 的,并且能够 ping 通宿主机的 ip,那就能说明 qemu 已经能和宿主机进行通信了


启动 httpd 服务
在 squashfs-root 的上一级目录中,执行下面的命令, IP 换成 qemu 的。这样可以实现计算机远程之间的文件传输,作用就是把提取出来的文件系统传到 qemu 里面
sudo scp -r ./squashfs-root root@10.214.140.139:/root/squashfs-root
然后在 qemu 中的 squashfs-root 目录下新建一个 http_conf 文件
写入以下代码(网卡和 IP port 要改成自己的)
Umask 026 |
然后在物理机上 /opt/tools/mipsel 目录(没有的话就自己创建吧)中新建 init.sh 文件,写入如下配置
! /bin/sh |
给这个 init.sh ,可执行权限,然后将其执行
然后在 qemu 中的 squashfs-root 目录下创建 init.sh 文件,写入下面的内容。给可执行权限,然后执行
!/bin/bash |
最后进到 /squashfs-root/sbin 目录下,执行 ./httpd -f /root/squashfs-root/http_conf
在宿主机中访问 http://10.214.140.139/hedwig.cgi 发现可以正常访问了(如下)

开启 httpd 服务后,如果要进行调试则需要下载一个 gdbserver.mipsle ,然后再用 scp 命令将其上传到 qemu 中的 /root/squashfs-root/ 目录下。
在 qemu 中 /root/squashfs-root/ 目录下新建 run.sh 脚本(IP 改成宿主机的,端口)
!/bin/bash |
正常情况下应该是能从宿主机中调试 qemu 中的程序,但我这里报了这个错误(折腾了很久也没有解决,于是就暂时放弃了远程调试的想法)
不过还有一个方法也能确定 libc 基地址,就是用运行 hedwig.cgi 后进行后台挂起,然后用 cat /proc/pid/maps 查看,先跑几次程序,发现 pid 的增长是有规律的,于是提前预测一下,多尝试几次就能打印出来内存布局获取 libc 基地址(如下)

因为没法调试,这里就直接用网上师傅的脚本打了(主要用户模式已经写了好几种脚本,这个没法调试的问题死活解决不了,导致了没法调试 rop的布局)思路和用户模式 ROP-system 的那个脚本是一样的,就把命令换成反弹 shell 的命令即可
EXP
#!/usr/bin/python3 |
可以看到是已经将 qemu 中模拟的环境 shell 反弹到了宿主机上。

参考文章
从零开始复现 DIR-815 栈溢出漏洞-二进制漏洞-看雪-安全社区|安全招聘|kanxue.com
DLink 815路由器栈溢出漏洞分析与复现 - unr4v31 - 博客园 (cnblogs.com)