type
status
date
slug
summary
tags
category
icon
password
根据漏洞描述
D-Link DIR-645是一款无线路由器设备。
D-Link DIR-645 "post_login.xml","hedwig.cgi","authentication.cgi"不正确过滤用户提交的参数数据,允许远程攻击者利用漏洞提交特制请求触发缓冲区溢出,可使应用程序停止响应,造成拒绝服务攻击。
程序分析
找到
hedwig.cgi
文件所在位置
发现
hedwig.cgi
是一个软链接,指向 cgibin
文件这里不知道为什么我的这个不显示,我之后研究一下
正常应该是这样的

直接
cgibin
文件是个二进制程序,ida看一下
这里大概就是根据
/
符号最后的参数,来判断要执行的函数,根据之前提到的hedwig.cgi
在这里也可以找到,
进入
hedwigcgi_main
函数中看看
这里程序一开始通过
来判断http的请求方式,
REQUEST_METHOD
环境变量,存放这请求方式。
从环境变量里取 CGI 的 REQUEST_METHOD
(如 "GET"
, "POST"
)。Web 服务器在执行 CGI 程序前会设置这些环境变量。
之后
这里用来判断是否为
POST
请求,如果不是就会给v1赋值,并打印再屏幕上,如果是这可以往下执行,可以看到我们刚刚访问
http://192.168.2.2:4321/hedwig.cgi
时的报错内容:"unsupported HTTP request"
分析可知,其会读取并判断环境变量
REQUEST_METHOD
是否为 POST,因此只支持 POST 请求方式,刚刚通过浏览器访问是 GET 方式,所以报错
接下来会执行

这里要在
CONTENT_TYPE
,CONTENT_LENGTH
,REQUEST_URI
都放入一定的数据用于满足程序的绕过,
application/x-www-form-urlencoded
1. 为什么是这个值
- 浏览器在提交表单时,默认的
enctype
就是application/x-www-form-urlencoded
。
- 这意味着:表单里的
key=value
数据会被 URL 编码(比如空格 →+
,中文 →%E4%B8%AD
),然后放在 HTTP 请求体里传过去。
- Web 服务器(例如 Apache、Nginx + CGI/FastCGI)会根据请求头自动设置环境变量
CONTENT_TYPE
,告诉后端程序请求体是什么格式。
所以你在 CGI 程序里看到:
就是因为浏览器用的默认表单编码。
2. 什么时候不是这个值
- 如果表单设置了
enctype="multipart/form-data"
(常见于文件上传),那么CONTENT_TYPE
就会变成:
- 如果你用 AJAX/Fetch 手动发 JSON 数据:
那么 CGI 环境变量
CONTENT_TYPE
就会变成 application/json
。3. 总结
application/x-www-form-urlencoded
是 HTML 表单的默认编码方式。
- 所以你用普通表单 POST,服务器端
CONTENT_TYPE
一直都是这个值。
- 想改它,就要在 请求头里明确指定 Content-Type,或者在
<form>
里修改enctype
。
之后重点来看
sess_get_uid(v4);
函数
获取环境变量
HTTP_COOKIE
里的值其判断
uid
的逻辑为:以 '='
作为分隔,'='
前面的内容存入 v2
,'='
后面的内容存入 v4
,假设原字符串为 uid=xxx
,如果 v2 == 'uid'
,则 v4 == 'xxx'
就是 uid
数据最后将
v4
中的 uid
数据赋值给变量 string
,最后将其写入 a1
,也就是该函数的形参之后回到
hedwigcgi_main
函数,就会来到程序的第一个溢出点
这里程序把我们输入的数据解析之后
前面
sess_get_uid()
函数会将 uid
写入形参,因此 v4
的值就是 uid
也就是说,
sprintf(v27, "%s/%s/postxml", "/runtime/session", string)
中的 string
就是 uid
,而这个 uid
是用户可以控制的v27
是一个长度为 1024 的字符数组,明显是可以被人为输入的 uid
溢出的:后面还有一个,相同的指令,

由于
v4
没有被修改过,因此这里的 v20
同样是 uid
,v27
同样可以被溢出因此我们可以利用这里覆盖上一次
sprintf()
的内容但是要想执行两次
sprintf()
需要满足两个条件判断:- 第一个是需要存在
/var/tmp/
路径,其会创建一个temp.xml
文件并写入数据
- 另一个是要求
haystack
非空

这个第一个条件比较好满足,这里我要自己创建一个
/var/tmp
文件就行,这个在真实的路由上传中,我们这里没有然后是有关
haystack
不能为0,这里我没搞懂,可以看看其他师傅的解释,
关于
haystack
,通过交叉引用(IDA 快捷键为 X),发现 haystack
在此之前只有 sub_409A6C()
函数进行过修改,也就是 cgibin_parse_request((int)sub_409A6C, 0, 0x20000u)
的第一个参数:

