php2shell.in
CRITICAL • PATCHED 21 OCT 2026

PHP2Shell

Two critical vulnerabilities in the PHP engine itself that let an unauthenticated attacker run commands on your web server with nothing but ordinary HTTP requests. No login, no application bug, no file upload the app accepts — the flaw lives in the runtime, below whatever framework or CMS is installed.

CVE-2026-109618 — memory disclosure (defeats ASLR)
CVE-2026-109619 — remote code execution
CVSS 4.0: 9.2 Critical (both)

⚠ If you run PHP, do this now

Upgrade to 8.2.35, 8.3.36, 8.4.27 or 8.5.12 — then restart every PHP worker (Apache, PHP-FPM). Updating WordPress, Laravel, Drupal or plugins does not fix this. Check changelogs if you use distro packages, which may backport the fix under older version numbers.

On this page — the complete attack chain:
  1. Overview — what PHP2Shell is, the two CVEs, timeline
  2. A harmless feature — session.upload_progress, the pre-auth foothold
  3. The bug — the memory bailout that leaves a dangling pointer (CWE-824)
  4. Reconnaissance — confirming a vulnerable target
  5. Stage 1+2 — leaking memory & defeating ASLR (CVE-2026-109618)
  6. Stage 3 — from a leak to a shell (CVE-2026-109619)
  7. The exploit — how the chain fits together
  8. Detection — what the attack looks like in logs
  9. Mitigation — the real fix and interim measures

01Overview

PHP2Shell is the name researchers gave to an exploit chain built on two memory-safety bugs in the PHP interpreter. Both are classified CWE-824: Access of Uninitialized Pointer, both score CVSS 4.0 9.2 (Critical), and both are remotely triggerable without authentication or user interaction. The vulnerable code paths date back to PHP 5.4.0 (2012) — a fourteen-year-old bug reaching every release since.

The bugs were found by researchers at Calif, Wiz, and OpenAI's Codex security team (plus Quarkslab on one), working with the PHP security team, and disclosed as GitHub advisories GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp. The PHP changelog addresses both with a single line — "Avoid state corruption via bailout" — which names the root cause precisely.

CVE-2026-109618
CVSS 9.2 • Critical

Memory disclosure

A stale pointer is read back as a string with an attacker-inflated length and returned in the Set-Cookie: PHPSESSID response header — up to ~67 KB of worker memory per request. Leaks heap and library addresses, defeating ASLR, and can expose passwords, session tokens, API keys, even TLS private keys.

CVE-2026-109619
CVSS 9.2 • Critical

Remote code execution

The freed memory is reoccupied with a forged HashTable whose destructor pointer (pDestructor) is set to the address of system() inside libphp. When the engine cleans up the value, it calls the attacker's function instead. Commands run as the web server user.

Timeline

2012
Vulnerable code paths introduced in PHP 5.4.0 — the bug sleeps for 14 years.
Pre-disclosure, Oct 2026
Exploitation attempts recorded by network sensors within hours of disclosure — some even before patched releases shipped. Details leaked a day early.
21 October 2026
PHP ships emergency releases: 8.2.35, 8.3.36, 8.4.27, 8.5.12 — plus source backports for EOL 7.4, 8.0 and 8.1.

Update — 23 October 2026: now broadly exploited in the wild

17 October 2026
Amazon Threat Intelligence's MadPot honeypots log a CVE-2026-109618 exploitation attempt — four days before disclosure — from an anonymisation-network IP.
21 October, within hours of release
PoC traffic arrives from multiple sources: watchTowr observes both CVEs in the wild, Cloudflare reports early attacks from 07:43 UTC, VulnCheck logs a China-based IP attempting a webshell drop.
Now
Amazon's assessment: "now being broadly exploited in the wild." Honest caveats — Canada's cyber centre saw nothing as of 21 Oct, neither CVE was in CISA's KEV catalog as of 22 Oct, and Sansec notes the public PoCs aren't one-shot reliable everywhere. So: confirmed, broadening exploitation; not yet a mass worm. Patch as if it were.

