PHP2Shell Explained: CVE-2026-109618 & CVE-2026-109619

On 21 October 2026, PHP shipped emergency patches for two critical, unauthenticated remote-code-execution bugs — CVE-2026-109618 and CVE-2026-109619, both rated CVSS 9.2 — that the security community is calling PHP2Shell. One's a memory-corruption bug in how PHP handles out-of-memory errors while parsing form data; the other's a use-after-free hiding in the session upload-progress feature. Both live in PHP core itself, so your login page, your WAF, your app code — none of that matters, since PHP processes the request before any of it runs.

What makes this one worse than most is the timeline: honeypots caught exploitation attempts four days before the patch even existed, and multiple security vendors confirm it's now being exploited at scale in the wild. I broke down exactly how both bugs work, what the official patch changes, and — because most "PHP2Shell checkers" out there give up the moment a server hides its PHP version — I built a free, open-source scanner that can still settle the hidden-version case using an active, non-destructive behavioral check. Full writeup, the patch table, and the tool are all below.

 Author:  @ Deepak Kandpal
Thu Oct 22 2026

PHP2Shell Explained: Inside CVE-2026-109618 & CVE-2026-109619 (And How To Check If You're Hit)

If you run anything on PHP — WordPress, Laravel, Drupal, a weird internal tool from 2014, doesn't matter — there's a good chance you spent part of last week patching it. On 21 October 2026, the PHP security team shipped emergency fixes for two critical, unauthenticated remote code execution bugs that the security community has nicknamed PHP2Shell.

I wanted to actually understand what's broken under the hood instead of just copy-pasting "upgrade now" into a Slack channel, so I went digging — and then I built a tool so you (and I) don't have to dig by hand on every host we're responsible for. This post covers both: the bug, and the tool.

:: Enjoy the content☕ ::


🧨 What Is PHP2Shell?

PHP2Shell isn't one bug — it's two, tracked separately but disclosed together:

CVE-2026-109618 CVE-2026-109619
Type Out-of-memory memory corruption Use-after-free
Severity CVSS v4.0 9.2 CVSS v4.0 9.2
Auth required None None
Where it lives PHP's request-parameter hash table PHP's worker-wide request state
GHSA ID GHSA-w8hw-w9fx-j79w GHSA-6wmf-4rv5-p3rp

Why It Matters:

Both bugs live in PHP core, not in your application. That means your login page, your WAF rules, your .htaccess deny-lists — none of that matters. PHP processes the request before any of your code runs.


🕵️ How It Was Found

The short version, pieced together from the public advisories and vendor write-ups:

Timeline: Oct 17 first exploitation attempt hits Amazon's MadPot honeypot, four days before disclosure; Oct 20 technical details leak publicly; Oct 21 PHP ships emergency fixes; Oct 21 onward watchTowr, Cloudflare and Hadrian report mass exploitation

Security Note:

Amazon's threat intel team said it's unclear how an attacker had working exploit information before the public leak. That's the uncomfortable part of this story — some people apparently knew before the rest of us did.


🔬 The Two Bugs, Broken Down

1. CVE-2026-109618 — The Out-of-Memory Trap

PHP stores your submitted form fields in a hash table. The insertion code has a quiet ordering bug: it increments the table's bookkeeping counters before it allocates the new key's memory.

Flowchart: a POST with a huge form field name triggers a hash table insert where counters are incremented before allocation; if the allocation exceeds memory_limit, a fatal error abandons the insert mid-way, leaving the table thinking an uninitialized bucket is valid, which shutdown then trusts and reads as leftover heap memory, leading to RCE

Key Insight:

The exploit doesn't need to crash the worker or even change the HTTP response you see. It can succeed silently with an HTTP 200 still coming back. Don't trust "nothing looks broken" as a sign you're safe.

2. CVE-2026-109619 — The Stale Pointer

This one's a classic use-after-free, with a very un-classic entry point: PHP's session upload-progress feature.

Some fields PHP uses are meant to live for the whole worker's lifetime, not just one request. Problem is, under certain conditions PHP frees the memory those fields point to — without clearing the pointer.

Sequence diagram: Request A (attacker) sends a POST with PHP_SESSION_UPLOAD_PROGRESS, the PHP worker writes request-specific state into a worker-wide pointer, Request A finishes and the memory is freed without clearing the pointer, then Request B lands on the same worker and the code dereferences the stale pointer, resulting in use-after-free -> memory disclosure or RCE

Critical:

This needs the attacker's request and the victim-triggering request to land on the same PHP worker. A bigger worker pool lowers the odds per attempt — it does not fix the bug. Confirmed trigger paths: session.upload_progress (mod_php and PHP-FPM) and, on PHP 8.5 only, fatal-error backtrace generation under mod_php.


🌍 Is It Actually Being Exploited?

Yes. Multiple independent sources confirm active, broad exploitation, not just scanning:

  • Amazon MadPot honeypots — first hit 17 October, four days pre-disclosure
  • watchTowr's Attacker Eye honeypot network — attempts from 21 October onward
  • Hadrian — exploitation at scale on 21 October, mostly via the session.upload_progress path
  • Cloudflare — confirms exploitation is underway on its network

Risk:

At the time PHP shipped the patch, PHP itself said it had not identified a working public exploit — but unverified proof-of-concept code started circulating within hours. Treat "is there a public exploit" as a moving target, not a reason to wait.


🩹 The Patch — What Actually Changed

PHP's security release on 21 October 2026 fixed four currently-supported branches:

Branch Fixed in Status
8.2 8.2.35 ✅ Fixed
8.3 8.3.36 ✅ Fixed
8.4 8.4.27 ✅ Fixed
8.5 8.5.12 ✅ Fixed
7.4 / 8.0 / 8.1 (source backports only) ⚠️ No official release
5.4 – 7.3 (none) ❌ End-of-life, unfixed

Pie chart: official fix status by branch — 4 branches (8.2–8.5) have an official fix, 3 branches (7.4, 8.0, 8.1) have source backports only

If you're on Ubuntu, the fix is already in your archives — check the exact package version, not just what php -v prints, since distro builds keep the upstream version number in some places:

Ubuntu release Fixed package version
26.10 8.5.9-0ubuntu2.1
26.04 LTS 8.5.4-0ubuntu1.5
24.04 LTS 8.3.6-0ubuntu0.24.04.13
22.04 LTS 8.1.2-1ubuntu2.28
20.04 / 18.04 / 16.04 / 14.04 ESM only — needs Ubuntu Pro

Interim mitigation (not a fix — buys you time only):

; Reduces exposure to CVE-2026-109618
memory_limit = -1

; Reduces exposure to CVE-2026-109619
session.upload_progress.enabled = 0

; PHP 8.5 only, also reduces 109619 exposure
fatal_error_backtraces = 0

Security Note:

PHP's own advisory is blunt about this: these settings reduce exposure, they do not make the server safe. Patch the binary. Restart every worker afterward — a config change alone, without a restart, won't touch workers already running.


🔎 How Do I Know If I'm Vulnerable?

Here's the annoying part: most production servers hide their PHP version (expose_php=Off), which is normally good hardening — except now it also hides whether you're exposed to this specific bug.

Decision flowchart: is PHP running? if the version is disclosed, check whether it's below the fixed version for its branch -> VULNERABLE or PATCHED. If hidden, check on the host itself if you can; if remote-only, run the active behavioral check -> 3/3 warnings means VULNERABLE, 2/3 means PATCHED, no signal means UNKNOWN, confirm on the host

Option 1: Check it yourself, on the box

php -v
# or, on Debian/Ubuntu:
dpkg -l 'php*' | grep ^ii
# or, on RHEL-family:
rpm -q --changelog php

Compare the result against the table above. Done — this is always the most reliable answer, since it's not guessing from the outside.

Option 2: Check it from the outside — with my tool

This is where it got genuinely interesting for me to build. Most "PHP2Shell checkers" floating around right now just read the X-Powered-By header and call it a day — which tells you nothing if the version is hidden (the common, hardened configuration). So I built php2shell-scanner to actually try to settle the hidden-version case too, not just the easy one.


🛠️ Meet php2shell-scanner

github.com/kunshdeep2812/php2shell-scanner

A free, open-source, single-file Python scanner — no dependencies, GUI and CLI, built for bug bounty hunters, sysadmins, and anyone who wants a straight answer about their own infrastructure.

php2shell-scanner results table showing a mix of VULNERABLE, INDETERMINATE and PATCHED hosts

What it actually does

  • Fingerprints the stack — web server, PHP, CMS, framework, CDN/WAF — from ~5,800 known technology signatures plus ~70 built-in checks, so you also get a map of what's running, not just a yes/no on PHP.
  • Grades the PHP version against the real fixed-release table above, with backport awareness for Ubuntu, Debian, RHEL-family and deb.sury.org builds (so it doesn't cry wolf on a patched distro package just because the upstream version string looks old).
  • Settles the hidden-version case with an opt-in active patch oracle: one small, harmless multipart POST that reads how PHP itself reports a malformed session upload. Three warnings back = unpatched. Two = patched. It never triggers the actual memory-corruption bug — it reads behavior, it doesn't exploit anything.

A row expanded: host discloses no PHP version at all, yet the active oracle confirms VULNERABLE with high confidence

That screenshot above is the whole point of building this: 127.0.0.1 there discloses zero PHP version information — a properly hardened host — and the tool still gets a confirmed answer.

  • Writes real evidence. With --evidence, it produces a Markdown report with the exact request sent, a trimmed response, and a curl command to reproduce it — handy for attaching to a bug bounty report or a ticket.
  • Scans your own LAN too — single IPs, CIDR ranges (192.168.1.0/24), custom ports, the works.
  • One-click setup, whatever Python you have:
git clone https://github.com/kunshdeep2812/php2shell-scanner.git
cd php2shell-scanner
python3 setup.py

setup.py finds (or, with your permission, installs via pyenv) a Python 3.8+ interpreter, builds an isolated virtual environment, and gets out of your way.

Quick scan of your own site

python3 scanner.py --cli --authorized -t yourdomain.com --deep --oracle

Or just run python3 scanner.py with no arguments for the local web GUI.

Why It Matters:

I deliberately did not build this as an exploit-confirmation tool. It never triggers the memory-corruption bug, never crashes a worker on purpose, and never writes anything to the target. A known-vulnerable version is reason enough to patch — you don't need a working shell to justify that.

I also built a little docker-compose lab in the repo with real, deliberately-unpatched and patched PHP containers so you (or I) can verify the tool's claims against the real bug instead of trusting marketing copy. Transparency over vibes.


✅ TL;DR Checklist

  • Confirm your PHP version — on the host if you can, with the tool if you can't
  • Upgrade to 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 or your distro's equivalent patched package
  • Restart every PHP worker after patching — config changes alone don't help running processes
  • If you can't patch right now, set the interim .ini mitigations above and treat it as a ticking clock, not a fix
  • Rebuild any container images that bake in an old PHP version

📚 Further Reading


Patch your stuff, restart your workers, and go check that server you've been meaning to look at since last Tuesday. 😉

⭐ Star / fork php2shell-scanner on GitHub →

Stay tuned – for more exciting content !