A self-contained, local-only verification lab for the PHP session serialize-handler mismatch and upload-progress key-injection chain.
____ _ _ ____ ____ ____ _ _ _____ _ _
| _ \| | | | _ \|___ \/ ___|| | | | ____| | | |
| |_) | |_| | |_) | __) \___ \| |_| | _| | | | |
| __/| _ | __/ / __/ ___) | _ | |___| |___| |___
|_| |_| |_|_| |_____|____/|_| |_|_____|_____|_____|
Scope: This is an independent adaptation of existing research, not a new vulnerability or CVE claim. The lab is intentionally vulnerable. It binds only to
127.0.0.1and writes a fixed proof page instead of an arbitrary-command webshell.
The lab uses the official php:8.5.11-apache image and demonstrates that:
- The upload-progress callback stores an attacker-controlled session key while the write handler is
php_serialize. - A second endpoint switches the read handler to
php. - The handler mismatch places a serialized
DeferredWriterobject at an object-injection position. - Its destructor writes a PHP proof page into the document root.
- Requesting that page prints
SESSGAP_RCE_OK, the PHP version, and the worker UID.
A negative control repeats the same request without a handler mismatch and verifies that no proof page is created.
- Docker with Compose v2
- Python 3.10 or newer
- No third-party Python packages
From Git Bash, Linux, or macOS:
git clone https://github.com/yatuk/php2shell-session-gap-lab.git
cd php2shell-session-gap-lab
bash run.shOr run the steps separately:
docker compose up -d --wait
python verify.pyExpected output:
SESSGAP_LAB_OK php=8.5.11
PASS exploit: http://127.0.0.1:18085/proof_<random>.php
SESSGAP_RCE_OK php=8.5.11 uid=33
PASS negative control: no handler mismatch, no proof file
Each run generates fresh session IDs and proof filenames. Generated proof files and Python bytecode are excluded from Git.
Stop the lab:
docker compose downTo remove generated proof files as well:
rm -f html/proof_*.php html/control_*.phpThis chain is not an unconditional exploit against a default PHP installation. The lab deliberately configures all of the following:
php_serializeon the session write path andphpon the read path.session.upload_progress.enabled=1.session.upload_progress.cleanup=0, so the injected key remains after a completed upload.- An application gadget whose destructor performs a file write.
- A document root writable by the PHP worker.
The last two conditions turn object injection into the proof-page write. A real application would need a compatible gadget and a useful write or execution path.
verify.py— generates the multipart request and PHP serialization payload, executes the chain, and checks the negative control.compose.yaml— loopback-only PHP/Apache lab and health check.php.ini— intentionally vulnerable session configuration.html/up.php— multipart upload endpoint.html/read.php— switches to thephpread handler.html/read_control.php— keeps matching write/read handlers.html/gadget.php— demonstrationDeferredWritergadget.run.sh— starts the lab and runs verification.
-
Disable upload progress when it is not needed:
session.upload_progress.enabled = 0 -
Use one session serialize handler everywhere. Do not switch handlers between routes, virtual hosts, directories, or application bootstrap paths. Consistent handlers prevent this format-confusion chain, but do not replace an upstream fix.
-
Remove gadget reachability and unsafe side effects. Application destructors and wakeup methods must not write files or perform other sensitive actions using attacker-controlled properties.
-
Do not let PHP workers write into the executable document root. Use a separate non-executable upload directory with least-privilege permissions.
-
Enable session cleanup and strict session-ID handling as defense in depth:
session.upload_progress.cleanup = 1 session.use_strict_mode = 1 session.use_only_cookies = 1
These settings are not standalone fixes for every upload-progress or slow-upload variant. In particular, this lab already supplies a cookie, so
use_only_cookiesalone does not stop the demonstrated chain. -
Track the upstream issues and install fixed PHP releases when available. A closed issue does not by itself prove that every supported release branch has received the fix; verify the exact deployed version.
A robust engine-level fix should:
- Validate the upload-progress key before it reaches session storage.
- Reject malformed input before opening, truncating, or destroying an existing session.
- Avoid invoking
__wakeup(),__unserialize(), or destructors for objects belonging to a session payload that has already been rejected. - Keep session encode/decode grammars consistent and fail safely on trailing or malformed data.
Status checked on 2026-10-24. These entries cover related behavior; they are not all the same vulnerability.
| Topic | Reference | Status when checked |
|---|---|---|
Rejected session_decode() payload still invokes __wakeup() and __destruct(); includes the php_serialize/php mismatch shape |
php-src#24518 | Open; no linked fix PR found |
var_destroy() re-entrancy UAF during unserialize() |
php-src#24481 | Open |
| Destructor-pass fix for GH-24481 | php-src#24551 | Open |
| Reset freed unserialize var entries before destructors | php-src#24532 | Open |
| Alternative hardening for GH-24532 | php-src#24538 | Open |
| Upload progress destroys sessions containing reserved property names | php-src#24493 | Open |
| Fix for GH-24493 | php-src#24521 | Open |
serialize() context reused by destructors during teardown |
php-src#24373, php-src#24452 | Closed; see commit 08f4b64 |
| SimpleXML GC double-free regression | php-src#24488, php-src#24494 | Closed |
C: serialization format and non-serializable internal classes |
php-src#22046 | Closed |
- Original research package and technique: ACD421/PHP, particularly
zero1_session_gaps. - Related upstream report: php-src#24518.
- This adaptation was built for local education, defensive validation, and reproducible testing.
Run it only against your own isolated lab. Do not expose the intentionally vulnerable container to untrusted networks.