Back Sploitus Exploit for Improperly Controlled Sequential Memory Allocation in Php
| ---------- | --------------------------------------- | ------------------------------------------------------------ |
| `vuln` | PHP **8.4.26** + Apache prefork/mod_php | remote code execution as `www-data` |
| `fixed` | PHP **8.4.27** (security release) | no code execution |
| `attacker` | Python 3.12 | runs the exploit against the vulnerable target and prints the `id` and `whoami` proof |
The vulnerable page is just ``. It never calls
`session_start()`: the bug is reached by PHP's `session.upload_progress`
Fixed in 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 (fix commit `31c0ea7d7f`,
[*] stage 3: HTDATA=0x... zif_system=0x...
PROOF: uid=33(www-data) gid=33(www-data) groups=33(www-data)
The command executed by the exploit reads a script from the shared volume.
That script runs `id` and `whoami`, writes both results to the shared
`proof-vuln` volume, and the attacker reads the result. The proof lines are
read from the file; the sample account name and numeric IDs above are not
hard-coded. On success, the PoC stops after the `WHOAMI:` line.
`run_poc.sh` starts only the vulnerable target, hides Docker Compose's setup
and shutdown messages, and stops the lab after the result. The Compose network
is internal and no host port is published.
The local-only scope applies to `run_poc.sh` and the Compose lab. Running
`attacker/exploit.py` directly accepts a caller-supplied URL and command via
`--url` and `--cmd`; use that interface only with systems you are authorised
- `cannot overwrite digest sha256:...` — old Docker versions can trip over a
locally tagged `php:8.4.26-apache` / `php:8.4.27-apache` image while
composing a digest-pinned service. Remove the tags and retry:
docker rmi php:8.4.26-apache php:8.4.27-apache
- `exec format error` on an arm64 host — the amd64 QEMU binfmt handler is not
### Running on arm64 (Apple Silicon, WSL2 on ARM, ...)
The images are pinned to the official **linux/amd64** build because the heap
calibration and `libphp.so` offsets were measured there. On an arm64 host,
docker run --privileged --rm tonistiigi/binfmt --install amd64
This PoC was developed and verified on an arm64 host with that emulation.
PHP keeps a lot of worker-global state that points into the *request* heap.
Normally each request resets it. Two things can go wrong:
1. **Producer.** `session.upload_progress` makes PHP initialize the session
from the multipart callback *before* `RINIT` (inside
`php_hash_environment()`). If a `memory_limit` OOM bails out of
`php_session_initialize()`, the request is torn down with
`PG(modules_activated) = 0`, so the session RSHUTDOWN path is skipped.
`PS(http_session_vars)` (a `zend_reference*`) is left pointing into the
2. **Consumer.** The request on the same worker parses its multipart
body, allocating attacker-controlled field buffers, and only then reaches
`php_session_initialize()` → `php_session_track_init()`. The stale
`PS(http_session_vars)` is dereferenced and destroyed before normal
The OOM is only the *interruption* that abandons session initialization
(the same bailout class is also fixed by the related OOM advisory
GHSA-w8hw-w9fx-j79w). The vulnerability demonstrated here is the stale
session state surviving into the request and being consumed before
`php_session_track_init()` runs `zval_ptr_dtor()` on the stale reference:
zval_ptr_dtor(&ref->val) # forged IS_ARRAY_EX
pDestructor(&bucket->val) # -> zif_system()
The whole exploit is two kinds of ordinary multipart `POST`s. The producer
and consumer must hit the same worker (the lab uses one prefork worker).
When a worker aborts, the prefork parent forks a replacement that inherits
the same address-space layout, which keeps the heap/ASLR leak results valid
The 8.4.27 patch (and the equivalents on 8.2/8.3/8.5) does several things
- replaces OOM `longjmp()` bailouts with delayed/hard OOM handling,
- resets request globals at shutdown (`PG(http_globals)`, last error,
- early-resets session globals at `MULTIPART_EVENT_START`,
- runs upload-progress decode in `in_rfc1867_update` mode with
`__PHP_Incomplete_Class`-only decoding and no user callbacks,
- removes `$_SESSION` before the target script runs.
On 8.4.27 the producer no longer publishes and keeps the dangling session
reference, so the heap leak stops working and the chain cannot proceed.
All three stages use the same stale-session primitive.
1. **Zend MM chunk base.** The producer OOMs. The consumer reclaims the
stale `PS(id)` with a 79-byte field whose `zend_string` length field is
`0x1000`; `php_url_encode()` then reads past the string and
percent-encodes many request-heap pointers into `Set-Cookie`. The
2 MB-aligned mode is the Zend MM chunk base.
2. **libphp base.** A 56-byte HashTable page is placed at the stale
`PS(id)` so that the cookie data begins at
`nNextFreeElement | pDestructor`. `pDestructor` is `zval_ptr_dtor`
(`libphp+0x65eb60`); its address yields the libphp base.
3. **Control flow.** A forged 55-byte reference is placed at `R0-0x10`
(reclaiming the stale `zend_reference`), and a live 167-byte
`zend_string` (a normal multipart field, kept in `$_POST`) carries the
forged HashTable. `pDestructor` is `zif_system`
(`libphp+0x465370`); the forged `zend_execute_data` to the bucket
passes the command string to it. `system()` runs the command before the
Relevant addresses are all chunk-relative or libphp-relative; ASLR is
defeated by the two in-band leaks, not disabled.
├── run_poc.sh # local runner; hides Compose lifecycle output
├── compose.local.yaml # vulnerable target + attacker only
├── compose.yaml # vulnerable + patched + attacker
│ ├── exploit.py # exploit implementation (stdlib only)
│ ├── run_demo.sh # default Compose entry point
│ └── run_local_poc.sh # id and whoami proof driver
├── apache.conf # single prefork worker, KeepAlive Off
├── php.ini # 8M memory_limit, display_errors On, upload progress On
The lab configuration matches the known reachable case:
- `session.upload_progress.enabled=On`, `cleanup=On` (defaults),
- `memory_limit` small enough to OOM during upload-progress session init
- `display_errors=On` for the in-band `Set-Cookie` leaks,
- Apache prefork with a single worker (or at least a way to pin the
producer and consumer to the same worker),
- `session.use_strict_mode=0`, `session.save_path` writable.
Production exploitation is more constrained (the producer/consumer must be
routed to the same worker, and the leaks require displayed errors); the
advisory's point is that the underlying stale-state lifecycle is a critical
The `fixed` service is the official **php:8.4.27-apache** image with exactly
the same Apache/PHP configuration. It can be checked separately; the normal
PoC run ends as soon as the vulnerable target produces the proof.
To check the fixed target explicitly, start the comparison lab and run the
docker compose run --rm attacker python3 /exploit/exploit.py \
-u --proof /proof/fixed/p --cmd 'id>/out/p' --retries 2
`exploit.py` exits 0 only when the proof file contains `uid=`.
`zif_system`, `zval_ptr_dtor` and the 167-byte carrier offset are specific
to the pinned image. Recompute them from the target's `libphp.so`:
docker create --name libphp php@sha256:60111400ff77bdc44fa79b4f072afe49a42abba71e0846ad496be4761dc9c11a
docker cp libphp:/usr/lib/apache2/modules/libphp.so .
nm libphp.so | grep -E ' (zif_system|zval_ptr_dtor)$'
then pass `--zif-offset`, `--dtor-offset` and (after re-calibrating the
heap layout) `--htdata-offset`. Heap offsets are deterministic for a given
build/configuration but may move between PHP versions and architectures.
[cozybear-dev/cve-2026-109619](
at commit `7a73bb50fbc9d0f5aafce9c135bf4cbf4575601b`. The local
adaptation uses an internal Compose network with no published target port,
accepts the lab's heap address range, records `id` and `whoami` in one proof
- PHP security advisory GHSA-6wmf-4rv5-p3rp / CVE-2026-109619 and the fix
commit `31c0ea7d7f` by Arnaud Le Blanc, Ilija Tovilo, Jakub Zelenka,
Nora Dossche, Nicolas Grekas; reporters sagitzadik-wiz, tomash-areion,
- The session upload-progress lifecycle chain and the in-band Set-Cookie
leaks were published in the `9bie/php2shell-bypass-aslr-exp` research
repository and are credited by the upstream PoC.
The upstream repository did not display a license file when this copy was
reviewed on 2026-10-23. This directory does not claim to relicense upstream
code. Confirm redistribution permission before publishing it as an independent
repository; a GitHub fork retains the source relationship.
For authorised security testing and research only. The exploit runs
arbitrary commands as the web server user and intentionally corrupts the
worker heap; run it only against systems you own or are explicitly
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