Update — 23 October 2026 (evening): a third session CVE — narrow, not patched yet

CVE-2026-115560
Today the PHP project published an informational advisory (not an emergency release) for another session upload-progress issue. PHP's own words: an "unlikely remote code execution vector." The real-world risk was judged low enough that no emergency fix is coming — the full fix lands with the next regular bugfix release.
When it bites
All of these at once: the app sets session.serialize_handler to php_binary or php_serialize in its configuration, then switches it back to php at runtime with ini_set() before calling session_start() — and session upload progress is still enabled. Miss any one of the three and this path doesn't exist. Apps that never switch the serialize handler at runtime can keep upload progress enabled.
What to do
One line: session.upload_progress.enabled=0 until the next bugfix release. If you turned it off after the 21 October advisory for CVE-2026-109619 and left it off, you're already covered — PHP says so explicitly. And a warning that matters: the 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 emergency releases do not fix this one.

02A harmless feature: upload progress

When I started digging into this, the first thing I wanted to understand was the trigger — because every vulnerability abuses something built to be useful. PHP has a feature called session.upload_progress: while PHP is still reading a multipart/form-data request body off the wire, it writes a running description of the upload — its name, size, bytes received so far — into the user's $_SESSION. A second request can read that session data and report progress back to the browser.

An attacker reaches it with an ordinary multipart POST — no login required:

POST /index.php HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----Boundary123
Connection: close

------Boundary123
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

upload-test
------Boundary123
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

This is a test file.
------Boundary123--

The critical detail is when this runs. Parsing the request body is one of the first things PHP does — the progress updates are written from inside the body-parsing (SAPI) callbacks, long before the request environment is initialised and long before the target script compiles and runs:

1. Parse body — upload-progress writes $_SESSION via SAPI callbacks  ← attacker is here, pre-auth
↓
2. RINIT — request start-up
↓
3. Run script — index.php finally executes  ← first line of your code

Reaching this code needs no session_start() in the target page, no valid login, no application logic at all. Any URL that accepts a POST is enough — even one whose script does nothing but print a single line. This pre-execution window is the foothold: the whole of PHP2Shell happens inside it, while the application is still fast asleep.

03The bug: a bailout at the wrong moment

The upload-progress feature is not itself the vulnerability. The vulnerability is what happens when PHP runs out of memory while that feature is in the middle of its work.

PHP caps per-request memory with memory_limit. When a request tries to exceed it, PHP cannot carry on — so it performs what the engine calls a bailout: it uses longjmp() to jump straight back to a safe recovery point, abandoning whatever it was doing. This is the mechanism behind the familiar "Allowed memory size exhausted" fatal error. Normally a request tidies up after itself, freeing memory and clearing pointers — but a bailout skips all of that. It is an emergency exit, not an orderly shutdown.

The flaw is a matter of timing. If the out-of-memory condition strikes after the engine has freed a block of memory but before it has cleared the pointer referring to that block, the bailout leaves the pointer behind — a classic dangling pointer:

// the pattern, simplified:
char *ptr = malloc(100);  // Event 1: allocates memory
free(ptr);                // Event 2: memory is freed
// OOM bailout happens here — longjmp skips the cleanup!
ptr = NULL;               // Event 4: pointer is cleared (NEVER RUNS)

The allocator considers the block freed and available for reuse — but the pointer still holds its old address. Classified as CWE-824 (Access of Uninitialized Pointer) and CWE-416 (Use After Free).

Why the pointer survives: the worker model

On its own, a dangling pointer in a dead request would be harmless — the process ends, the OS reclaims everything. What makes it dangerous is the server model: under Apache prefork with mod_php, a single worker process serves one request after another without restarting between them. The request ends, but the worker does not.

Request A — OOM bailout occurs mid-cleanup → stale pointer ptr = 0x1000 remains
↓ worker survives ↓
Request B — same worker. A new allocation reuses 0x1000; the block now holds different data
↓
The stale pointer still references 0x1000 → following it now reads the new allocation's contents

