跪拜 Guibai
← All articles
Artificial Intelligence

PHP's php2shell RCEs Break the Runtime Before Your Code Runs: Patch, Restart, and Don't Trust the Copy-Paste PoCs

By BX760 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
— CVE-2026-109618 and CVE-2026-109619 are unauthenticated PHP runtime RCEs, both rated CVSS 4.0 9.2.
— Amazon MadPot logged the first exploitation attempt on October 17, 2026, four days before disclosure; a public PoC appeared about 12 hours before the embargo lifted.
— CrowdStrike's public testing showed a single anonymous POST can make an Apache mod_php worker execute arbitrary commands on x86-64 and ARM, with ASLR bypass on ARM via HTTP requests alone.
— The root cause is `zend_bailout()` using `longjmp()` without C stack unwinding, so request shutdown processes partially built HashTable buckets and stale module globals.
— 109618 lands an OOM during key allocation after `nNumUsed` is incremented; 109619 leaves process-level globals pointing into freed per-request heap across worker reuse.
— Reachability is likely when `memory_limit < 4 × post_max_size`; 64M/128M and 100M/256M combinations are reachable, while the default 8M/128M is not via URL-encoded paths.
— Affected SAPIs include PHP-FPM, Apache mod_php, php-cgi, FrankenPHP, and LiteSpeed; Apache mod_php with prefork worker reuse is officially the highest risk.
— Fixed versions are 8.5.12, 8.4.27, 8.3.36, and 8.2.35; PHP 7.4/8.0/8.1 get only minimal source backports and no new official releases.
— Upgrading files on disk is not enough: PHP-FPM masters, Apache prefork workers, and OPcache shared memory keep running old code until every worker is restarted, and containers must be recreated.
— A single high-fidelity detection signature is `Allowed memory size ... exhausted` with `in Unknown on line 0`, because it proves the failure happened before any script ran.
— A widely circulated session.upload_progress zval UAF root-cause claim did not reproduce under ASAN against public PoCs; the official patch diff instead reworks OOM bailout, initializes session state before RINIT, and resets stale state.
— After patching, application-layer LFI chains that combine session.upload_progress file writes with `include` sinks remain viable.
Conclusions

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.

Concepts & terms
zend_bailout()
PHP's fatal-error exit path. It sets an unclean-shutdown flag and calls LONGJMP to jump back to an outer setjmp point, discarding C stack frames without running destructors or cleanup.
longjmp
C non-local jump that transfers control without unwinding the stack; stack frames between the jump point and target are skipped, so cleanup functions, destructors, and refcount rollbacks never run.
HashTable
The internal structure behind PHP arrays and superglobals such as $_POST. It tracks nNumUsed and nNumOfElements, and cleanup iterates buckets by nNumUsed, which is why a partially initialized bucket is still processed.
Zend MM
PHP's per-request memory allocator between PHP and the OS. Small allocations come from size-class free lists with strict LIFO reuse, and freed blocks are not zeroed, so stale bytes can be shaped by an attacker.
memory_limit / post_max_size reachability rule
A heuristic for whether an attacker can force OOM during request parsing: if memory_limit is below roughly 4x post_max_size, a request body can exhaust memory without exceeding the post size limit. Default 8M/128M is not reachable via URL-encoded paths; 64M/128M and 100M/256M are.
session.upload_progress
A PHP feature that writes upload progress into session data. It is a confirmed consumer path for CVE-2026-109619, but not the root cause; disabling it only blocks the confirmed session paths, not other variants.
mod_php
Apache module that embeds PHP into the web server process. Prefork workers are reused across requests and share a stable memory layout, making cross-request stale-global exploitation natural and payload refinement cheap.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