Vulnerability
PHP2Shell Deep Dive: Dissecting CVE-2026-109618 & CVE-2026-109619 Unauthenticated RCE in PHP Core
An exhaustive technical analysis of the PHP2Shell vulnerabilities (CVE-2026-109618 and CVE-2026-109619) affecting PHP 5.4 through 8.5. We dissect zend_bailout out-of-memory control-flow flaws, examine session.upload_progress use-after-free conditions, analyze heap grooming mechanics, and provide an enterprise remediation guide.
12 min read
PHP2Shell Deep Dive: Dissecting CVE-2026-109618 & CVE-2026-109619 Unauthenticated RCE in PHP Core
Executive Summary
On October 21, 2026, the PHP core security team issued an emergency, out-of-band security advisory addressing two critical vulnerabilities rated 9.2 under CVSS v4.0: CVE-2026-109618 and CVE-2026-109619, collectively designated by the cybersecurity community as PHP2Shell. Discovered in August 2026 through collaborative AI-assisted vulnerability fuzzing by security researcher Calif, Wiz Research, and OpenAI, the flaws expose a catastrophic architectural weakness in how the Zend Engine manages request heap lifecycles and abnormal memory exhaustion.
The vulnerabilities affect virtually every deployment of PHP dating back to PHP 5.4 through modern PHP 8.5 (specifically patched in PHP 8.2.35, 8.3.36, 8.4.27, and 8.5.12). Because PHP powers over 70% of all public-facing web servers—underpinning global CMS platforms like WordPress, Drupal, Magento, and Joomla, as well as enterprise edge management appliances from vendors like Palo Alto Networks (PAN-OS) and Citrix—the theoretical blast radius of PHP2Shell is immense.
What makes PHP2Shell exceptionally dangerous is its zero-authentication requirement and network accessibility. The exploit chain targets PHP's built-in session.upload_progress subsystem, an engine feature enabled by default in standard distributions even when applications do not explicitly use PHP sessions. By combining a memory-exhaustion trigger using multipart file uploads with an abrupt control-flow escape via zend_bailout() (longjmp), attackers leave global engine pointers pointing into freed request memory. When the persistent PHP-FPM or Apache mod_php worker handles a subsequent HTTP request, it dereferences these stale pointers, executing a classic Use-After-Free (UAF) condition that can be groomed into full remote code execution under the privileges of the web worker account (www-data).
This technical deep dive provides an authoritative systems and vulnerability engineering analysis of PHP2Shell. We dissect the internal memory layout of the Zend Engine heap, trace the abnormal termination mechanics of zend_bailout(), evaluate the heap grooming primitives abusing session.upload_progress, examine telemetry from in-the-wild exploitation attempts, and deliver an enterprise hardening and incident response playbook.
+---------------------------------------------------------------------------------------------------+
| PHP2Shell (CVE-2026-109618 & CVE-2026-109619) Attack Anatomy |
+---------------------------------------------------------------------------------------------------+
| |
| [ Attacker HTTP Client ] |
| | |
| | (1) Attacker sends crafted multipart/form-data POST with PHP_SESSION_UPLOAD_PROGRESS |
| | Payload forces request heap allocation near memory_limit boundary |
| v |
| +---------------------------------------------------------------------------------------------+ |
| | PHASE 1: ABNORMAL REQUEST TERMINATION (CVE-2026-109618) | |
| | - Zend Engine allocates memory for session tracking in request heap | |
| | - Allocator encounters Out-Of-Memory (OOM) or fatal allocation failure | |
| | - Invokes zend_bailout() -> longjmp() bypasses normal cleanup code | |
| | - Request memory structures left incompletely initialized and traversed during destruction | |
| +---------------------------------------------------------------------------------------------+ |
| | |
| | Request heap destroyed; persistent worker process survives |
| v |
| +---------------------------------------------------------------------------------------------+ |
| | PHASE 2: DANGLING GLOBAL POINTER RETENTION (CVE-2026-109619) | |
| | - Global session tracking structures maintain references to destroyed request heap | |
| | - Pointer cleanup routine skipped due to longjmp() execution path | |
| | - Persistent PHP-FPM worker now holds DANGLING GLOBAL POINTERS | |
| +---------------------------------------------------------------------------------------------+ |
| | |
| | (2) Attacker sends follow-up HTTP request to the same worker process |
| v |
| +---------------------------------------------------------------------------------------------+ |
| | PHASE 3: HEAP GROOMING & USE-AFTER-FREE WEAPONIZATION | |
| | - Attacker sprays controlled object data into the newly allocated request heap | |
| | - Worker references stale global pointer before re-initialization completes | |
| | - Stale pointer resolves into attacker-controlled payload in reallocated memory | |
| | - Hijacks function pointer / vtable -> UNAUTHENTICATED REMOTE CODE EXECUTION (RCE) | |
| +---------------------------------------------------------------------------------------------+ |
| |
+---------------------------------------------------------------------------------------------------+
Deep Vulnerability Mechanics: The Zend Engine Heap & zend_bailout()
To understand why PHP2Shell went undetected for over a decade, security engineers must analyze the memory lifecycle architecture of the Zend Engine.
The Two-Tiered Memory Architecture: Persistent vs. Request Heaps
Unlike simple command-line utilities that exit after completing a task, PHP in production operates as a persistent daemon process—most commonly via PHP-FPM (FastCGI Process Manager) or Apache's embedded mod_php module. In this model, worker processes remain alive across thousands of successive HTTP requests to avoid the latency of process spawning.
To manage this, the Zend Engine divides memory into two distinct tiers:
- Persistent Memory (
malloc/pemalloc(..., 1)): Retained across the entire process lifetime. Used for loaded extensions, persistent database connection pools, global configuration settings, and compiled OPcache data structures. - Request Heap (
emalloc/erealloc): Allocated dynamically for a single HTTP request. At the conclusion of request execution (RSHUTDOWN), the entire request heap is swept and recycled by the Zend Memory Manager (zend_mm), guaranteeing that memory leaks in user application code do not cause unbounded process growth.
The zend_bailout() Architectural Flaw (CVE-2026-109618)
When a PHP script encounters a fatal error—most notably hitting the configured memory_limit during a large allocation—the engine cannot execute normal unwind procedures. Instead, PHP relies on a legacy C mechanism: setjmp() and longjmp().
When an out-of-memory condition occurs, zend_error() invokes zend_bailout(), which performs a direct longjmp() back to the top-level request dispatcher:
/* Historic Zend Engine abnormal termination pattern */
void zend_bailout(void) {
if (EG(bailout)) {
longjmp(*EG(bailout), 1); /* Abruptly jump over execution stack */
}
exit(255);
}
The fatal flaw in this design is that longjmp() bypasses all local cleanup code. If memory allocation fails midway through building complex nested structures—such as array hashtables, object property tables, or AST nodes—control jumps immediately to the outer catch handler. During the subsequent RSHUTDOWN memory destruction phase, PHP traverses these partially initialized structures under the mistaken assumption that all internal pointers are valid. If an attacker can predictably trigger an allocation failure while specific data structures are half-constructed, they can force the engine into memory corruption and type confusion.
Stale Global Pointers & Use-After-Free (CVE-2026-109619)
While CVE-2026-109618 concerns uninitialized structures during cleanup, CVE-2026-109619 represents a severe cross-request Use-After-Free (UAF) condition.
During request processing, certain PHP subsystems (including error handling, module callbacks, and session serialization) store references in persistent global variables (EG(...) or module globals). Under normal execution, these pointers are meticulously cleared before the request ends:
/* Ideal cleanup lifecycle */
PHP_RSHUTDOWN_FUNCTION(session) {
php_rshutdown_session_globals(); /* Clears global pointers */
zend_mm_shutdown(heap); /* Destroys per-request memory */
}
However, when a request is terminated abnormally via zend_bailout(), execution skips module-level shutdown functions. The Zend Memory Manager frees the entire request heap, but the global pointers remain pointing to the now-freed addresses.
When the persistent PHP-FPM worker picks up the next HTTP request from the FastCGI queue, it begins initializing request state. If a global pointer is referenced before being explicitly overwritten, the engine reads and operates on the freed memory region. If an attacker controls the contents of that newly allocated memory, they achieve a reliable Use-After-Free primitive.
+---------------------------------------------------------------------------------------------------+
| Zend Heap Request Recycling & Stale Pointer Lifecyle |
+---------------------------------------------------------------------------------------------------+
| |
| REQUEST N (Attacker Trigger): |
| +---------------------------------------------------------------------------------------------+ |
| | Global State: PS(upload_progress)->active_session = 0x7fff40001080 (in Request Heap) | |
| +---------------------------------------------------------------------------------------------+ |
| | |
| | OOM Condition Triggered -> zend_bailout() invoked |
| v |
| +---------------------------------------------------------------------------------------------+ |
| | REQUEST HEAP DESTROYED: Address 0x7fff40001080 marked as FREED in zend_mm | |
| | BUT: Global PS(upload_progress)->active_session STILL POINTS TO 0x7fff40001080! | |
| +---------------------------------------------------------------------------------------------+ |
| | |
| | Worker process persists; accepts Request N+1 |
| v |
| REQUEST N+1 (Attacker Grooming): |
| +---------------------------------------------------------------------------------------------+ |
| | New Request Heap Allocates chunk at 0x7fff40001080 (Attacker payload occupies memory) | |
| | Engine dereferences PS(upload_progress)->active_session | |
| | Result: Controlled function pointer execution -> RCE | |
| +---------------------------------------------------------------------------------------------+ |
| |
+---------------------------------------------------------------------------------------------------+
The Weaponization Path: Abusing session.upload_progress
The mechanism that transforms these memory corruption primitives into a universal, pre-authentication remote exploit is PHP's session.upload_progress feature.
Why session.upload_progress is the Universal Vector
Introduced in PHP 5.4, session.upload_progress allows web applications to display real-time upload progress bars without requiring external daemons. It operates with several distinct behaviors:
- Enabled by Default: In standard distribution packages (Ubuntu, Debian, Red Hat, Docker PHP base images),
session.upload_progress.enabled = 1is turned on out of the box. - Session Independence: The feature activates even if the target web application never calls
session_start(). Whenever PHP receives amultipart/form-dataPOST request containing a field matchingsession.upload_progress.name(defaulting toPHP_SESSION_UPLOAD_PROGRESS), the engine automatically initializes a temporary session to record bytes uploaded. - Execution Before Application Code: The multipart parser and upload progress hooks execute inside PHP's core request initialization handler (
SAPI_POST_READER_FUNC), before any PHP script code (such asindex.php) begins executing. Consequently, custom application firewalls or framework-level input sanitizers (like Laravel middleware or WordPress filters) are completely bypassed because they have not yet been loaded.
The Attack Chain Mechanics
Security researchers identified an ingenious method to exploit this pathway using minimal bandwidth:
POST /any_existing_php_file.php HTTP/1.1
Host: target-server.internal
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 486
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
backdoor_session_key
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="payload.bin"
Content-Type: application/octet-stream
[... Attacker streams payload, pausing transmission for 1.1 seconds to trigger progress update ...]
- Bypassing Update Rate Limiting: PHP limits progress updates using
session.upload_progress.freq(default: 1% or 1 second). By pausing transmission of the multipart stream for slightly over one second, the attacker forces PHP to trigger a session write operation. - Memory Limit Starvation: By padding the request or manipulating headers to consume available per-request memory up to
memory_limit(often 128MB or 256MB), the attacker induces an allocation failure insidesession.upload_progressduring serialization. - Triggering
zend_bailout(): The out-of-memory error invokeszend_bailout(), destroying the request heap while preserving the dangling pointer inside the session module's global tracking struct. - Immediate Second-Request Spray: The attacker immediately fires a second HTTP request to the same worker process. This request allocates small string buffers that re-occupy the exact heap address (
0x7fff40001080) previously freed. - Instruction Pointer Hijacking: As PHP initializes the second request, the dangling session pointer dereferences the newly allocated memory, treating attacker-supplied integers as a virtual method table (vtable). Execution jumps directly to shellcode or system wrappers (
system()/exec()), yielding an interactive shell aswww-data.
Empirical Threat Telemetry & Exploitation Footprint
Following the accidental information leak on October 20, 2026, threat intelligence networks observed widespread scanning and exploitation attempts targeting internet-facing PHP applications.
Attack Vector Characteristics & Indicators of Compromise (IoC)
| Telemetry Dimension | Observed Exploit Value | Baseline Benign Value | Security Impact |
|---|---|---|---|
| HTTP Request Method | POST (Multipart/form-data) |
GET (85%) / POST (15%) |
Targets core parser hooks |
| Payload Request Size | 450 Bytes - 1.2 KB | 10 KB - 50 MB (Normal Uploads) | Bypasses large-upload WAF rules |
| Form Variable Name | PHP_SESSION_UPLOAD_PROGRESS |
Rarely seen in modern apps | Core trigger marker |
| Transmission Timing | Artificial TCP Pause (>1.05s) | Continuous wire streaming | Bypasses frequency rate limiter |
| Target Endpoints | Any valid .php file / URI |
Dedicated upload routes | Exploits static entrypoints |
Threat Actor Targeting Profiles
- Edge Appliance Management Consoles: Adversaries specifically prioritized enterprise perimeter appliances utilizing embedded PHP web management interfaces (such as legacy Palo Alto PAN-OS and Citrix NetScaler consoles). Because perimeter devices often run privileged management web servers directly in the DMZ, remote code execution provides an immediate foothold for lateral network penetration.
- Content Management Systems: Automated scanning botnets targeted WordPress
xmlrpc.php,wp-login.php, and Drupal entrypoints, testing for vulnerable PHP versions across unpatched shared hosting environments.
Systems Engineering and Hardening Playbook
Remediating PHP2Shell requires immediate patching combined with Defense-in-Depth configuration hardening across all web tiers.
+---------------------------------------------------------------------------------------------------+
| Enterprise PHP2Shell Remediation Architecture |
+---------------------------------------------------------------------------------------------------+
| Layer 4: Edge CDN & WAF Inspection Rules |
| - Block multipart POST requests containing `PHP_SESSION_UPLOAD_PROGRESS` |
| - Drop requests with artificial TCP window stalling / slow-POST patterns |
+---------------------------------------------------------------------------------------------------+
| Layer 3: Core Runtime Engine Upgrades |
| - Deploy official vendor patches: PHP 8.2.35, 8.3.36, 8.4.27, 8.5.12 |
| - Recompile legacy end-of-life runtimes with community security backports |
+---------------------------------------------------------------------------------------------------+
| Layer 2: PHP Engine Configuration Hardening (php.ini) |
| - Globally disable: `session.upload_progress.enabled = Off` |
| - Enforce uncatchable limits: `php_admin_flag[session.upload_progress.enabled] = off` |
+---------------------------------------------------------------------------------------------------+
| Layer 1: OS Process Sandboxing & Container Isolation |
| - Enforce seccomp-bpf filters blocking `execve` inside PHP-FPM worker pools |
| - Mount `/tmp` and session directories as `noexec, nosuid, nodev` |
+---------------------------------------------------------------------------------------------------+
1. The Definitive Patch: Upstream Fix Details
The official upstream patch developed by the PHP Foundation and core maintainers addresses both vulnerabilities at their architectural root:
- Recoverable Memory Allocation: Reworked out-of-memory handling in the upload progress subsystem so that allocation failures return a recoverable error code rather than invoking
zend_bailout()abruptly. - Global Pointer Sanitation: Explicitly sanitizes all module globals during request teardown, ensuring that even if an execution path aborts, global pointers are nullified before the request heap is destroyed.
- Disabled Class Code Execution: Prohibits
session.upload_progressfrom triggering custom class autoloaders or object deserialization during stream parsing.
2. Immediate Zero-Downtime Interim Mitigation
For organizations unable to deploy kernel or runtime updates immediately, disabling the upload progress subsystem in php.ini eliminates the primary known weaponization path:
; /etc/php/8.x/fpm/php.ini and /etc/php/8.x/cli/php.ini
[Session]
; Disable vulnerable upload progress tracking engine
session.upload_progress.enabled = Off
; Prevent attackers from overriding setting via user scripts or .htaccess
session.upload_progress.cleanup = On
In PHP-FPM pool configurations (e.g., /etc/php/8.x/fpm/pool.d/www.conf), enforce the setting using php_admin_flag to prevent overrides:
; Enforce immutable upload progress disablement
php_admin_flag[session.upload_progress.enabled] = off
Reload the service immediately:
systemctl reload php8.4-fpm
3. WAF & ModSecurity Detection Rule
Deploy the following ModSecurity rule at the Edge CDN or reverse proxy layer (Cloudflare, NGINX, HAProxy) to block inbound exploitation attempts before they reach the PHP backend:
# ModSecurity Rule: Block PHP2Shell Exploitation Attempts
SecRule REQUEST_METHOD "@streq POST" \
"id:1000981,\
phase:2,\
block,\
t:none,\
msg:'PHP2Shell CVE-2026-109618/109619 Attempt Blocked',\
logdata:'Matched Variable: %{MATCHED_VAR_NAME}',\
severity:'CRITICAL',\
chain"
SecRule ARGS_NAMES|REQUEST_BODY "@contains PHP_SESSION_UPLOAD_PROGRESS" \
"t:none,ctl:auditEngine=RelevantOnly"
Semantic FAQ: PHP2Shell and Core Memory Corruption
What makes PHP2Shell an unauthenticated RCE?
PHP2Shell requires zero credentials or authenticated session tokens because it executes inside PHP's core multipart POST reader during request startup—before the web server evaluates authentication middleware, session cookies, or user credentials. Any accessible .php file on the web server can serve as the injection target.
Are applications that do not use PHP sessions vulnerable?
Yes. The session.upload_progress feature operates independently of user-land application sessions. If session.upload_progress.enabled = 1 in php.ini (which is the default on most Linux distributions), PHP automatically boots a temporary session state whenever it detects the PHP_SESSION_UPLOAD_PROGRESS form parameter in an incoming upload stream.
Can an attacker exploit this vulnerability with a small file upload?
Yes. Unlike traditional denial-of-service attacks that exhaust bandwidth with gigabyte-sized files, PHP2Shell exploits memory management logic. Attackers send small HTTP POST bodies (often 450 to 800 bytes) containing crafted multipart boundaries and an artificial transmission delay (>1 second) to force progress serialization while triggering an out-of-memory error.
Does setting session.upload_progress.enabled = Off completely fix the vulnerability?
Disabling session.upload_progress blocks the only known public and weaponized exploit vector for PHP2Shell. However, it does not fix the underlying architectural flaw in zend_bailout() out-of-memory handling (CVE-2026-109618). Permanent remediation requires upgrading to patched PHP versions: PHP 8.2.35, 8.3.36, 8.4.27, or 8.5.12.
What should organizations running End-of-Life (EOL) PHP 7.x or 8.0 do?
Official upstream patches are only released for actively supported branches (PHP 8.2+). Organizations running legacy PHP 7.4, 8.0, or 8.1 must apply community backports provided by enterprise Linux LTS distributions (e.g., Ubuntu ESM, Red Hat ELS), disable session.upload_progress.enabled immediately, and accelerate migration to modern PHP releases.
How can security teams hunt for past PHP2Shell exploitation in access logs?
Inspect web server access logs for POST requests to generic PHP scripts with small payload sizes accompanied by response status codes 500 (Internal Server Error) or process crash messages in PHP-FPM error logs (SIGSEGV or child exited on signal 11). Web application firewall (WAF) logs should be queried for query parameters or multipart bodies referencing PHP_SESSION_UPLOAD_PROGRESS.