php2shell-2-memory_gradient

PHP2Shell: Inside the Two Critical PHP RCE Flaws

Lidor Machluf October 26, 2026

PHP2Shell: Inside the Two Critical PHP RCE Flaws

Book A Demo
See how Upwind uses runtime context to turn cloud security fingdings into real-time protection.
Book a Demo

Two critical remote-code-execution flaws in PHP (CVE-2026-109618 and CVE-2026-109619) were patched on 21 October 2026. What we found: the public “php2shell” exploit targets the wrong bug, the real flaw is engine-wide, and the loud “the patch is already broken” claims do not hold up.

TL;DR

On 21 October 2026 the PHP project shipped emergency fixes for two critical, unauthenticated remote-code-execution flaws (CVSS 9.2), nicknamed “php2shell” after the leaked exploit. We tracked them from the pre-disclosure leak to the patch and tested every major claim in our own lab.

  • The real bug is in PHP’s memory engine. The press and the leaked proof-of-concept blamed PHP’s session upload-progress feature. But that feature is only one way to reach it and the fix touches around 30 files.
  • It is an out-of-memory corruption bug. When PHP runs out of memory at the wrong moment, it jumps out of a half-finished internal operation and then trusts the broken leftovers. That is the whole vulnerability.
  • The public exploit does not work. We ran it against a default PHP build and got no crash and no code execution. Its “indicators” identify the tool, not the vulnerability.
  • Apache mod_php is the confirmed target. On Apache mod_php an attacker reaches code execution with no application bug and default settings. On PHP-FPM, exploitation was not demonstrated.
  • “The patch is already bypassed” is false. Every bypass claim we checked needs a separate application vulnerability or is only a crash. Patched PHP is safe from these two CVEs.
PHP branchStatusFixed version
8.5Vulnerable below 8.5.128.5.12
8.4Vulnerable below 8.4.278.4.27
8.3Vulnerable below 8.3.368.3.36
8.2Vulnerable below 8.2.358.2.35
8.1 and older (incl. 7.x, 5.x)Vulnerable, end-of-lifeNo fix, upgrade to a supported branch

Timeline

Date (UTC)What happened
20 Oct 2026Technical details leak on Hacker News and X before the coordinated release. PHP posts an advance notice of an emergency fix for two critical RCEs.
21 Oct, ~05:00We set up an isolated lab and run the circulating “php2shell” exploit. It does not produce a crash or code execution.
21 Oct, 13:34GitHub Security Advisories published: GHSA-w8hw-w9fx-j79w (CVE-2026-109618) and GHSA-6wmf-4rv5-p3rp (CVE-2026-109619).
21 Oct, ~13:35PHP 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 released; MITRE publishes both CVE records.
21 Oct, ~13:50We diff the patch: the fix is in PHP’s core memory engine. 
21 Oct, 17:42A maintainer-verified crash regression (php-src #24488) is filed against the brand-new releases.
22 Oct“The patch is already bypassed” claims spread online. We reproduce the regression and verify each bypass claim, none is a real patch bypass.

What are CVE-2026-109618 and CVE-2026-109619?

Both bugs come down to one sentence: when PHP runs out of memory at the wrong moment, it leaves its own internal state half-finished, and then trusts it.

Every PHP request has a memory budget (memory_limit, 128 MB by default). For years, when a request blew that budget, PHP “bailed out” a hard jump (longjmp) that abandoned whatever it was doing mid-step. That is usually harmless. But in some spots PHP has only partly built an internal object when the jump fires. The half-built object is left in a broken state, and a later step reads it or frees it again. An attacker who can line the memory up just right turns those broken leftovers into attacker-controlled data and then into code execution.

The two CVEs are two faces of the same problem:

  • CVE-2026-109618 (“RCE via OOM corruption”): the out-of-memory jump corrupts memory inside a single request.
  • CVE-2026-109619 (“RCE via stale global state”): a reused worker keeps a pointer to a previous request’s memory after it was freed, and a later request acts on it.

The official fix is a single commit, and its own message says it plainly:

Avoid state corruption via bailout

- Replace bailout on OOM with UnwindExit
- Properly reset various globals that may be accessed during startup

Fix GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
Fix GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)

In plain terms: stop the hard jump on out-of-memory. Instead PHP now finishes to a safe point and raises a normal fatal error, and a second, higher limit triggers an immediate clean process exit. It also wipes the leftover state between requests. The fix is spread across about 30 core and extension files. The session upload-progress path is hardened too, but it is only one of many ways to reach the bug: the flaw can be triggered from anywhere PHP runs out of memory.

