When a vulnerability hits a language runtime rather than application code, every abstraction layer above it collapses. The PHP2Shell vulnerability set (CVE-2026-109618 and CVE-2026-109619) bypasses all application-layer defenses, WAF URL whitelists, and framework sanitization logic because it detonates before your framework routes the request.
In this follow-up deep dive, we break down:
- The Zend Engine request lifecycle mechanics behind the bug.
- The exact Use-After-Free (UAF) primitive in
session.upload_progress. - Practical Blue Team detection rules (YARA, Suricata, and audit logs).
1. Zend Engine Internals: Where It Breaks
To understand why PHP2Shell works, we have to look at the transition between Zend's request startup phases:
The Flaw: Incomplete Bailout Handling
PHP implements its own virtual memory allocator called Zend MM (Zend Memory Manager). Zend MM chunks memory into large blocks (typically 2MB) and serves smaller slice allocations to the runtime.
When an allocation surpasses the configured memory_limit, PHP invokes zend_bailout(), which performs a setjmp/longjmp directly to the exception handler to cleanly unwind the request.
Under CVE-2026-109618, if zend_bailout() occurs specifically inside the multi-part body parser while tracking file progress:
- Tracking structures allocated in
session.care marked as persistent or cross-request pointers. -
zend_bailout()tears down the current request allocator slice without invoking the registered destructors for the tracking context. - The session structure pointer (
PS(upload_progress_key)) remains non-NULL in the persistent worker heap.
2. From Dangling Pointer to Control Flow (CVE-2026-109619)
When the worker accepts the subsequent HTTP request:
c
/* Pseudocode representation of the vulnerable reuse */
if (PS(upload_progress_key) != NULL) {
/*
* The worker assumes tracking was already initialized
* and attempts to dereference the stale pointer from the previous request!
*/
zval *bucket = zend_hash_find(PS(upload_progress_key), key);
bucket->value.handler->invoke(); // <-- Controlled pointer dispatch
}
Because the memory backing upload_progress_key was reclaimed into the Zend allocator's free list at the end of Request 1, an attacker can groom the heap on Request 2 by sending arbitrary string data of the exact same bin size.
Once the memory slot is overwritten:
Phase 1 (Info Leak): Attacker craftings force a leak of adjacent memory words in the response headers, defeating ASLR by calculating the base of libphp.so.
Phase 2 (Hijacking): The fake struct's function pointer is pointed to php_exec() or system(), and the argument pointer is pointed to an attacker-controlled shell string (/bin/sh -c "...").
3. Simulating the Leak Vector (Lab Environment)
Disclaimer: This is for educational, defensive testing, and verification in isolated sandbox environments.
In vulnerable builds, sending an oversized, deeply nested payload with upload progress keys triggers premature bailouts:
HTTP
POST /index.php HTTP/1.1
Host: target.local
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX
Content-Length: 34500000
------WebKitFormBoundaryX
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
payload_trigger_01
------WebKitFormBoundaryX
Content-Disposition: form-data; name="data[]"; filename="payload.bin"
[Large 32MB+ padded byte stream designed to exhaust memory_limit]
------WebKitFormBoundaryX--
If the server responds with a standard 500 Internal Server Error containing:
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate ...)
and subsequent requests to the same worker process exhibit erratic headers, truncated sessions, or core dumps, the worker pool is susceptible to session-state desynchronization.
4. Blue Team: Detection & Threat Hunting
Suricata / Snort Signature
Monitor for anomalies targeting PHP_SESSION_UPLOAD_PROGRESS accompanied by abnormally fragmented multipart payloads:
Code snippet
alert http any any -> $HTTP_SERVERS any ( \
msg:"ET EXPLOIT Possible PHP2Shell (CVE-2026-109618/19) Pre-Auth Attempt"; \
flow:established,to_server; \
content:"POST"; http_method; \
content:"multipart/form-data"; http_header; \
content:"PHP_SESSION_UPLOAD_PROGRESS"; http_client_body; \
pcre:"/name=[\"']?PHP_SESSION_UPLOAD_PROGRESS[\"']?[^\r\n]*[\r\n]+[a-zA-Z0-9_\-\.\/]{128,}/i"; \
threshold:type limit, track by_src, count 1, seconds 60; \
classtype:attempted-admin; \
sid:2026019; rev:1; \
)
Log Hunting (Nginx / Apache)
Inspect your access logs for sequential requests from the same client IP characterized by an immediate 500 error followed within seconds by anomalous 200 or 502 Bad Gateway responses (indicating worker crashes/SIGSEGV):
Bash
# Search for worker segfaults in system logs
grep -E "(php-fpm|apache2).*segfault at" /var/log/syslog /var/log/messages
# Look for memory exhaustion events followed immediately by downstream proxy errors
awk '$9 == 500 {print $1, $4, $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -n 20
5. Defense-in-Depth Hardening
Beyond updating PHP packages (e.g., updating to 8.2.35, 8.3.36, 8.4.27, or 8.5.12), deploy these defensive guardrails:
1. Reverse Proxy Request Normalization (Nginx)
Filter unneeded upload progress parameters at the reverse proxy layer so the PHP interpreter never processes them:
Nginx
# Drop requests attempting to abuse the upload progress variable
if ($request_body ~* "PHP_SESSION_UPLOAD_PROGRESS") {
return 403;
}
2. Lock Down PHP Session Directives
In /etc/php/8.x/fpm/conf.d/30-security-hardening.ini:
Ini, TOML
; Disable upload progress entirely unless your application explicitly builds against it
session.upload_progress.enabled = Off
; Prevent session ID generation from untrusted external inputs
session.use_strict_mode = 1
; Ensure garbage collection runs deterministically
session.gc_probability = 1
session.gc_divisor = 100
3. Implement System-Level Seccomp & Namespacing
Run PHP-FPM inside a sandboxed systemd service file or container with restricted syscall permissions:
Ini, TOML
# /etc/systemd/system/php8.3-fpm.service.d/override.conf
[Service]
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ProtectKernelTunables=true
Conclusion
PHP2Shell is a stark reminder that software running in managed runtimes is still ultimately bound by the C/C++ memory safety model of the underlying interpreter.
Patching runtimes promptly, auditing baseline Docker configurations, and enforcing proxy-level payload constraints are your best tools to keep production clusters resilient.
Top comments (0)