W5M1n9/NGINX-ngx_http_rewrite_module-heap-buffer-overflow-CVE-2026-9256
GitHub: W5M1n9/NGINX-ngx_http_rewrite_module-heap-buffer-overflow-CVE-2026-9256
Stars: 0 | Forks: 0
# CVE-2026-9256 NGINX ngx_http_rewrite_module PoC 推导过程与思考
适用范围:仅用于本地靶场、授权复现环境、漏洞验证和防护规则分析。不要对未授权目标使用。本文中的 PoC 思路只验证远程可见的 NGINX worker crash 行为,不包含 RCE、ASLR 绕过或稳定利用链。
## 1. 漏洞背景
CVE-2026-9256 是 NGINX `ngx_http_rewrite_module` 中的堆缓冲区溢出漏洞。漏洞触发并不是单纯访问某个固定 URI,而是依赖特定的 rewrite 配置模式:rewrite 正则中存在互相重叠的 PCRE 捕获组,并且 replacement 部分引用了多个捕获变量,例如 `$1`、`$2`。
当攻击者构造特殊 URI 使 rewrite 逻辑进入相关路径时,NGINX 在处理捕获内容、拼接 rewrite 结果或进行 URI/参数转义时可能出现长度计算与实际写入不一致,最终造成 worker 进程堆内存破坏。
因此,这个漏洞的关键不在于 `/api` 这个路径本身,而在于目标 NGINX 配置中是否存在可被请求命中的脆弱 rewrite 规则。PoC 默认使用的 `/api` 只是当前复现环境中的示例路径,实际测试时需要根据 NGINX 配置中存在重叠捕获组并引用多个捕获变量的 rewrite 规则来调整请求路径。
当前 PoC 的目标是验证 worker crash / denial of service 行为。它不尝试构造精确堆布局,不尝试覆盖返回地址或函数指针,也不证明远程代码执行。远程侧能观察到的稳定证据主要是:触发请求连接被异常断开、随后 NGINX 服务恢复响应、keep-alive 连接在触发后被 worker crash 打断。
## 2. 为什么不能只看一个 HTTP 状态码?
这个漏洞触发后不一定表现为固定的 HTTP 500、502 或 400。原因是 NGINX 的 master-worker 模型会让 worker 进程崩溃后由 master 重新拉起新的 worker。攻击者在远程侧看到的现象通常不是整个服务彻底不可用,而是某个连接突然断开、读超时、连接被 reset,随后再次访问 `/` 又能得到正常响应。
所以,PoC 不能只根据一次请求的 HTTP 状态码判断漏洞是否存在。如果只发送一次长 URI,然后看到连接断开,就直接判断“漏洞存在”,误报风险较高。连接断开也可能来自网络抖动、代理超时、请求被中间设备拦截、后端限流或服务端主动关闭连接。
因此,PoC 需要设计成多阶段验证:
1. 先确认目标存活。
2. 再确认示例 rewrite 路径可能生效。
3. 发送溢出触发请求,观察连接是否异常断开或超时。
4. 马上发送普通请求,确认 NGINX 是否已经恢复响应。
5. 通过 keep-alive 方式重复验证 worker 连接是否在触发后稳定掉线。
只有“触发连接异常 + 后续服务恢复 + keep-alive 多轮掉线”同时出现时,才能更稳妥地判断存在 CVE-2026-9256 风格的 worker crash 行为。
## 3. PoC 构造思路
当前 PoC 的核心触发路径为:
GET /api/++++++++++++++++++++++++++++++++... HTTP/1.1
Host: 127.0.0.1:19321
其中 `/api/` 是当前靶场中用于命中 rewrite 规则的示例路由,后面拼接大量 `+` 字符。默认数量为 4096 个。
选择 `+` 的原因主要有三个。
第一,`+` 是合法 URI 字符,使用普通 HTTP 客户端发送时通常不会像空格、`#` 等字符那样被截断或强制改写。因此当前 PoC 不需要像某些 request-target 绕过漏洞那样必须使用 raw socket 构造非法请求行。
第二,大量重复字符可以让 rewrite 捕获组得到足够长的输入,扩大后续 replacement 拼接或转义处理的输出规模,从而更容易触发长度计算与实际写入之间的不一致。
第三,重复 `+` 的 payload 结构简单,便于在抓包、日志和 IDS 规则中观察,也便于调整长度做阈值测试。
不过需要注意,`+` 不是漏洞的唯一理论触发字符。真正的触发条件仍然是“命中脆弱 rewrite 配置 + 输入能进入相关捕获组 + rewrite 输出处理触发堆溢出”。在不同环境中,触发路由、字符类型、长度阈值都可能需要调整。
## 3.1 其他可触发字符说明
当前 PoC 默认选择大量 `+` 作为触发字符,但这不代表只有 `+` 能触发问题。`+` 只是最适合写进通用 PoC 的字符,因为它在 URI 中相对稳定,容易被普通 HTTP 客户端发送,抓包特征也清晰。
从漏洞原理看,只要字符在 NGINX rewrite 处理过程中进入 `NGX_ESCAPE_ARGS` 转义逻辑,并且会从原始 1 字节扩张成 `%XX` 形式的 3 字节,就可能造成“长度计算值小于实际写入值”的差异。也就是说,触发点本质上不是 `+` 本身,而是“可被 args 模式转义的字符密集出现”。
除 `+` 外,理论上需要重点考虑的字符包括:
空格:0x20
#:0x23
%:0x25
&:0x26
?:0x3F
控制字符:0x00-0x1F
高位字节:0x7F-0xFF
这些字符如果进入相关 capture 并在 rewrite replacement 中被当作 args 内容进行转义处理,都会产生类似扩张效果。例如:
+ -> %2B
& -> %26
% -> %25
# -> %23
? -> %3F
空格 -> %20
每出现一个这类字符,理论上都会从 1 字节扩张到 3 字节,实际写入长度增加 2 字节。如果输入中存在大量此类字符,实际写入长度就可能显著超过前面错误计算出的缓冲区长度,从而更容易触发 heap buffer overflow。
不过,不同字符在 PoC 中的可用性并不完全一样。
`+` 最稳定。它通常可以直接出现在 HTTP request-target 中,不容易被浏览器或命令行工具截断,也不会天然改变 URI 的 path/query 结构。因此当前 PoC 使用 4096 个 `+` 作为默认 payload。
`&` 也可以作为候选字符,因为它在 args 模式下会被转义为 `%26`。但在 shell 中 `&` 有后台执行含义,在 URL 中也常被当作 query 参数分隔符,所以测试时需要注意引用和位置,否则请求可能没有按预期发送。
`%` 同样可以作为候选字符,因为它会被转义为 `%25`。但 `%` 本身也是 URL 编码前缀,部分客户端、代理或框架可能会尝试解释 `%XX` 序列。如果构造不当,目标收到的可能不是原始 `%` 字符,而是被客户端预处理后的内容。
`?` 理论上也属于可转义字符,但它在 HTTP request-target 中会分隔 path 和 query。如果直接放在路径中,后续内容可能被解析成 query string,从而改变 rewrite 捕获范围。因此它更适合作为补充测试字符,不适合作为默认 PoC 主字符。
`#` 理论上可以触发转义,但浏览器不会把 `#` 及其后的 fragment 发送给服务器,很多高级 HTTP 客户端也会对它进行编码或截断。因此如果要测试字面量 `#`,通常需要 raw socket、Burp Repeater 或能保留原始 request-target 的工具,不能直接依赖浏览器地址栏。
空格 `0x20` 也属于转义字符,但普通 HTTP/1.1 请求行中空格本身是分隔符,直接放进 request-target 会破坏请求行结构。实际测试时如果写成 `%20`,服务端处理阶段看到的是编码形式还是解码后的空格,取决于具体解析流程和 rewrite 位置。因此空格更适合做原理说明和辅助测试,不适合作为默认 payload。
`0x00-0x1F` 控制字符和 `0x7F-0xFF` 高位字节也在转义范围内,但它们在真实 HTTP 链路中更容易被客户端、代理、WAF 或 NGINX HTTP parser 拦截、规范化或拒绝。它们可以作为源码层面的转义对象说明,但不建议作为常规 PoC 的默认触发字符。
因此,当前 PoC 使用 `+` 的原因不是漏洞只能由 `+` 触发,而是 `+` 同时满足三个条件:能够触发 args 转义扩张、容易稳定发送、不会明显改变 URI 结构。防护规则或流量研判时不能只匹配连续 `+`,还应该考虑其他可转义字符的高密度组合,尤其是 `+`、`&`、`%`、`?`、`#` 等字符在长 URI 中大量出现的情况。
从检测角度看,更合理的概括不是:
/api/ 后面出现大量 +
而是:
长 URI 中出现大量会在 NGX_ESCAPE_ARGS 模式下扩张为 %XX 的特殊字符
如果只检测 `++++`,规则只能覆盖当前 PoC 的默认写法;如果攻击者把 payload 换成 `&&&&`、`%%%%`、`????`,或者混合使用 `+%&?#`,单一 `+` 特征就可能漏报。更稳妥的检测思路是结合 URI 长度、特殊字符密度、连续重复次数、rewrite 风险路径以及 NGINX 服务暴露面共同判断。
## 4. 目标规范化逻辑
PoC 的 `normalize_target` 函数负责处理命令行输入,支持三种形式:
python3 CVE-2026-9256-poc.py 127.0.0.1:19321
python3 CVE-2026-9256-poc.py 127.0.0.1 19321
python3 CVE-2026-9256-poc.py http://127.0.0.1:19321
如果用户只输入 `host:port` 而没有写 scheme,脚本会自动补成 `http://host:port`。随后使用 `urllib.parse.urlparse` 解析出 hostname 和 port,并生成 `base`,例如:
host = 127.0.0.1
port = 19321
base = http://127.0.0.1:19321
当前实现主要面向 HTTP 明文靶场。虽然 `normalize_target` 接受 `https://` 形式,但后面的 keep-alive 检测使用的是普通 TCP socket,没有包 TLS 层,因此 HTTPS 场景下 crash probe 会不准确。如果要支持 HTTPS,需要给 socket 增加 `ssl.wrap_socket` 或 `ssl.create_default_context().wrap_socket()`。
## 5. 存活检测与 rewrite 探测
PoC 首先调用 `check_alive(base)` 访问根路径 `/`:
GET /
如果目标能返回任意 HTTP 状态码,说明服务基本存活,可以继续测试。如果连接失败,则直接退出,避免把不可达目标误判为漏洞触发失败。
随后调用 `check_rewrite(base)` 访问:
GET /api/test
这一请求用于观察 `/api/*` 是否可能命中当前靶场中的 rewrite 逻辑。如果返回 301、302、303、307、308 等重定向状态码,说明 rewrite redirect 行为比较明显,PoC 会打印 Location 头作为辅助证据。
但这个步骤不是强制成功条件。因为在某些复现配置中,`/api/*` 本身就可能进入有问题的 rewrite 路径,甚至普通探测请求也可能超时或被异常处理。所以脚本即使没有拿到正常 rewrite 响应,也会继续进入触发阶段。
## 6. 溢出触发请求设计
触发函数为 `send_trigger(base, plus_count=4096)`,核心逻辑是拼接:
payload = "/api/" + ("+" * plus_count)
默认最终请求路径类似:
/api/++++++++++++++++++++++++++++++++... 共 4096 个 +
然后通过 `requests.get(base + payload, timeout=10, allow_redirects=False)` 发送请求。
这里禁用自动跳转有两个原因。
第一,rewrite 本身可能返回 redirect。如果 HTTP 客户端自动跟随跳转,原始触发请求和后续跳转请求会混在一起,不利于判断第一次请求到底发生了什么。
触发结果被分成几类:
如果捕获到 `ConnectionError`,说明触发请求过程中连接被异常关闭,这可能是 worker crash 的远程表现。
如果捕获到 `ReadTimeout`,说明请求发出后长时间没有正常响应,也可能是 worker 卡死、崩溃前未正常返回,或网络环境导致的超时。
如果正常收到 HTTP 响应,则打印状态码和响应体长度,但不能仅凭正常响应否定漏洞,因为某些环境下触发条件可能没有完全满足,或者 payload 长度不足。
## 8. keep-alive crash probe 设计
PoC 中最关键的稳定性验证是 `keepalive_probe(host, port, rounds=5, plus_count=4096)`。
它不用 `requests`,而是直接使用 `socket.create_connection` 建立 TCP 连接,并在同一条 keep-alive 连接上连续发送三个请求。
第一个请求是普通请求:
GET / HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
这个请求用于确认当前连接可用,并尽量让后续触发请求落在同一条连接上。
第二个请求是触发请求:
GET /api/++++++++++++++++++++++++++++++++... HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
如果这个请求触发了 worker crash,那么该 worker 维护的 keep-alive 连接很可能被直接断开。
第三个请求仍然是普通请求:
GET / HTTP/1.1
Host: 127.0.0.1
Connection: close
如果第三个请求还能收到响应,说明连接没有因为触发请求而中断,本轮不认为 worker crash。
如果第三个请求发送失败、读取不到数据,或者连接已经被关闭,则记录为:
keepalive connection dropped
PoC 默认重复 5 轮。多轮重复的意义是降低偶发网络错误带来的误判。如果 5 轮中多次出现 keep-alive 掉线,并且服务随后仍能恢复响应,那么远程侧证据就更充分。
## 9. 成功判断逻辑
PoC 最终判断分三档。
第一档:漏洞确认。
条件是:
crash_count > 0 and recovered == True
VULNERABILITY CONFIRMED - CVE-2026-9256 style crash behavior
Impact confirmed: worker crash / denial of service
RCE is not proven by this script
这说明当前环境表现出 CVE-2026-9256 风格的 worker crash 行为,但不证明远程代码执行。
第二档:疑似存在。
条件是:
kind == "connection_error" and recovered == True
也就是主触发请求发生连接断开,并且后续服务恢复,但 keep-alive probe 没有稳定确认 worker crash。脚本输出 `VULNERABILITY SUSPECTED`。
这种情况说明有异常现象,但证据不够稳定,需要结合服务端 error.log、core dump、容器日志或调试器进一步确认。
第三档:未确认。
如果既没有可靠的 keep-alive 掉线,也没有触发连接异常加服务恢复的组合,脚本输出:
VULNERABILITY NOT CONFIRMED
这并不一定代表目标绝对无漏洞,也可能是路径没有命中 rewrite、payload 长度不足、字符选择不适配、目标版本已修复、前置代理改变了 URI,或者当前脚本没有适配 HTTPS。
## 10. 当前 PoC 的完整执行流程
脚本执行流程可以概括为:
1. 解析目标地址,生成 host、port、base。
2. 打印 PoC 基本信息,明确说明只验证 worker crash,不实现 RCE。
3. 请求 `/`,确认目标服务存活。
4. 请求 `/api/test`,尝试判断示例 rewrite 路径是否活跃。
5. 发送 `/api/` 加 4096 个 `+` 的长 URI 触发请求。
6. 根据连接断开、超时或 HTTP 响应记录第一次触发结果。
7. 等待 1 秒后再次请求 `/`,确认 worker 是否恢复。
8. 使用 socket 建立 keep-alive 连接,连续发送普通请求、触发请求、普通请求。
9. 重复 keep-alive probe 5 轮,统计连接掉线次数。
10. 根据 crash_count、trigger 连接状态和 follow-up 恢复情况输出 confirmed、suspected 或 not confirmed。
## 11. 使用示例
本地靶场示例:
python3 CVE-2026-9256-poc.py http://127.0.0.1:19321
或:
python3 CVE-2026-9256-poc.py 127.0.0.1 19321
成功触发时,典型输出会表现为:
[+] Connection dropped during trigger request
[+] Worker is responding after trigger (HTTP 200)
round 1: worker likely crashed (keepalive connection dropped)
round 2: worker likely crashed (keepalive connection dropped)
...
[+] VULNERABILITY CONFIRMED - CVE-2026-9256 style crash behavior
[+] Impact confirmed: worker crash / denial of service
[*] RCE is not proven by this script
这类输出说明远程侧观察到了比较稳定的 worker crash 证据。
## 12. PoC 设计中的安全边界
这个 PoC 的安全边界比较清晰:
第一,它只做 crash 验证,不做 RCE 利用。
第二,它没有构造堆喷、ROP、ASLR 绕过、shellcode 或命令执行逻辑。
第三,它的成功标准是 worker 连接掉线和服务恢复,不是拿 shell 或读取文件。
第四,它适合用于本地复现、漏洞验证、IDS/IPS 规则构造和修复前后对比测试。
如果要进一步增强安全性,可以增加以下限制:
1. 只允许访问 `127.0.0.1`、`localhost`、私有地址或明确授权的实验网段。
2. 增加 `--plus-count` 参数,避免默认就发送过大的 payload。
3. 增加 `--route` 参数,让用户显式指定触发路径,而不是写死 `/api/`。
4. 增加 `--rounds` 参数,控制 keep-alive 探测次数。
5. 增加 HTTPS 支持,否则当前 keep-alive socket 逻辑只适合 HTTP 明文服务。
6. 增加 `--print-request` 调试参数,打印实际发送的 HTTP 请求,方便和抓包结果对照。
## 13. 对防护规则编写的启发
从 PoC 推导角度看,检测规则不能只盯 `/api/`,因为 `/api` 不是漏洞固定路径,只是当前靶场示例。真正更有价值的检测点应当是:
1. 请求方向必须是客户端到服务端。
2. URI 长度明显异常。
3. URI 中存在大量连续或高密度可触发 rewrite 输出扩张/转义处理的字符,例如大量 `+`,或者 `+`、`&`、`%`、`?`、`#` 等特殊字符混合出现。
4. 目标服务存在 NGINX rewrite 暴露面时,命中风险更高。
如果规则只写死 `/api/++++`,只能覆盖当前 PoC 和当前靶场;如果要覆盖更通用的攻击流量,应当围绕“长 URI + 大量特殊字符密集出现 + HTTP 请求方向 + NGINX rewrite 风险路径”来提取特征。
同时,由于合法业务中也可能存在长 URL 或大量编码字符,规则需要通过长度阈值、字符密度、重复次数和路径上下文共同降低误报。比较稳妥的检测方向是:
长 URI
+
大量可被 NGX_ESCAPE_ARGS 转义扩张的特殊字符
+
请求方向 to_server
+
NGINX rewrite 相关暴露面
而不是单纯检测:
/api/++++
对于 Suricata / Snort 规则而言,如果只想覆盖当前公开 PoC,可以使用连续 `+` 作为强特征之一;如果希望覆盖变体,则需要把特殊字符范围纳入 PCRE,例如 `+`、`%`、`#`、`&`、`?` 以及其他可能被转义扩张的字符。但这类规则也更容易误报,需要配合 `urilen`、字符重复阈值、路径约束和 NGINX 资产范围共同使用。
## 14. 总结
CVE-2026-9256 的 PoC 推导重点不是寻找固定漏洞路径,而是先理解漏洞触发条件:脆弱 rewrite 配置、重叠捕获组、多捕获变量引用,以及能够让 rewrite 处理结果异常扩张的特殊 URI 输入。
当前脚本选择 `/api/` 加 4096 个 `+`,是因为该路径能命中当前复现环境中的 rewrite 规则,且大量 `+` 能稳定制造长输入压力。脚本没有实现 RCE,而是通过连接断开、服务恢复和 keep-alive 多轮掉线来证明 worker crash。
同时,`+` 只是最稳定、最容易发送的默认字符,不是唯一可能触发的字符。凡是会在 `NGX_ESCAPE_ARGS` 模式下扩张为 `%XX` 的字符,都应纳入原理分析和防护规则考虑范围。更准确的理解应该是:长 URI 中密集出现可转义扩张字符,进入脆弱 rewrite 捕获和 replacement 处理后,触发长度计算与实际写入不一致,最终造成 worker crash 或更严重的内存破坏。