Does the public php2shell exploit work?

The leaked exploit everyone calls “php2shell” targets PHP’s session.upload_progress feature with a crafted file upload. We rebuilt it in an isolated lab (nginx in front of PHP-FPM, default settings) and fired it repeatedly, using both the plain version and the “armed” version with the exploit payload:

109618  status HTTP/1.1 200 OK   command_in_body False
109619  status HTTP/1.1 200 OK   command_in_body False
... (every run)

Every request returned a clean 200 OK, with no crash and no code execution. The exploit author’s own notes admit the same: their lab never triggered the bug either.

The reason is simple arithmetic. The exploit sends a request of about 62 KB, but a default PHP gives each request 128 MB of memory. It never comes close to running out, so the out-of-memory bug it depends on never fires. On a normal server, this tool does nothing.

It does leave a recognizable fingerprint: a fixed User-Agent (php2shell-109618 / -109619), 149 repeated t[] form fields, and a session file named after an attacker-chosen ID. Useful for spotting someone running the tool, but those are indicators of the tool, not of a successful attack. We still ship detections for them, clearly labelled as such.

Who is affected?

Exposure depends on how PHP is hosted, and the official advisory is specific about it:

Web serverExposure
Apache mod_phpUnauthenticated RCE with no application bug, at default memory and input limits. Validated over HTTP. Apache runs its shutdown routine even when request startup already failed on out-of-memory, that is where the broken state gets used.
FrankenPHPLikely the same as mod_php (also runs shutdown unconditionally).
PHP-FPMNot demonstrated. The worker exits when request startup fails, which blocks the confirmed attack. Source analysis still flags it as susceptible.
LiteSpeed (LSAPI)Models PHP-FPM; app-specific exploits likely but unvalidated.

The attack works by forcing PHP to run out of memory during request parsing, before any application code runs. That is why “we don’t use that feature” or “our app is safe” offers little protection. And the usual size limits don’t reliably stop it: the advisory lists tricks that stay within post_max_size yet still exhaust memory, a multipart upload with no declared length, an unlimited number of part headers (tiny on the wire, large in memory), empty file fields, and deeply nested field names.

The practical takeaway: scope your exposure by web server first. Apache mod_php and FrankenPHP are the priority. Our own lab ran PHP-FPM, the hardest case to exploit, so a clean result there does not mean Apache mod_php is safe.

Is the patch already bypassed?

No. Within a day of the release, posts and two public repos claimed the patched versions were still exploitable. Customers understandably got nervous. We checked every claim against the patched source and in our lab. None is a bypass of these CVEs. Each one either needs a separate vulnerability in the application, or is just a crash.

ClaimWhat it really isWhat it needsBypasses the fix?
Session upload raceOld session-file poisoningA file-include flaw in the appNo
RLIMIT / memory failureA crash (denial of service) patched PHP exits cleanlyAn unusual memory_limit=-1 + OS limit configNo
/proc/PID/fd includeA 2018-era local-file-include trickA file-include flaw in the appNo
var_destroy unserialize bugA separate memory bug, unrelated to these CVEsThe app must call unserialize() on attacker data and have a rare gadget classNo
Handler mismatchPHP object injectionA gadget class + an unusual configNo
Session-to-includeSession poisoningA file-include flaw in the appNo

The pattern is consistent: these are known techniques that require the target application to already be vulnerable. They are not flaws in the PHP patch.

One is worth a footnote. The var_destroy claim points at a separate weakness in PHP’s unserializer that the security patch did not change. But it only matters if an application already hands attacker-controlled data to unserialize() with an unusual destructor gadget, a situation that is usually game-over on its own. We could not reproduce it cleanly with a minimal test, and we did not run the weaponized exploit. It deserves a heads-up to the PHP team as its own bug, not a “the patch failed” headline.

Did the patch introduce a new bug? 

