PHP's php2shell RCEs Break the Runtime Before Your Code Runs: Patch, Restart, and Don't Trust the Copy-Paste PoCs
PHP sits underneath roughly 69–77% of known server-side language sites, and these flaws trigger before application code runs, so framework choice, code quality, and authentication add zero protection. Teams need to inventory every PHP instance, patch, and restart workers; WAF rules and config tweaks only buy time.
PHP patched two unauthenticated remote code execution flaws on October 21, 2026: CVE-2026-109618 and CVE-2026-109619, both rated CVSS 4.0 9.2. Tracked as php2shell, the pair lives in the language runtime, so it fires before a single line of application code executes, before framework middleware, and without any session or account. An anonymous POST can make an Apache mod_php worker spawn a shell; CrowdStrike's public testing showed the chain works reliably on x86-64 and ARM and can bypass ASLR on ARM with HTTP requests alone.
The root cause is `zend_bailout()`: PHP handles fatal errors with `longjmp()`, which skips C stack unwinding. Destructors, refcount rollbacks, and cleanup functions never run, and request shutdown then trusts partially constructed structures. One CVE abuses an OOM during hash key insertion; the other leaves stale module-level globals pointing into freed per-request heap across worker reuse. Apache mod_php with resident workers is the highest-risk SAPI, and the attack is reachable when `memory_limit` is below roughly 4x `post_max_size`.
The only complete fix is upgrading to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, with 7.4/8.0/8.1 getting source backports only, and restarting every worker, because running processes keep old Zend code in memory. Mitigations like WAF rules, `session.upload_progress.enabled=0`, or `memory_limit=-1` only reduce exposure. Detection rests on one high-fidelity log signature: `Allowed memory size ... exhausted` with `in Unknown on line 0`, which proves the failure happened before any script ran.
From private report to mass exploitation, the timeline compresses the decision window: August disclosure, a honey pot hit four days before public notice, a PoC 12 hours before the embargo lifted, and millions of requests within hours of release. 'Too complex to exploit' is no longer a defensible triage reason for a widely deployed runtime.
A plausible zval UAF story matched real line numbers but failed to reproduce under ASAN, while the official diff changed OOM bailout, session init order, and stale-state reset. Patch-diff ground truth beats PoC reverse engineering.
Runtime-level fixes stay dormant in memory until processes restart, so the upgrade-is-not-enough warning generalizes beyond this incident: verifying running versions, not disk versions, belongs in the change checklist.
A language runtime fix does not remediate application-layer sinks. The post-patch LFI chain makes that boundary explicit, and teams that stop at the kernel patch leave a separate, independent compromise path open.
RLIMIT_AS and RLIMIT_DATA are a useful counterexample to naive hardening: making allocations fail directly pushes execution back onto the vulnerable OOM path, so cgroup-style limits with a kill-the-worker policy are the safer mitigation.