The attacker controls what the pointer now points at — because the attacker controls what Request B allocates. From this single defect the researchers built two attacks: CVE-2026-109618 reads the corrupted state (disclosure), CVE-2026-109619 steers it (execution).

Lab vs production: a lab setup pins the server to a single worker with a low memory_limit so the dangling pointer is reliably reused. A production host runs many workers with higher limits — the timing is harder to hit, but not impossible. The exploit rotates workers over HTTP until it lands on a corrupted one.

04Reconnaissance: confirming the target

I worked through this in the TryHackMe PHP2Shell room lab. Confirming the target takes seconds — and the punchline is the application itself:

PHP 8.4.26 — one release short of the 8.4.27 fix — and the entire application is one line that prints OK. No login form, no file upload, no database, no bug in this code to exploit. If the attack works against this page, it works against the runtime, not the application: a WordPress or Laravel site is no better protected just because its own code is sound.

Target requirements

SettingNeeded for the exploit
PHP versionVulnerable build: anything from 5.4.0 up to 8.2.34 / 8.3.35 / 8.4.26 / 8.5.11
SAPIApache mod_php, prefork (worker reuse is what keeps the stale pointer alive)
session.upload_progress.enabled1 (the default) — the delivery mechanism
memory_limitLow enough to provoke OOM reliably (the attack sends a 12 MB field)
display_errors1
session.use_cookies / use_strict_mode1 / 0

05Stages 1+2: leaking memory & defeating ASLR (CVE-2026-109618)

Modern systems randomise where code and data load (ASLR), so an attacker who wants to redirect execution must first learn the real addresses. CVE-2026-109618 removes that protection by turning the dangling pointer into a leak of the worker's own memory.

The mechanism

  1. Request A: an out-of-memory condition is provoked while a session identifier is being handled. The bailout interrupts cleanup, leaving a stale pointer to the session id.
  2. Request B: on the same worker, the freed space is reoccupied — and the stale pointer now describes a string whose length the attacker has inflated far beyond the real data.
  3. Cookie generation: when PHP builds the Set-Cookie: PHPSESSID header, it trusts the incorrect length and reads beyond the actual session ID.
  4. Disclosure: the response header now carries whatever sat in worker memory after the string — measured at up to ~67 KB of process memory per request.
Conceptually, the response looks like this:
Set-Cookie: PHPSESSID=[32-byte real id][~67 KB of worker memory]

That memory can hold: passwords, session cookies, API keys,
TLS private keys — and the code addresses the exploit needs.

Two leaks, ASLR defeated

The exploit runs the leak twice. The first pass discloses the heap chunk base (found by scanning the cookie for 2 MB-aligned pointers); the second discloses the libphp base by reading a pointer at a known offset into the library:

The addresses differ on every run — that is ASLR doing its job. The attack simply reads them out each time rather than guessing. With the heap and the engine's own code located, the second CVE takes over.

06Stage 3: from a leak to a shell (CVE-2026-109619)

The leak told the attacker where everything is. CVE-2026-109619 turns that knowledge into a command running on the server — by reoccupying the freed memory not with data to read, but with a carefully forged object that hijacks the engine's own clean-up machinery.

When PHP destroys a value, it may call a function pointer stored alongside it — a destructor — to release the value's resources. The exploit forges an internal HashTable structure in the reoccupied memory with its destructor pointer, pDestructor, set to the address of system() inside libphp (an address the leak made known). When the engine tidies up that value at the end of request handling, it believes it is calling a harmless destructor — and instead calls system() with attacker-controlled bytes as its argument.

That is remote code execution from nothing but multipart POST requests to a page that prints OK — against a server with no application vulnerability of any kind, with commands running as the web server user.

The corruption is probabilistic — a failed attempt at the leak stage does not mean the target is safe; the attack can simply be retried.

07The exploit: how the chain fits together

End to end, the chain is: rotate workers over HTTP until one carries a corrupted session pointer, run the Set-Cookie leak twice to learn the heap and libphp bases, then reoccupy the freed memory with a forged HashTable whose destructor points at system(). The full chain completes in under ten seconds, entirely over plain HTTP, with commands running as the web server user.

