PHP2Shell: The Critical PHP Engine Vulnerability That Broke the Internet
In October 2026, two devastating CVEs rocked the PHP ecosystem. Dubbed PHP2Shell, these weren’t your typical WordPress plugin bugs or sloppy eval() calls, they lived deep inside PHP’s core engine, turning every PHP-powered server on the planet into a potential attack surface. No authentication required. No application code involved. Just a crafted HTTP request and you’re in.
What Is PHP2Shell?
PHP2Shell is the informal name given to a pair of critical Remote Code Execution (RCE) vulnerabilities tracked as:
| CVE ID | Name | CVSS v4.0 | Core Issue |
|---|---|---|---|
| CVE-2026-109618 | OOM Corruption | 9.2 (Critical) | Memory corruption via longjmp() during Out-of-Memory handling |
| CVE-2026-109619 | Stale Global State | 9.2 (Critical) | Use-After-Free via stale pointers persisting across PHP-FPM workers |
These vulnerabilities affect PHP versions 5.4 through 8.5 (prior to the emergency patches), covering over a decade of PHP releases. The attack is unauthenticated, meaning no login, no session, no valid endpoint is needed, just a reachable PHP server.
Why “PHP2Shell”?
The name captures exactly what the exploit chain achieves: from PHP straight to shell. A single crafted HTTP POST request can trigger memory corruption deep in the interpreter, hijack execution flow, and drop the attacker into a reverse shell running as www-data. No web shell upload. No file write. Just memory manipulation at the engine level.
The Affected Attack Surface
Because the bugs live in PHP’s C-level request lifecycle, not in any PHP userland code, the impact is universal:
- Every SAPI: Apache
mod_php, PHP-FPM,php-cgi, LiteSpeed, FrankenPHP - Every framework: Laravel, Symfony, Django-adjacent PHP tools, WordPress, Drupal, Magento, Nextcloud
- Every hosting model: Shared hosting, VPS, Kubernetes pods, Docker containers, serverless PHP runtimes
If it runs PHP and accepts HTTP requests, it was vulnerable.
Deep Dive: How the Exploits Work
CVE-2026-109618, The OOM Abort Trap
This vulnerability exploits how PHP handles Out-of-Memory (OOM) conditions using C’s setjmp/longjmp mechanism.
The Normal Flow
Under normal operation, when PHP hits memory_limit, it needs to abort the current request immediately. PHP does this using longjmp(), a low-level C function that performs a non-local jump, instantly teleporting execution to a previously saved checkpoint (setjmp). Think of it as an emergency eject button.
1
2
3
4
5
6
7
8
9
Normal request lifecycle:
[Request Start] → [Parse Headers] → [Parse Body] → [Execute Script] → [Cleanup] → [Done]
↓
memory_limit hit!
↓
longjmp() fires
↓
[Emergency Cleanup] → [Done]
The Problem
The critical flaw is: longjmp() doesn’t unwind the stack. Unlike C++ exceptions or Go’s defer/panic, it doesn’t call destructors, free temporary allocations, or finalize partially-initialized structures. It just jumps.
When PHP is mid-way through parsing a multipart form/file upload and an OOM occurs:
- Internal data structures (file metadata, boundary markers, upload tracking) are half-initialized
longjmp()fires, bypassing all cleanup for those partial structures- The shutdown handler runs and tries to free everything, including the garbage pointers left behind
- This results in heap corruption: freeing uninitialized memory, double-frees, or writing to already-reclaimed addresses
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Simplified pseudo-code of what goes wrong
void parse_multipart_body(request *req) {
upload_entry *entry = zmalloc(sizeof(upload_entry)); // Step 1: Allocate
entry->filename = NULL; // Step 2: Partial init
// ... more field initialization happening here ...
entry->metadata = zmalloc(large_computed_size); // Step 3: OOM HERE!
// longjmp() fires, we never reach the rest of init
// But entry is already registered in the request's cleanup list
// with entry->metadata pointing to garbage
entry->filename = zstrdup(parsed_name); // Never reached
entry->is_valid = true; // Never reached
}
void request_shutdown(request *req) {
for (entry in req->uploads) {
if (entry->filename) zfree(entry->filename); // UAF or wild pointer!
if (entry->metadata) zfree(entry->metadata); // Freeing garbage!
}
}
The Exploitation Primitive
An attacker sends a carefully sized multipart POST request designed to:
- Trigger OOM at a precise moment, during upload entry construction
- Groom the heap beforehand, fill the allocator with controlled data blocks so that when the corrupted free occurs, it hits attacker-influenced memory
- Hijack a function pointer or vtable, redirect execution to a ROP chain or shellcode
CVE-2026-109619, The Stale Worker Ghost
This vulnerability targets persistent PHP worker processes (PHP-FPM, mod_php) that handle multiple sequential requests.
The Setup
In production PHP deployments, worker processes don’t die after each request. A PHP-FPM pool might have 50 workers, each handling thousands of requests over their lifetime. This is efficient, but it means global state must be perfectly cleaned between requests.
1
2
3
4
5
6
7
PHP-FPM Worker Lifecycle:
Worker Process (PID 12345)
├── Request #1: GET /index.php → Process → Cleanup → ✓
├── Request #2: POST /upload.php → Process → CRASH → Incomplete Cleanup → ⚠️
├── Request #3: GET /about.php → Process → reads STALE POINTERS → 💀
└── ...
The Problem
When Request #2 crashes or hits a fatal error condition (via the OOM vector from CVE-2026-109618, or other error paths), certain global variables are not reset:
- Error backtrace caches retain pointers to freed request memory
- Session upload progress (
session.upload_progress) callbacks hold references to deallocated structures - Internal module state hooks point to reclaimed per-request arenas
When Request #3 arrives on the same worker, the engine reads these stale pointers before they’re overwritten. The attacker controls what now occupies that memory (via heap spraying in Request #3’s body), turning a dangling pointer into arbitrary code execution.
The session.upload_progress Amplifier
The session.upload_progress feature, enabled by default in most PHP installations, is the primary amplifier for this attack chain:
1
2
3
4
; php.ini, these defaults make exploitation trivial
session.upload_progress.enabled = 1 ; ON by default!
session.upload_progress.cleanup = 1
session.upload_progress.prefix = "upload_progress_"
This feature parses multipart POST data before any PHP script executes, meaning:
- No authentication checks have run yet
- No framework middleware has loaded
- No WAF rules at the application layer apply
- The attack surface is the raw PHP engine itself
The Full Attack Chain
Here’s how a real-world PHP2Shell exploit flows, step by step:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Step 1: Reconnaissance
└── Attacker identifies target running PHP (trivially via headers, error pages, etc.)
Step 2: Heap Grooming (Request A)
└── Send carefully sized POST requests to shape the worker's heap layout
└── Fill allocator bins with controlled data patterns
Step 3: Trigger OOM Corruption (Request B)
└── Send malicious multipart POST with:
├── session.upload_progress tracking name
├── Deeply nested/oversized boundary structures
└── Payload sized to trigger OOM during parse_multipart_body()
└── longjmp() fires → half-initialized structures left in cleanup queue
└── Worker survives but global state is now poisoned (CVE-2026-109619)
Step 4: Exploit Stale Pointers (Request C)
└── New request hits the SAME worker process
└── Heap spray in POST body occupies freed memory regions
└── Engine dereferences stale global pointers → reads attacker data
└── Function pointer hijack → RCE as www-data
Step 5: Post-Exploitation
└── Reverse shell, lateral movement, persistence
Who Was Affected?
Everyone running PHP. But some configurations were at higher risk:
| Configuration | Risk Level | Why |
|---|---|---|
Apache + mod_php | 🔴 Critical | Single-process model; stale state reuse is trivial |
| PHP-FPM (default pools) | 🔴 Critical | Persistent workers; default session.upload_progress enabled |
| PHP-FPM (strict recycling) | 🟡 High | Workers recycled frequently reduce stale state window |
php-cgi (fresh process/request) | 🟡 High | CVE-109618 still applies; CVE-109619 mitigated by process isolation |
| Containerized + read-only FS | 🟡 High | RCE still possible; post-exploitation limited |
Real-World Impact
- WordPress (43%+ of the web), universally affected at the PHP layer
- Magento / Adobe Commerce, e-commerce platforms handling payment data
- Nextcloud, self-hosted file storage with sensitive documents
- Laravel Forge / Vapor, managed PHP hosting platforms scrambled for patches
- cPanel / Plesk, shared hosting panels with hundreds of tenants per server
Emergency Remediation
Patch Immediately
The PHP core team released emergency patches. Upgrade now:
| PHP Branch | Patched Version |
|---|---|
| PHP 8.5 | 8.5.12 |
| PHP 8.4 | 8.4.27 |
| PHP 8.3 | 8.3.36 |
| PHP 8.2 | 8.2.35 |
| PHP 8.1 and older | ⚠️ End-of-Life, no official patch |
1
2
3
4
5
6
7
8
9
10
11
12
13
# Check your current PHP version
php -v
# Ubuntu/Debian, update via package manager
sudo apt update && sudo apt upgrade php
# Or use Ondřej Surý's PPA for latest versions
sudo add-apt-repository ppa:ondrej/php
sudo apt update && sudo apt install php8.4
# Docker, update your base image
# FROM php:8.4.26-fpm ← VULNERABLE
# FROM php:8.4.27-fpm ← PATCHED ✓
Temporary Mitigations (If Patching Is Delayed)
If you absolutely cannot patch immediately, apply these defense-in-depth measures:
1
2
3
4
5
6
7
8
; php.ini, disable the primary attack amplifier
session.upload_progress.enabled = 0
; Reduce memory limit to shrink OOM trigger window (tradeoff: may break large uploads)
memory_limit = 64M
; Aggressive worker recycling (php-fpm.conf)
pm.max_requests = 100
1
2
3
4
5
6
7
8
9
# Nginx, block oversized multipart bodies at the reverse proxy
client_max_body_size 2M;
# Rate-limit POST requests to slow down heap grooming
limit_req_zone $binary_remote_addr zone=post_limit:10m rate=10r/s;
location ~ \.php$ {
limit_req zone=post_limit burst=5 nodelay;
# ... existing fastcgi config
}
Detection: Are You Being Targeted?
Look for these indicators in your access and error logs:
1
2
3
4
5
6
7
8
9
10
11
# Unusual multipart POST floods to random endpoints
grep -c "POST.*multipart" /var/log/nginx/access.log
# PHP-FPM child processes dying unexpectedly
grep -i "child.*exited\|segfault\|signal 11" /var/log/php-fpm.log
# OOM kills in system logs
dmesg | grep -i "oom.*php"
# Abnormal session file creation patterns
ls -la /tmp/sess_* | wc -l
Lessons for the PHP Ecosystem
1. Memory-Unsafe Languages in Critical Infrastructure
PHP is written in C. The longjmp() / setjmp() pattern is a known footgun in C programming, it bypasses RAII, doesn’t respect stack unwinding, and leaves resource management entirely to the programmer’s discipline. This class of bug is structurally impossible in memory-safe languages like Rust or Go.
2. Default-On Features Are Attack Surface
session.upload_progress being enabled by default, with pre-authentication parsing of untrusted input, is a textbook example of unnecessary default attack surface. The principle of least privilege should extend to default configurations.
3. Persistent Workers Need Paranoid State Isolation
Any server architecture that reuses processes across trust boundaries (different HTTP requests from different users) must treat state isolation as a security-critical invariant, not a performance optimization detail.
4. Defense in Depth Matters
Organizations that had:
- Reverse proxies with request size limits
- WAFs blocking anomalous multipart patterns
- Aggressive PHP-FPM worker recycling
- Network segmentation limiting
www-datablast radius
…bought themselves critical time between disclosure and patching.
Timeline
| Date | Event |
|---|---|
| 2026-09-14 | Vulnerability discovered by Wiz research team |
| 2026-09-15 | Responsible disclosure to PHP security team |
| 2026-10-01 | PHP core team confirms both CVEs |
| 2026-10-07 | Emergency patches released for PHP 8.2–8.5 |
| 2026-10-08 | CERT-EU and NCSC-NL publish advisories |
| 2026-10-10 | First in-the-wild exploitation attempts detected |
| 2026-10-14 | Major hosting providers (cPanel, Plesk, Cloudways) push forced updates |
| 2026-10-21 | You’re reading this. Have you patched yet? |
Quick Reference Checklist
- Check your PHP version:
php -v, are you on a patched release? - Disable
session.upload_progressif not needed:session.upload_progress.enabled = 0 - Update Docker base images to patched PHP versions
- Scan your infrastructure for forgotten PHP installations (monitoring panels, legacy apps, phpMyAdmin)
- Review PHP-FPM pool configs: set
pm.max_requeststo force periodic worker recycling - Add reverse proxy limits: cap
client_max_body_sizeand rate-limit POST requests - Check vendor appliances: firewalls, NAS devices, and routers often bundle PHP internally
- Monitor logs for segfaults, OOM kills, and unusual POST traffic patterns
Final Thoughts
PHP2Shell is a sobering reminder that the most dangerous vulnerabilities aren’t in your code, they’re in the runtime beneath it. You can write the most secure Laravel application in the world, follow every OWASP guideline, have 100% test coverage, and still be popped by a crafted POST request that never even reaches your router.
The PHP core team responded quickly with patches. The community rallied with detection tools and mitigations. But the window between disclosure and universal patching is where real damage happens.
Patch your servers. Today. Not tomorrow. Not after the sprint review. Now.
Disclaimer: This article is written for educational and defensive security purposes. Understanding how vulnerabilities work is essential for building secure systems. Always practice responsible disclosure and authorized testing.