Skip to content
Exploit for Improperly Controlled Sequential Memory Allocation in Php

Exploit for Improperly Controlled Sequential Memory Allocation in Php

Sploitus • October 23, 2026

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

Extracted Entities

Attack Types (1)

CWE Weaknesses (1)

Domains (1)

Platforms (1)