The exploit code itself is intentionally not published here. The technique is described in full above so defenders can understand and detect it; for hands-on practice, work through it in a legal lab environment such as the TryHackMe PHP2Shell room, and only against systems you own or have explicit written permission to test.

08Detection

The attack leaves distinctive marks — any of these, and especially the combination, is worth alerting on while patching is in progress:

Treat a suspected incident as both code execution and data disclosure: audit credential, session-token and key exposure too, and consider rotating secrets on exposed hosts.

09Mitigation

The real fix

Because PHP2Shell lives in the runtime, "updating the website" misses it entirely. Updating WordPress, Drupal, Joomla or Laravel changes the application; the vulnerable code is in PHP, underneath all of them. The only complete fix:

  1. Update the PHP package your distro ships (or the base image your container builds from) to 8.5.12, 8.4.27, 8.3.36 or 8.2.35.
  2. Restart every worker (Apache, PHP-FPM, FrankenPHP) so no process keeps running the old code.

Interim measures (not complete substitutes)

; php.ini — temporary exposure reduction only
session.upload_progress.enabled = 0   # removes the trigger for most reported techniques
opcache.enable = 0                    # frustrates the code-execution path
; on PHP 8.5 only:
fatal_error_backtraces = 0            # addresses CVE-2026-109619 specifically

10Am I affected?

Check every web server — and don't forget embedded copies (sidecars, workers, appliances need separate inventory):

# check the version
php -v
# VULNERABLE, e.g.:  PHP 8.4.26  # one release short of the fix
# FIXED, e.g.:      PHP 8.4.27
QuestionAnswer
Which PHP versions?Believed affected back to 5.4.0 (2012). Fixed in 8.5.12, 8.4.27, 8.3.36, 8.2.35; source backports published for 7.4 / 8.0 / 8.1. Distro packages may carry the fix under older numbers — check the changelog for the CVE IDs.
Which SAPIs?Apache mod_php is the easiest target and highest risk; also PHP-FPM, php-cgi, FrankenPHP, LSAPI, Windows PHP.
Does updating WordPress / plugins fix it?No. This is in the PHP runtime.
Does the app need sessions or uploads?Not necessarily — the upload-progress trigger runs before application code, even if the app never calls session_start().
What does the attacker get?Code execution as the web worker user (not root automatically) — enough to read configs/DB creds, drop webshells, and pivot. Plus potential memory disclosure of secrets and keys.

11FAQ

Is PHP2Shell a WordPress bug?
No. It lives in the PHP interpreter. Every PHP application — WordPress, Drupal, Joomla, Laravel, custom code — is exposed if the underlying PHP build is vulnerable.
Does the attacker need an account?
No. Exploitation is unauthenticated and needs no user interaction — just crafted multipart HTTP requests.
Why "Heartbleed of PHP"?
Because CVE-2026-109618 can leak ~67 KB of live server memory per request through a response cookie, potentially including secrets and private keys — the same class of failure that made Heartbleed infamous.
Why did it take 14 years to find?
The bugs sit in exceptional code paths (OOM bailouts, fatal-error handling) that are hard to reach reliably, and turning the corruption into working RCE needs precise heap shaping. AI-assisted research made that practical in 2026.
I'm on PHP 7.4 / 8.0 / 8.1 — am I out of luck?
Those branches are end-of-life, but the PHP team published source backports. Otherwise migrate to a supported branch or rely on your distro's backported package — and apply the interim mitigations meanwhile.
Why does the exploit need only 14 bytes for the command?
The stage-3 payload embeds the command in a 167-byte live zend_string carrier; after the fake HashTable and execute_data headers, 14 bytes remain. Longer commands chain through a short shell trigger like sh</out/c.

12Sources

CVE-2026-109618 • CVE-2026-109619 • CWE-824 / CWE-416 • CVSS v4.0 9.2 (Critical). Technical content on this page draws on the author's notes and the TryHackMe PHP2Shell room. Last updated 24 Oct 2026.