New MCP server — Bring vulnerability intelligence into your AI

PHP2Shell: 2 Critical RCE Flaws in PHP (CVSS 9.2)

Unauthenticated code execution demonstrated on WordPress, Drupal and Joomla, and reproduced in our lab. Patched versions and steps to take today.

Nicolas D9 min read

The score is 9.2, not the perfect 10.0 some people claimed before the fix. But the impact is real: Wiz used these bugs to run code on default WordPress, Drupal and Joomla, with no login. And we reproduced a working PHP2Shell code-execution chain in our own lab (PHP 8.4.26, Apache mod_php).

TL;DR — PHP fixed two critical bugs, nicknamed "PHP2Shell". Both involve memory left in a bad state when PHP stops an operation early. An attacker can crash a worker or, in some setups, run code without logging in. Our pentesters analysed how the flaws are exploited and how dangerous they are, and it comes down to two words: patch now.

Key takeaways

  • Two CVEs cover several related bugs. Fixed in PHP 8.2.35, 8.3.36, 8.4.27 and 8.5.12.
  • Wiz demonstrated unauthenticated code execution on default WordPress, Drupal and Joomla. We reproduced a worker crash (CVE-2026-109619) and, separately, chained CVE-2026-109618's memory leak and use-after-free into working code execution in our lab.
  • Code execution is harder than a crash and depends on the target. Updating PHP is the only full fix.

From speculation to confirmed risk

Before the technical details were public, plenty of videos and supposed PoCs were circulating. Many did not actually show a working exploit, and the claims of a perfect 10 score were still speculation. The disclosed CVEs are serious enough on their own: an unauthenticated, pre-auth code-execution path in PHP core, rated CVSS 9.2. Exploiting many different servers is harder, as attacks may need adapting to each target. But widespread exploitation is not impossible.

Two root causes, several bugs

There are two CVE numbers, but more than two bugs. PHP grouped several related problems under each number. Both groups involve PHP leaving memory in a bad state when an operation is interrupted, especially by an out-of-memory error. The fix is named after it: "Avoid state corruption via bailout."

The difference is what goes wrong:

  • CVE-2026-109618, OOM corruption. PHP stops while some request data is half-built. Cleanup can then use that invalid data, sometimes in the same request.
  • CVE-2026-109619, stale global state. PHP keeps a pointer to request memory after freeing it. A later request on the same worker can follow it.

The second group is a use-after-free. The first can also lead to memory corruption without that exact sequence. Wiz says other variants probably exist under these two numbers.

CVE-2026-109618: OOM corruption

Each PHP request has a memory budget (memory_limit, 128 MB by default). If a request goes over it, PHP stops it at once and jumps to cleanup. The problem is that this can happen while PHP is still reading the request body. The data is left half-built, and the same request's cleanup then uses it. Several failures fit this CVE.

The half-built upload data

If PHP runs out of memory while building the list of uploaded files, that list is left half-built. Cleanup then uses parts that were never set. On mod_php, this can happen before your code runs. PHP-FPM handles failed startup differently, so the same pre-application path is not confirmed there. We did not reproduce a standalone crash on this path in our lab; our confirmed hands-on result is the full code-execution chain described below, which relies on the related upload-progress use-after-free.

A memory leak

The second bug helps make real attacks practical. An out-of-memory stop can leave session state pointing at freed memory. A later multipart request can return a chunk of worker memory in a PHPSESSID response cookie, even if the app never starts a session. This gives the attacker memory addresses needed for exploitation. Wiz chained this leak with an upload-progress unserializer use-after-free to run code on WordPress, Drupal and Joomla.

CVE-2026-109619: leftovers for the next request

PHP keeps some data inside each worker between requests. A fatal error can leave this data pointing at freed memory and skip the reset, so the next request uses it. Wiz demonstrated two cases here; PHP's advisory also describes a session lifecycle case.

The saved error trace

When PHP saves a trace after a fatal error (on by default in 8.5), it can leave a pointer to freed memory. An early diagnostic on the next request can touch it. We reproduced this crash in our lab as well.

Upload progress

PHP's upload-progress feature (on by default) can also read stale session state left by an earlier error. PHP confirms this path under 109619. It needs no session_start() call, but the validated PHP-FPM attack also needs a suitable application operation.

OPcache mix-up

A failed memory allocation can leave PHP's shared code cache out of sync, so another worker mixes up one internal type for another. Because this cache is shared, the problem can reach more than one worker.

How exploitable is it?

We reproduced full code execution. By chaining PHP2Shell's memory leak with its upload-progress use-after-free, we achieved remote code execution in our own lab, not just a crash. This is direct, hands-on proof that the risk is real and not only theoretical. We are not releasing our exploit. Working attack code is already public, and one more ready-to-run tool helps attackers more than defenders. We spent our time the other way: making the exploit run reliably in our lab so we could understand it fully, then using what we learned to detect this vulnerability faster.

The hard part is not the vulnerability. It is the target. The two things that usually stop this class of bug are already handled. Address randomisation (ASLR) is defeated by the memory leak, which hands the attacker live addresses inside a PHPSESSID response cookie. And once the flaw is reached, the primitive is reliable.

What is left is per-target precision. A working exploit is pinned to one build: the exact symbol offsets (for example zif_system and zval_ptr_dtor) and the heap-grooming "magic values" that place attacker data where the freed pointer will read it. Change the PHP version, the distribution, the compiler, the extensions or the CPU architecture and those numbers move, so the proof-of-concept has to be re-tuned. Some configurations add friction too, mainly around reliably forcing the out-of-memory condition, but none of it is a wall.

So the realistic threat is targeted attacks, not one universal exploit. And the per-target data an attacker needs is often handed to them: a PHP version in a verbose error page, an X-Powered-By: PHP/8.x.y or Server header, a recognisable build. Leak the version and matching the offsets becomes mechanical: exactly the kind of repetitive work automation makes cheap.

