Sploitus

Exploit for CVE-2026-42533

githubexploit Β· 2026-08-05

Exploit Code

README246 lines
## https://sploitus.com/exploit?id=52C11545-FECA-52FC-A7F3-BAAE557BE349
# CVE-2026-42533

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)

## Affected

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.

## Layout

```
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.

## Contents

`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//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...)` 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`.

## Requirements

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.

## Build

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.

```sh
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)"
```

## Run

`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`.

### Overflow, with an ASan trace

```sh
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`.

### Info leak

```sh
../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.

### Full chain

```sh
../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.

### Deterministic full chain

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.

```sh
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`:

```sh
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.

### All 13 overflow sites

```sh
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
```

### Named capture variant

```sh
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/named_capture_poc.py    # ASan build
```

### Leak on two more sinks, stock config

```sh
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//maps`.

## Troubleshooting

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.

| Symptom | Why | What to do |
| --- | --- | --- |
| `--leak` shows no pointers, or `--rce` / `--rce-det` never land | You are on the ASan build; AddressSanitizer poisons freed memory, so the residue carries no real pointers | Use the clean `objs.dbg` build for `--leak`, `--rce`, `--rce-det`, and `leak_multi.py`. The ASan `objs` build is only for `--crash`, `test_all_sites.py`, and `named_capture_poc.py`. |
| `--rce` lands only about 2 in 3 tries | Single shot against a general config; the freed trigger-body location varies | Expected. On a miss the worker crashes and respawns, so just run it again. For a deterministic single shot use `--rce-det`. |
| `--rce-det` prints `No RCE. Recalibrate PL_OFF/HEAP_PAGE_OFF` on every run | `PL_OFF` is specific to your build, glibc, config, and the nginx `-p` prefix path length. The shipped value is for this repo's dev build launched with `-p run`. | Run `exploits/calibrate.py`, paste the `PL_OFF` it prints into `exploits/poc.py`, and start nginx with the **same** `-p` prefix you gave calibrate.py (the README uses `-p run`). |
| `--rce-det` occasionally prints `0x0a (regex-hostile) ... retry` | An ASLR draw put a `0x0a` (newline) byte in an address, which the `map` regex cannot carry | Not a failure. Rerun; the next draw almost always avoids it. |
| nginx or `calibrate.py`: `bind() to 127.0.0.1:8950 failed (Address already in use)` | A previous nginx still holds the port | `pkill -x nginx`, wait a second, retry. Keep only one nginx on 8950. |
| A standalone script prints `nginx not found` | `NGINX_BIN` unset or pointing at the wrong build | Set `NGINX_BIN` (clean build for `leak_multi.py`, ASan build for the validators). |
| `No such file` for a config, script, or the nginx binary | You are not in the repo root | `cd` into the repo root first; every command assumes it (`exploits/...`, `configs/...`, `../nginx-1.30.1/...`). |
| The leak works on one config but not another | `worker_connections` below ~256 shrinks the arena, so the freed chunk holds no pointers | Use a normal `worker_connections` (512 to 1024). Production values all leak. |

## Notes on the RCE offsets

`--rce` recovers the libc base and a heap pointer at runtime from the leak, so it
does not hardcode any address. It does hardcode offsets that are specific to the
build and libc it was developed on:

```
LIBC_LEAK_OFFSET   libc base to the leaked arena pointer
SYSTEM_OFFSET      libc base to system()
BODY_DELTA_*       leaked heap pointer to the overflow body buffer
POOL_OFF_FROM_BUF, D_LAST_OFF, D_END_OFF, LOG_OFF   forged pool geometry
```

On a different distro, glibc, or nginx build these need recalibration. Read the
real `system()` offset with `readelf -sW /lib/x86_64-linux-gnu/libc.so.6 | grep
'\bsystem\b'`, and read the pool offsets from the target's `ngx_pool_t`. The
`--crash` and `--leak` modes carry no such offsets and reproduce on any build of
an affected version.

## Notes on the leak

The leak needs no special tuning. It works on a stock `events {}` config, whose
default `worker_connections` (512) sizes the arena so the freed request chunk
lands in a glibc bin still holding arena and heap pointers that the oversized
leak buffer reuses. Only an unusually low `worker_connections` (under ~256)
escapes it; production values (512-1024) all leak.

## Config auditing

A static config scanner that flags the vulnerable pattern without exploiting
anything is at https://github.com/0xCyberstan/CVE-2026-42533-Config-Scanner.

## Authorization

This targets a patched, publicly disclosed vulnerability. It exists for defenders
verifying exposure and for research reproduction. Run it only against nginx that
you own or are explicitly authorised to test. Upgrade to 1.30.4 or 1.31.3.