A write-up and reproduction bundle for the php2shell use-after-free → RCE
chain against php:8.4.26-apache, built on top of
9bie/php2shell-bypass-aslr-exp.
This repo exists so people can study how the chain works: a request-by-request report, animated diagrams, a live gdb trace, and a fork of the original PoC with some reliability/usability improvements plus a repeatable lab harness.
⚠️ For authorized security research and education only. Everything here runs against a local Docker target bound to127.0.0.1; no traffic leaves the host. Do not point it at systems you don't own or have written permission to test.
report/ php2shell-report.html full technical write-up (open in a browser;
diagrams/ sit beside it)
php2shell-motion.html animated step-player of the chain
diagrams/ the SVGs used by the report
poc/ exploit.py fork of 9bie's exploit
pure/run_pure.sh brings up the isolated Docker target
lab/labtest.sh reliability harness (-n runs, -w workers, …)
lab/README.md harness docs
fork-9bie.patch the diff vs. the upstream PoC
evidence/ gdb_trace_verified.log live trace: forged-length OOB read, OOM
longjmp, popen
databytes/ builder scripts for the wire/memory bytes
Apache prefork is pinned so all chain requests land on a single worker. An
out-of-memory abort mid-cleanup strands the session globals in a stale state
(A1); a forged length then turns the session cookie into a large
out-of-bounds read that leaks the libphp/heap base, defeating ASLR (B1);
the remaining requests reclaim the freed object and drive the use-after-free to
popen(). Because the two leaks re-derive their bases at runtime, every attempt
survives fresh ASLR, and a crashed worker simply restarts — so repeated attempts
converge on success. The report walks through each request with the memory model
behind it.
Requirements: a running Docker daemon, Python 3, and curl. No image build, no
gdb, no php.ini changes — the target is the stock php:8.4.26-apache image
with the official production config.
# bring up the isolated target and run the exploit once
poc/pure/run_pure.sh
# or run the self-verifying reliability harness
poc/lab/labtest.sh # 10 runs, official prefork workers
poc/lab/labtest.sh -n 20 # 20 runs
poc/lab/labtest.sh -w auto # let the exploit discover the worker countA run counts as success only when the proof file is written inside the target and
contains uid=. See poc/lab/README.md for all options.
Over the upstream PoC:
--workers auto— discover the target's worker count over HTTP (with a keep-alive thread so slow-POST holders survivemod_reqtimeoutduring the measurement), instead of being told it.--probe-timeout/--max-workerstunables for the auto path.run_pure.shcan mount an arbitrarympm_preforkworker count.labtest.sh— a batch harness that restarts the container per run (fresh ASLR each time), counts PASS/FAIL, and reports a success rate.
The full diff is in poc/fork-9bie.patch; apply it onto a
fresh clone of the upstream repo:
git clone https://github.com/9bie/php2shell-bypass-aslr-exp && cd php2shell-bypass-aslr-exp
git apply /path/to/fork-9bie.patchOn the reference host (official php:8.4.26-apache, cgroup v1, overlayfs), a
batch of 8 runs reached RCE 7/8 times (88%). The single miss was an unlucky leak
window; the exploit's own --retries plus re-running the harness recovers it.
Exact offsets are tied to this PHP build — a different build/version needs fresh
calibration.
Original research and PoC: 9bie. This repo is a study fork — the analysis, diagrams, lab harness, and reliability changes are additions on top of that work.