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)