{ VULNERABILITY_ADVISORY }

php2shell Vulnerability & Impact Analysis

Vulnerability Advisory

Two critical, pre-authentication memory corruption bugs in PHP core. Here is what is real, what the headlines overstate, and how to verify exposure across an estate without crashing a single worker.

Key details

Affected technology
PHP 7.4 - 8.1 / ﹤ 8.3.36 / ﹤ 8.4.27 / ﹤ 8.5.12
Vendor
PHP
Severity
Critical (9.8)
Vulnerability ID
CVE-2026-109618, CVE-2026-109619

The short version

php2shell is a pair of critical pre-auth flaws in PHP itself: CVE-2026-109618, out-of-memory driven state corruption, and CVE-2026-109619, a cross-request use-after-free. Both are rated Critical, both were exploited before public disclosure, and both are fixed in PHP 8.2.35, 8.3.36, 8.4.27 and 8.5.12, with source backports for 7.4 through 8.1.

The popular framing says every PHP server is an instant one-click shell. That overstates a real problem. A reliable unauthenticated crash is broad and trivial. A working remote shell is concentrated on mod_php, conditional on PHP-FPM, and takes an information leak, heap grooming and build-specific work. Treating both as the same risk sends teams patching the wrong hosts first.

What the two bugs are

Both were closed by a single upstream change. At the level a defender needs, and with no weaponizable recipe:

CVE-2026-109618: out-of-memory state corruption

PHP marks a new hash-table slot as in use before it allocates the memory for that slot's key. If the allocation trips memory_limit at the wrong moment, the pre-patch engine unwound out of a half-finished insert and left a counted but uninitialized slot. Request teardown then reads that stale heap as a live value. On Apache mod_php this teardown runs on every request, including requests that never reach application code, which is why the bug is pre-authentication and application-independent there. The reachability trick is a request with no usable declared length, such as chunked transfer, which passes the post_max_size check yet keeps feeding the body until memory_limit is reached. The fix swaps the abrupt unwind for a graceful, deferred out-of-memory path, so a counted slot is always fully initialized.

CVE-2026-109619: cross-request use-after-free

Request-owned state, notably from session.upload_progress (on by default, and active without session_start()), can stay referenced by a worker-global after a fatal error skips its cleanup. The backing memory is freed, and a later request on the same worker follows the stale pointer into freed memory. This supplies the reliability and the leak that turn 618's corruption into a dependable exploit. The fix resets that worker-global state across the request boundary.

How the exploit actually works

The corruption (CVE-2026-109618)

A PHP array is a hash table. Inserting a key first reserves the next slot and bumps the table's in-use count, and only then allocates memory for the key. If that allocation is what finally trips memory_limit, the pre-patch engine aborted the insert and jumped back to a safe point, but the slot had already been counted. The table now claims one more live entry than it initialized: a counted slot holding whatever bytes were already on the heap.

Reaching it: the chunked-body trigger

To corrupt the table, memory has to run out at the instant of a key insert, without the request being turned away at the door. A body that declares a Content-Length larger than post_max_size is rejected up front. A chunked body declares no length, so it slips past that check, and the engine keeps reading and buffering it until memory_limit is reached mid-parse.

From corruption to a shell (109618 + 109619)

618 on its own gives a reliable unauthenticated crash and a controlled call. What it lacks for a dependable remote shell is an address to aim at, because of ASLR, and a controlled object sitting where the fake value points. CVE-2026-109619 supplies both: a cross-request use-after-free reached through session.upload_progress that leaks memory layout and lets an attacker place a controlled object which a later request on the same worker dereferences. Chained, the two turn teardown into an attacker-chosen instruction pointer and a command running as the web-worker account.

Exposure is not uniform

What is broad and reliable is an unauthenticated remote crash of a vulnerable mod_php worker. That alone is a real availability and reconnaissance problem.

