
A research exploit demonstrating pure-HTTP remote code execution against an unmodified PHP 8.4.26 mod_php target, built for authorized vulnerability research and patch-validation in isolated lab containers.
| Tool | 9bie/php2shell-bypass-aslr-exp — research exploit achieving RCE against stock PHP 8.4.26 + Apache mod_php via a pure HTTP heap-leak chain |
| Category | Memory-corruption exploit research (web runtime, heap grooming, ASLR bypass) |
| Primary Use | Reproducing and studying a session upload_progress heap exploitation chain against an intentionally vulnerable lab target, for defensive analysis and mitigation validation |
| Safe Use | Explicitly restricted by the author to authorized environments: vulnerability research and reproduction in local Docker containers you own; never against third-party systems |
| Telemetry Note | The chain depends on repeated chunked multipart uploads, Set-Cookie responses carrying anomalously large encoded session IDs, and worker OOM bailouts — patterns visible in Apache access logs, SAPI error paths, and memory-corruption crash telemetry |
9bie/php2shell-bypass-aslr-exp is a Python research project that documents, end to end, how an unmodified PHP 8.4.26 runtime under Apache mod_php on Debian 13 can be driven from a trivial <?php echo "OK\n"; ?> page to remote code execution using nothing but HTTP traffic. The repo's distinguishing claim is purity of conditions: the official Docker image, the stock php.ini-production with display_errors=Off and memory_limit=128M, no session_start() call, no target-side files, no debugger attached to the delivery path. That constraint set matters because most public PHP heap exploit writeups quietly depend on a weakened configuration; this one explicitly archives its earlier display_errors=On, memory_limit=8M lineage under archive/legacy-8m/ to keep the distinction honest.
The author reports 5/5 successful runs against the stock official image and 15/15 against a symbol-carrying build of the same official binaries, with each attempt taking roughly two seconds and leaving the worker alive. Those numbers are self-reported lab results reproduced in logged evidence files (pure/evidence/), which is the correct way to publish this kind of claim — batch logs and a proof file rather than a bare assertion. The environment is intentionally narrow: single-worker Apache prefork, files session handler, NTS build. The README is candid that moving to a full multi-worker production layout changes reliability rather than feasibility, and that recalibration is needed for any other PHP build.
Architecturally the chain is a four-stage heap manipulation dance, and the README's technical narrative (pure/RESEARCH_PURE_128M.md plus the main document) is where the real educational value sits. The first primitive is a size-check bypass: sending a chunked request without Content-Length means rfc1867_post_handler sees content_length=0 and skips the post_max_size gate, allowing arbitrarily large bodies for heap grooming. This is a class of parser-assumption bug worth internalizing for any defender auditing upload handling, independent of the rest of the chain.
The leak stage abuses PHP_SESSION_UPLOAD_PROGRESS, the same session-multipart feature that powers php-session-starvation-style tooling. The author floods the request with hundreds of deeply nested file fields plus a large PHP_SESSION_UPLOAD_PROGRESS value to trigger an out-of-memory bailout during END cleanup inside php_url_encode. Crucially, the bailout leaves dangling PS(id) and PS(mod_data) pointers with send_cookie=1 set. On the follow-up request, the stale PS(id) is steered to point at a forged zend_string header, and the Set-Cookie response dutifully encodes roughly 800KB of adjacent heap memory back to the client. Turning a session cookie into a disclosure oracle is the conceptual heart of the exploit.
What gets disclosed is what makes the ASLR bypass work. The leaked window contains clusters of 2MiB-aligned chunk pointers that reveal the main heap arena base, and — because nested array fields refill a hole left in the id chunk — real HashTable structures whose pDestructor field repeatedly holds the address of zval_ptr_dtor. That gives two independent anchors: heap base and, by subtracting a build-fixed offset, the libphp load address. The exploit validates each leak by sanity-checking that the dominant pointer falls in a plausible libphp range, discarding bad windows and retrying — a --retries discipline that explains the reliability figures.
The final stage is a controlled-write variant of the same OOM trick: a cheap allocation failure redirects PS(hash_serialize_verifier) to a predictable heap location, which a subsequent reference-typed multipart value overwrites to point at a forged zend_array whose pDestructor is set to zif_system. At shutdown, zend_hash_destroy invokes that destructor with an attacker-supplied string. The README publishes the build-specific offsets (zval_ptr_dtor at libphp+0x65EB60, zif_system at libphp+0x465370, the forged array at a fixed main offset) as calibration data for this exact binary — a detail that underlines how tightly version-locked the whole chain is.
For defenders, the detection surface is unusually rich because the chain is loud at the protocol level. Requests are chunked multipart uploads lacking Content-Length while carrying bodies far exceeding any sane post_max_size; responses include Set-Cookie headers with abnormally large session-ID payloads; and the grooming traffic produces repeated OOM bailouts in the SAPI layer that may surface in error logs or crash-reporting pipelines even when display_errors is off. A WAF or Apache-level rule flagging PHP_SESSION_UPLOAD_PROGRESS values in the hundreds of kilobytes, or cookie echoes above normal session-ID length, would catch the tooling pattern described here.
From a mitigation standpoint, the chain argues for several familiar hardening layers: disabling session.upload_progress.enabled where multipart upload progress bars are not used, capping max_input_vars and nesting depth, and treating unusual Transfer-Encoding: chunked POST bodies to PHP endpoints as suspect. The deeper fix belongs upstream — the dangling PS() state after OOM bailout is a lifecycle bug in session cleanup, and the size-check bypass in rfc1867 handling is a parser hardening issue. Reading this repo is effectively a case study in how memory-lifecycle bugs compose with protocol-level assumption gaps into a full RCE primitive.
The tooling itself is deliberately spare: a single exploit.py entry point and pure/run_pure.sh for standing up the vulnerable container locally (git clone https://github.com/9bie/php2shell-bypass-aslr-exp.git), with a --dbg flag reserved for offline offset calibration against a symbol-carrying build of the same version. The author's reproduction statement closes by restricting use to authorized environments for vulnerability research, which matches how the evidence and calibration artifacts are structured. For exploitation researchers and PHP internals maintainers this is a dense, well-documented reference chain; for everyone else it is best read as a warning about what an unmodified production PHP configuration will tolerate over plain HTTP.
9bie/php2shell-bypass-aslr-exp.Educational analysis for authorized security professionals. Use only in controlled, authorized environments.
Related coverage
0 comentários:
Post a Comment
Note: Only a member of this blog may post a comment.