PHP2Shell by Calif Research

Summary:

PHP2Shell is a pair of PHP memory-safety vulnerabilities that can turn a deliberately triggered fatal error into unauthenticated remote code execution. Calif’s write-up explains how apparently mundane out-of-memory failures leave corrupted or dangling internal state—and how a later cleanup operation or request turns that state into an exploit. The flaws are in PHP itself, not necessarily in application code.calif

How the attack works

The researchers connect three pieces:

  1. Trigger a precisely timed memory-limit failure. Specially structured multipart uploads can consume far more PHP heap memory than their HTTP-body size suggests, allowing an attacker to exhaust PHP’s memory limit despite ordinary request-size restrictions.
  2. Interrupt an unsafe state transition. PHP sometimes updates bookkeeping or publishes pointers before completing an operation that can fail. A fatal error skips the remaining initialization or cleanup.
  3. Make PHP trust the leftover state. Request shutdown—or a subsequent request handled by the same worker—then processes partially initialized structures or pointers into freed memory. That enables memory disclosure and, with further exploitation, code execution.calif

The underlying pattern is: publish state → do fallible work → finish initialization or restore state. If the middle step aborts, later code can mistake incomplete state for valid state.calif

Why Apache and PHP-FPM differ

The article identifies three exploitable paths, with an important deployment distinction:

Path What goes wrong Scope demonstrated
Partially initialized hash-table entry PHP counts an entry as live before its initialization finishes; shutdown then interprets stale memory as a valid entry. Apache mod_php, PHP 7.3 and later, against arbitrary PHP scripts.
Stale session-upload-progress state An early fatal error skips session cleanup, leaving pointers into a freed request heap for the next request. Apache mod_php, with the earliest affected path dating to PHP 5.4.0, against arbitrary PHP scripts.
Abandoned unserialization state A fatal error during decoding leaves shared bookkeeping dangling; a subsequent upload-progress request reuses it before normal initialization resets it. Both mod_php and PHP-FPM, but dependent on reachable application behavior.

All three paths are described and demonstrated in the write-up.calif

PHP-FPM avoids the first two paths because it exits the worker when request startup fails. It remains vulnerable through the third path because a fatal error after successful startup can leave the worker alive. Crucially, that path does not require the application to unserialize attacker-supplied objects: ordinary session decoding or legitimate serialized data can provide the operation that the attacker interrupts.calif

What the researchers demonstrated

Calif reports pre-authentication code execution against WordPress, Drupal, Joomla, MediaWiki, Roundcube, Magento, phpMyAdmin, Nextcloud, Laravel, Symfony, and WHMCS. That does not mean every deployment has identical exploit conditions: PHP version, server integration, and application behavior determine which path is available.calif

The research lesson

This was human-directed, AI-assisted vulnerability research—not an autonomous model finding everything from a generic “audit PHP” prompt. Ryan Austin used knowledge of PHP’s historical memory-limit vulnerabilities to guide an LLM through allocation, fatal-error handling, and shutdown code. After the first finding, they generalized the unsafe state-transition pattern and used it to locate additional variants.calif

The historical thread matters: a related out-of-memory issue was reported in 2020 and revisited during a 2024 audit, but exploitation had not been established. Calif’s contribution was connecting such failure conditions to reliable exploitation paths.calif

Remediation

The article’s recommendation is to patch immediately. Fixed releases are PHP 8.2.35, 8.3.36, 8.4.27, and 8.5.12.tenable

Its suggested configuration workarounds are explicitly temporary and partial. In particular, setting memory_limit=-1 removes PHP’s memory cap and increases memory-exhaustion risk; it is not an equivalent substitute for upgrading.