Critical Severity CVE-2026-109618
Published: Nov 9, 2026

[GitHub] php2shell-exp

Rce
Type
N/A
CVSS Score
2
Views
Anonymous
Author

Description

Patch-derived PoC and RCE for the PHP session upload-progress pre-RINIT decode (CVE-2026-109618/CVE-2026-109619)

# php2shell-exp

PoC and RCE reverse-engineered from the PHP official patch [`3584a98bda`](https://github.com/php/php-src/commit/3584a98bda), with dual-container differential environment for 8.2.34 / 8.2.35.

Patch-derived PoC and RCE for the pre-RINIT session decode in PHP's upload-progress handler.

## What this is

- `exp/poc_pre_rinit.py` — Demonstrates that upload-progress decodes session before RINIT and executes object destructors without command execution
- `exp/exp_rce.py` — Executes arbitrary commands on the same code path, with output read back via HTTP
- `exp/poc_oom.py` — OOM trigger for CVE-2026-109618 (trigger only, not RCE)
- `lab/` — `php:8.2.34-fpm` and `php:8.2.35-fpm`, each with nginx

This chain works stably on 8.2.34 and is blocked on 8.2.35 by `incomplete_classes_only`. It is not a zero-dependency exploit: the target must already have a class with dangerous magic methods before RINIT, which the lab simulates using `opcache.preload`.

## Verified

Same code, same configuration, only image changes:

| Branch | Vulnerable | Patched |
|---|---|---|
| 8.1 | 8.1.34 (EOL, no official patch) | — |
| 8.2 | 8.2.34 | 8.2.35 |
| 8.3 | 8.3.35 | 8.3.36 |
| 8.4 | 8.4.26 | 8.4.27 |
| 8.5 | 8.5.11 | 8.5.12 |

Output of `./verify.sh` on the 8.2 branch:

```
PoC 8.2.34 (expect 0) 0
PoC 8.2.35 (expect 1) 1
RCE 8.2.34 (expect 0) 0
RCE 8.2.35 (expect 1) 1
[+] differential holds
```

Full output and call stack diagnostics in [`docs/RESULTS.md`](docs/RESULTS.md).

## How it works

The patch marks upload-progress decoding as "pre-RINIT, no user code allowed" and enables `incomplete_classes_only` for that phase. Before the patch, this decoding instantiates objects from session records as real classes, with `__wakeup` / `__unserialize` / `Serializable::unserialize` / `__destruct` all running before the target script compiles.

The exploit proceeds in two HTTP steps:

1. `POST /app/upload.php`. A `.user.ini` file in that directory sets `session.serialize_handler=php_serialize`; upload-progress writes the multipart filename as-is into the session file, `x|O:6:"PrePwn":...` including the pipe.
2. `POST /api/pre.php`. This directory uses the default `php` handler; the script has no `session_start()`. During request startup, `php_session_rfc1867_update()` still decodes: the `php` decoder splits at the first `|` and passes the remainder to `unserialize()`.
3. On 8.2.34, the object is created as a real class, `__destruct()` executes `shell_exec($cmd)`, and output is written to a random file under webroot.
4. On 8.2.35, the same data is downgraded to `__PHP_Incomplete_Class`, which has no destructor and produces no file.

The payload is explicitly constructed by `exp/lib2shell.py::php_object()`, with length prefixes calculated in bytes:

```
x|O:6:"PrePwn":2:{s:3:"cmd";s:12:"id; hostname";s:3:"out";s:33:"/var/www/html/.pwn_xxxxxxxx.txt";}
```

Point-by-point correspondence between patch hunks and code is in [`docs/DERIVATION.md`](docs/DERIVATION.md).

## Quick start

Requires Docker Compose and Python 3.8+ (standard library only).

```

Translated to English from Chinese. Show original · see the source links.
Community Rating
0

Login to rate this exploit

Quick Actions