cgibin_parse_request()
函数在这里才调用了 sub_409A6C()
函数(作为形参 a1
):
off_42C014
处存放的是 "application/"
数据,这是在处理 HTTP 请求时用到的 MIME 类型字符串的一部分:
因此这里是对
POST
内容的读入要想读入
POST
,就必须先满足 v9 != -1
的 if
判断,而 v9
初值就是 -1
,因此需要走中间的 if
分支使 v9 = 0
,同时也必须保证环境变量 REQUEST_URI
不为空:
反正最后就是要让
REQUEST_URI
不为空。
QEMU 用户级复现
QEMU 用户级层面的漏洞复现不需要进行仿真,但相比之下,需要进行仿真的系统级复现更加直观、更符合现实场景,这里主要是介绍 QEMU 用户级层面的漏洞复现方式
这里我们先进行这种类似于本地测试的,之后在进行系统级远程测试,
我们在宿主机的路由器文件系统根目录
生成 2000 个字符的 payload 文件,用来测试
uid
溢出到栈上返回地址所需的字节数:创建以下
run.sh
脚本,通过 QEMU 用户模式启动 /htdocs/cgibin
程序:这里通过
-g 1234
在 1234 端口开启了 gdbserver 监听,cat payload
可以将 payload
文件中的内容读到 uid=
之后,echo $INPUT |
可以做到 POST 的效果,-0
就是 argv[0]
,-E
是设置环境变量然后执行
run.sh
:
可以看到本机开启了 1234 端口
然后使用本机的
gdb-multiarch
连接 gdbserver:

这里有个问题就是
vmmap
这个不能是用,我们不能直接看到libc地址,我们要在调试的过程中寻找,这个比较简单,我们可以直接利用程序的延迟绑定机制找到
通过两次t9寄存器跳转
memset
函数拿到libc
地址
此时的
t9
为第一次调用是用plt
表里的内容,
第二次就是libc中的真实地址。
在路由器文件系统的
/lib
文件夹内,找到其所使用的 libc 文件:libc.so.0
利用
objdump
查找 memset()
函数的偏移地址:
可以知道libc的基地址为
0x3ff6ca20-00034a20=0x3FF38000
,接下来就是 GDB 执行到
hedwigcgi_main()
函数结束将要返回的地方,观察返回地址来确定溢出的长度:
说明程序最后在溢出之后马上要执行的是
0x646b6161
这里,这个数据在
payload
中为1009
位置,说明我们要填充的数据长度为这个之后就是函数结束会执行的地址。向用户态 QEMU 传递 payload 参数
由于 QEMU 用户级复现不需要仿真,我们只需要用 qemu-mipsel-static 运行 /htdocs/cgibin 程序,然后将 payload 作为参数传递
poc 如下:
mips基础知识
加法:add $1,$2,$3 ⇒ $2+$3=$1
减法:sub $1,$2,$3 ⇒ $2-$3=$1
取数:lw $1,20($2) ⇒ 2号寄存器中的值为基值,偏移20字节,取出数据写入1号寄存器中
存数:sw $1,20($2) ⇒ 2号寄存器中的值为基值,偏移20字节,将1号寄存器中的值写入其中
加立即数 addi $1,$1,1 ⇒ $1=$1+1(这里1可以为负数,所以没有减立即数指令)
addu
指令用于计算无符号数之间进行的加法操作,addu $t0,$t1,$t2
将 $t1
和 $t2
进行无符号相加,结果存储在 $t0
add
指令和 addu
一样,只不过进行的是有符号数之间的加法。addiu
指令将上面的 addi
和 addu
结合了一下, addiu $a1, $zero, 2
进行的是将寄存器$zero
加上一个立即无符号数 2
,并将结果存回寄存器 $a1
中与·或·或非,同1则1,有1为1,同0则1 and,or,nor
左移:sll $1,$2,4 将$2中的数据左移4位放入$1中
右移:srl $1,$2,4 将$2中的数据右移4位放入$1中
相等跳转:beq $1,$2,label 如果1,2号寄存器中的值相等则跳转执行label
不相等跳转:bne $1,$2,label 如果1,2号寄存器中的值不相等则跳转执行label
(这两个指令的原理都是相减,判断结果是否为0)
跳转:j label 直接跳转执行label jr $1 直接跳转执行1号寄存器里的地址
li
(Load Immediate)指令用于将一个立即数存入一个通用寄存器, li $gp, 0x498300
将 $gp
寄存器的值赋值为 0x498300
lui
指令将一个 16
位的立即数左移 16
位后存入目标寄存器中, lui $v0, 0x46
是将 0x46
立即数左移 16
位后存入 $v0
寄存器,即 $v0
寄存器的值为 0x460000
ori
指令是 MIPS
汇编中的一种逻辑运算指令,它可以将一个寄存器的低 16 位与一个 16 位的立即数按位或运算,并将结果存入另一个寄存器中。ori $t6,$t6,0x430a
指令将 t6
寄存器与 0x430a
立即数进行或运算,将结果放回 $t6
la
(Load Address) 指令用于将一个地址或标签存入一个寄存器,la $v0, puts
指令将 puts
函数地址存入 $v0
寄存器中
lw
(Load Word) 指令用于从一个指定的地址加载一个 word
类型的值到一个寄存器 lw $v0, 0x14($fp)
将 $fp+0x14
的位置中的数据存入到 v0
中sw
(Store Word) 将源寄存器中的值存入指定地址,sw $ra, 0x24($sp)
将 $ra
的值写入距离栈顶($sp
)偏移 0x24
的内存单元中jr
是跳转指令,jr $ra
跳转到 $ra
寄存器指向的地址处jal
指令是跳转指令,jal target
复制当前的 PC
值到 $ra
寄存器,然后跳转到 target
处bnez
指令用于在寄存器的值不为零时进行分支跳转,bnez $v0, loc_4005E8
表示当 $v0
不为零时跳转到 0x4005E8
b
是无条件跳转指令,b loc_400604
直接跳转到 0x400604
地址处


- 作者:wgiegie
- 链接:https://tangly1024.com/article/25e3ecc9-5160-80de-90aa-d879e6ab9a20
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。