Full remote code execution is real but not turnkey. In our own lab we reproduced the chain and reached a working code-execution primitive: we drove PHP's teardown to an attacker-controlled instruction pointer and ran a command as the web-worker account on a stock build. Turning that into a point-and-shoot remote shell against an arbitrary target still needs a leak to defeat ASLR, heap grooming to place a controlled object, same-worker landing on multi-worker pools, and offsets specific to the exact PHP build and libc. We are not publishing that chain.

So the practical risk splits three ways:

  • mod_php, internet-facing, confirmed-vulnerable build. Highest risk and the turnkey path.

  • PHP-FPM behind nginx or a proxy. The corruption path is application-dependent and often blocked by a front-end body limit. Still patch, but the instant-RCE story is weaker.

  • memory_limit = -1. Removes the known trigger, and is a documented interim control.

Rank remediation by exposure, SAPI and confirmed version, not by raw CVSS alone.

Why checking the version is not enough

A version lookup has two blind spots that matter in a real estate:

  • Hidden version. Hardened servers disable expose_php, so there is no X-Powered-By banner to read.

  • Distro backports. Vendors backport the fix without changing the version string, so PHP/8.1.x can be either vulnerable or patched.

An accurate signal that does not crash anything

Memory-corruption bugs are hard to fingerprint safely, because the obvious reliable signal is the crash itself, which you do not want to fire across production. We found a behavioral signal that is both accurate and non-crashing, and it keys on the patch's own change rather than on a version string.

A vulnerable server keeps reading an over-limit body past post_max_size and ends in a graceful out-of-memory fatal, so the application page fails to render. A patched server enforces the limit, aborts the oversized body, and the page renders normally. Sized correctly, the probe stays well clear of the crash threshold, so the worker survives.

In our lab this separated vulnerable from patched on every run, three of three per host, across both the 8.3 and 8.4 branches. The probe is a single flat, continuously-growing field sized to about 0.35× the target's memory_limit, roughly 44 MB on a default 128 MB host, delivered as a chunked body, with a short escalation ladder and a second-run confirmation of any positive. We measured zero worker crashes across every run. Because it reads behavior, it works through hidden versions and distro backports. A WAF or front-end body cap is reported as inconclusive, and never as patched. The check delivers no payload and writes nothing to the target.

Why the obvious "nested keys" trick fails here

The intuitive way to force an out-of-memory condition cheaply is a deeply nested array key, which amplifies in-memory footprint by about 100× per wire byte. We tested it, and it is the wrong tool for a non-crashing check. Under PHP's default max_input_vars = 1000, that nested memory is front-loaded into the first thousand variables, parsed early in the body before post_max_size is reached. Reading past the limit then adds no memory, so a vulnerable server never reaches the graceful out-of-memory state and there is no difference to measure at any size. Nesting only helps an intrusive crash probe. An accurate non-crashing signal needs memory that keeps growing past the limit, which is a single flat, continuously-growing field. That distinction is why our check stays gentle where version-and-crash checks do not.

Response

  • Patch immediately. It is the only complete fix.
  • If you cannot patch at once, prefer migrating mod_php to FPM where feasible, apply the documented interim controls (for example session.upload_progress.enabled=0, and memory_limit=-1 blocks the known 618 path), and watch for worker segfaults and memory-exhaustion error bursts.
  • Verify, do not assume. Confirm the running build, since backports and hidden versions make the banner unreliable.
Know which hosts are actually exploitable

Mind The Hack runs the accurate, non-crashing signal above across your internet-facing and internal estate, correlates it with SAPI and exposure, and returns a ranked, patch-first worklist. You spend emergency effort on the hosts that are genuinely turnkey-exploitable, not on every box that shows a PHP banner.

Related topics
  • CVE-2026-109618
  • CVE-2026-109619

See what Mind The Hack would prove
in your environment.

Request a demo and see which exposures an attacker could actually reach, exploit, and chain.