Back Sploitus Exploit for Improperly Controlled Sequential Memory Allocation in Php
mod_php / PHP-FPM **pre-auth** heap-corruption → RCE, reachable before any application code runs.
This repository is a working, configurable implementation of the chain plus the tooling we needed to
make it usable outside a lab: a one-command checker, a per-build offset builder for stripped distro
packages, and the lab harness used to validate everything.
> Authorized testing only. The `--active` checker and the exploit both make the target fail requests
> (one OOM per probe, and a worker abort on vulnerable builds). Do not point them at hosts you do not own
`php_request_startup()` calls `sapi_activate()` — which reads and parses the POST body — **before**
`php_hash_environment()` resets `PG(http_globals)`. If the multipart parser hits `memory_limit` during
that window, `zend_bailout()` long-jumps out with `PG(http_globals)[*]` still holding the * *
request's `IS_ARRAY` zvals, whose memory has already been freed. Apache mod_php calls
`php_request_shutdown()` regardless of the failed startup, and its step 8 does
`zval_ptr_dtor(&PG(http_globals)[i])` on those dangling pointers → double free / use-after-free of
request memory. A second variant (CVE-2026-109619) leaves worker-level error/session/backtrace pointers
aimed at freed request memory, consumed by the request before normal init.
Because this happens during startup, **no application vulnerability is required** — a page that only
# 4) offsets for a build you care (see docs/OFFSETS.md)
python3 offsets.py /path/to/libphp8.4.so ./out/offsets.env
python3 zif_recover2.py /path/to/libphp8.4.so --expect 0x24a330
`poc.py --help` lists the knobs that matter in the field: `--no-keepalive` (old behaviour, one
connection per request), `--libphp` (derive offsets from a local `.so`, no docker), `--force` /
`--no-gate`, `--allow-unsupported` (< 8.0), `--huge-mb`, `--retries`.
The upstream PoC assumes a single-worker prefork with `KeepAlive Off` and 8 MB of memory. Real targets
are nothing like that; the following were required, each one found by a specific failure:
| keep-alive single socket (pins one prefork child) | on multi-worker prefork, one-connection-per-request scatters stage 1/2/3 across different children, so the leaked heap and `PS(http_session_vars)` are in different processes |
| TLS verification disabled | targets are addressed by IP; cert CN never matches, and the old code failed with `CERTIFICATE_VERIFY_FAILED` before sending anything |
| `preflight()` gate | refuses to fire blind; measures `display_errors` and, when possible, `memory_limit` |
| automatic `huge` sizing | a fixed 12 MB field can never OOM a 128 MB limit; the probe reads `Allowed memory size of N bytes` and sizes the field to N+4 MB |
| version gate (< 8.0 refused) | the zend_string / zend_reference / zend_execute_data / HashTable layouts here are 8.x; 5.x and 7.x are a different port, not a flag |
| `--libphp` + `zif_recover2.py` | distro packages are stripped and lack `zif_system`; see `docs/OFFSETS.md` |
Verified matrix (all on local docker, `php:8.4.26-apache` = vulnerable, `8.5.12` = fixed):
| 8.4.26, single worker, KeepAlive Off | `PROOF: uid=33(www-data)` |
| 8.4.26, `memory_limit=128M`, `post_max=256M`, **4 prefork workers**, KeepAlive On | auto-sized to 132 MB, `PROOF` |
| 8.5.12 patched | `HEAP_LEAK_FAILED` ×3 |
| 8.4.26 with `display_errors=Off` | `HEAP_LEAK_FAILED` ×3 |
| `--no-keepalive` legacy path | `PROOF` |
poc.py exploit: rotation → heap leak → libphp leak → fake reference → system()
check.py non-destructive checker (version + optional behavioural OOM probe)
offsets.py libphp.so → zif_system / zval_ptr_dtor offsets (symtab or container)
zif_recover2.py recover zif_system from a STRIPPED .so via .rela.dyn function-table slots
dtor_of_deb.py pull zval_ptr_dtor offset straight out of a .deb
build_offsets.py .deb → offsets (helper)
sweep.py sweep the chunk-relative carrier offset (per-build calibration)
oom_diff.py minimal differential harness (vulnerable vs patched)
lab_setup.sh docker lab: 8088 vulnerable / 8089 patched
docs/OFFSETS.md per-build offset table + how to obtain/derive it
docs/LAB.md lab reproduction report, evidence inventory
evidence/ raw logs from the validation runs
- Command length is capped at 14 bytes by the 167-byte carrier (`sh</out/c` style bootstrap).
- The leak and the fire must land in the same worker: single worker, or a pinned keep-alive
- `memory_limit=-1` removes the controllable OOM entirely; the chain cannot start.
- Per-build offsets are mandatory and shift even between adjacent patch releases (and differ between
distro builds of the same version). See `docs/OFFSETS.md` for the table and the gaps.
- Tested on x86-64 / mod_php / prefork only.
MIT. The exploitation chain derives from the public research PoC by 9bie; this repository adds the
lab harness, the differential/probe tooling, the offset recovery for stripped packages, and the
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.