Status Finding
Demonstrated Unauthenticated code execution. We chained the memory leak and the use-after-free of CVE-2026-109618 to run commands pre-authentication in our lab, with no credentials and no gadget chain.
Demonstrated Worker crashes. A reproducible denial of service; repeated crashes can take a service offline.
Harder One exploit per server. Build-specific offsets and heap-grooming values differ from target to target, so each one needs tuning: a speed bump that disappears as soon as the version is disclosed.

What to do now

Update PHP. The fixed releases are 8.2.35, 8.3.36, 8.4.27 and 8.5.12 (or newer). Vendor backports may also carry the fix. Restart PHP workers after updating. This is the only full fix.

If you cannot update today, these lower your risk. None of them replaces the update:

  • Set and enforce memory_limit=-1 in server configuration, then restart every worker and confirm that no pool, directory or app setting puts a finite limit back. This removes one trigger, not the bug.
  • Keep post_max_size conservative and enforce the real request size at your proxy. Reduce max_input_nesting_level and cap input variables, parts, per-part headers and uploads. If your app does not use uploads, block multipart requests (including ones with no Content-Length) at the proxy.
  • Set session.upload_progress.enabled=0 (worth keeping off even after patching if you do not use it). On PHP 8.5 also set fatal_error_backtraces=0. On PHP-FPM, pm.max_requests=1 helps if performance allows. Restart all workers.
  • Avoid configuration that prints a warning before a request starts: prefer settings applied at startup over per-request overrides, and remove obsolete directives.
  • Check your app for places where attacker input can cause a fatal error or a timeout (uploads, imports, serialization, error and shutdown handlers).
  • If you use a CDN or WAF, check its advisory. Several vendors (Cloudflare, Fastly, Akamai, AWS and others) published interim mitigations.
; php.ini — interim hardening until PHP is updated
session.upload_progress.enabled = 0
fatal_error_backtraces = 0   ; PHP 8.5 only

What to watch for

  • Odd multipart requests followed by memory-limit errors.
  • Many HTTP 500 errors.
  • Very large or malformed PHPSESSID response cookies.
  • PHP workers crashing and restarting for no clear reason.

These bugs act below your application, so your app logs may show little. Rely on your web server and PHP error logs.

Expect more of these

These are not two clean bugs with one clean patch. The fix touches several parts of PHP, including out-of-memory handling and global state between requests. Wiz expects other variants. That does not mean the patched releases are still vulnerable. For now, updating is the fix.

For defenders, the lesson is to know which internet-facing systems run an affected PHP version, and to keep knowing, because this class of bug invites targeted attacks and new variants keep surfacing. A version printed in an error page or a response header is all an attacker needs to start matching offsets to your estate.

This is the gap Patrowl is built to close. Continuous external attack-surface monitoring discovers every internet-facing asset, fingerprints the PHP versions they expose (including the verbose errors and headers that hand an attacker a head start), and flags the ones still on a vulnerable release, re-checking as each new advisory lands. Our AI agents probe that surface daily and a human pentester validates every finding, so an affected, exposed server is something you find and fix first, not something an attacker finds for you.

Frequently asked questions

What is PHP2Shell?

PHP2Shell is the nickname for two critical vulnerabilities in the PHP core, CVE-2026-109618 and CVE-2026-109619, patched in October 2026. They leave memory in an inconsistent state when PHP aborts an operation, which lets an attacker crash a worker or execute code remotely without authentication.

Which PHP versions fix PHP2Shell?

The fixed versions are PHP 8.2.35, 8.3.36, 8.4.27 and 8.5.12, or later. Distribution backports may also include the fix. After updating, restart all PHP workers so the fix takes effect.

Does PHP2Shell allow unauthenticated code execution?

Yes. Wiz Research demonstrated unauthenticated code execution on default WordPress, Drupal and Joomla installations, and Patrowl reproduced a full code execution chain in its lab on PHP 8.4.26 with Apache mod_php.

Are WordPress, Drupal and Joomla vulnerable to PHP2Shell?

Yes, if they run on an unpatched PHP version. The flaw is in PHP itself, not in the CMS: updating WordPress, Drupal or Joomla is not enough, PHP is what needs to be updated.

Is there a public PHP2Shell exploit?

Yes, working attack code is already public. Each exploit still has to be tuned to the targeted PHP build (symbol offsets, heap-grooming values), which favors targeted attacks over a single universal exploit.

How do I know if my internet-facing servers are vulnerable?

You need to identify the PHP version of every exposed asset, including versions revealed by X-Powered-By headers or verbose error pages. Patrowl automates this inventory continuously and flags servers still running a vulnerable version, with every finding validated by a pentester.

Sources and references

  • PHP security advisories: GHSA-w8hw-w9fx-j79w (CVE-2026-109618) and GHSA-6wmf-4rv5-p3rp (CVE-2026-109619).
  • Both fixed in commit 31c0ea7d7ff, "Avoid state corruption via bailout".
  • Demonstrated code execution and bug breakdown: Wiz Research, "PHP2Shell".
  • Hands-on testing: our own lab. Crash reproductions on PHP 8.4.x and 8.5.11 under AddressSanitizer, and a public CVE-2026-109618 code-execution proof-of-concept reproduced on a stock PHP 8.4.26 (Apache mod_php) target.

This article describes the vulnerabilities and their impact. It contains no working exploit or attack code. Our testing was done on isolated systems.

CVEPHPRCE

Originally published on patrowl.io.

Is this exposed on your attack surface?

Patrowl maps your internet-facing assets, fingerprints the versions they run and flags the vulnerable ones, validated by a pentester.

Talk to an expert