There is one new issue in the release, but it is not a security bypass. The fix bundle accidentally introduced a separate bug (PHP tracks it as php-src #24488, status: Verified): a double-free in the SimpleXML extension’s garbage collector, caused by an unrelated bugfix that shipped in the same release.

We reproduced it on the official Docker images, with a clean before/after:

php:8.4.26   ->  done              (clean)
php:8.4.27   ->  zend_mm_heap corrupted
             ->  free(): double free detected in tcache 2

The important details:

  • It is a crash (denial of service), with no code execution demonstrated.
  • It only triggers from application code, specifically get_object_vars(), an (array) cast, or json_encode() on a SimpleXMLElement that sits inside a reference cycle the garbage collector cleans up. It is not reachable pre-authentication.
  • PHP will fix it in the next point release (8.4.28 / 8.5.13).

This is almost certainly the “double-free regression” fueling the online chatter that “the fix isn’t fixed.” Even the researcher whose post customers forwarded to us said it clearly: “Neither finding establishes that the php2shell patches failed or that RCE is possible.”

How can exploitation be detected?

This vulnerability is hard to catch at the moment of the trigger. Forcing out-of-memory during request parsing spawns no new process, writes little to disk, and the crash itself is often invisible to runtime telemetry. And because the real bug is in PHP’s core, blocking the leaked tool’s fingerprint would miss any attacker who triggers the same flaw a different way.

The lesson for defenders: don’t anchor on the exploit’s trigger, watch for the code execution and the crashes that follow. Upwind’s runtime sensor sees the process a PHP worker launches and the lineage behind it, which is exactly where this attack becomes visible.

Indicators of Compromise 

Fixed versions are in the summary table above. These are the signs of the tool and of the real attack. Treat the tool fingerprints as “someone is probing,” and the host-log and process signals as “this may be a real attempt.”

IndicatorWhere to lookWhat it means
A multipart POST with a PHP_SESSION_UPLOAD_PROGRESS field and ~149 repeated t[] fieldsWeb / proxy logsThe leaked tool’s request shape
sess_<attacker-chosen-id> files in the session directory (often /tmp)Host / session save pathThe tool wrote an attacker-named session file
Allowed memory size ... exhausted, especially in Unknown on line 0, in burstsPHP / PHP-FPM error logsOut-of-memory during parsing/startup, the real trigger
child N exited on signal 11 (SIGSEGV) repeating; nginx recv() failed (104: Connection reset by peer) ... fastcgiPHP-FPM / nginx logsWorker crashes consistent with exploitation grooming
A PHP worker (php-fpm, apache2, httpd) spawning sh, curl, wget, nc or pythonRuntime / EDRPost-exploitation code execution, the highest-confidence sign

What should security teams do now? 

  • Patch now. Upgrade to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 and rebuild your container images. This is the only complete fix.
  • Prioritize Apache mod_php and FrankenPHP. Those are the confirmed, application-independent targets at default settings. PHP-FPM is lower risk but still patch it.
  • Do not roll back to avoid the SimpleXML crash. Rolling back re-exposes the actual RCE. If a workload uses SimpleXML with get_object_vars() or json_encode() on cyclic data and you see zend_mm_heap corrupted crashes, that is the #24488 regression, wait for the point release (8.4.28 / 8.5.13) or avoid that specific call pattern.
  • End-of-life PHP (8.1 and older) gets no fix upgrade. As a stopgap the official guidance is to set memory_limit=-1 administratively and restart workers, and to never use RLIMIT_AS/RLIMIT_DATA. Disabling session.upload_progress and tightening input limits help but do not fully close it.
  • Ignore the “patch is bypassed” noise but fix your own app gaps. The circulating bypasses all need a local-file-include or an unsafe unserialize() in your application. Those are worth fixing regardless of this CVE.

How can Upwind help?

Upwind gives you the two things this incident demanded: scope and runtime visibility. The sensor inventories which workloads run PHP and at which versions, and shows the web server in front of each one, so you can find and prioritize the Apache mod_php and FrankenPHP hosts that are actually exploitable, instead of guessing.

And because the only reliable signal for this flaw is what happens after exploitation, Upwind watches for exactly that: a PHP worker spawning a shell or a network tool, the process lineage behind it, and the crash bursts that precede a successful attempt. 

To learn more about how Upwind finds exposed PHP workloads and detects post-exploitation activity, schedule a demo.

Demo the Upwind Platform

See how Upwind is reducing risk and strengthening cloud security for global enterprises.
Get a Demo
Contents
Add the Upwind RSS Feed to Slack
Connect the Upwind RSS Feed to your Slack.
Follow the how-to here.
Threat RSS
Add the Upwind RSS Feed to Slack
Connect the Upwind RSS Feed to your Slack.
Follow the how-to here.
Main RSS