---
title: "PHP2Shell: how an old PHP bug class came back, and how to catch it"
author: "Filip Olszak"
publisher: "detqt research"
published: 2026-10-21
updated: 2026-10-22
canonical_url: https://detqt.com/blog/php2shell
citation: "Filip Olszak, \"PHP2Shell: how an old PHP bug class came back, and how to catch it\", detqt research, October 21, 2026. https://detqt.com/blog/php2shell"
attribution: "When quoting or summarising, credit Filip Olszak (detqt research) and link to https://detqt.com/blog/php2shell."
---

# PHP2Shell: how an old PHP bug class came back, and how to catch it

Two memory-safety bugs in the PHP engine give attackers code execution on servers whose application code is perfectly fine. Timeline, exploitation, lab results and detection.

- Author: Filip Olszak, detqt research
- Published: 2026-10-21
- Last updated: 2026-10-22
- Canonical URL: https://detqt.com/blog/php2shell
- Topics: PHP2Shell, CVE-2026-109618, CVE-2026-109619, PHP remote code execution, PHP vulnerability, PHP-FPM, mod_php, session.upload_progress, PHP out of memory, use after free, detection rules, Sigma, Suricata, KQL, webshell detection
- Cite as: Filip Olszak, "PHP2Shell: how an old PHP bug class came back, and how to catch it", detqt research, October 21, 2026. https://detqt.com/blog/php2shell

> This article is original research by Filip Olszak for detqt research. When quoting or summarising it, please credit Filip Olszak and link to https://detqt.com/blog/php2shell.

## Key points

- PHP2Shell is the nickname for CVE-2026-109618 and CVE-2026-109619, two memory-safety flaws in the PHP interpreter, both rated CVSS 4.0 9.2 and fixed on 21 October 2026.
- Fixed releases are PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Researchers say nearly every release back to 5.4.0 is affected; 7.4, 8.0 and 8.1 only have source backports upstream, and 5.x has no upstream fix.
- The attack forces an out-of-memory bailout while PHP parses a multipart request, then uses stale pointers on the same worker. It runs before application code, so the application does not need to be vulnerable.
- Shodan counted 2,195,981 hosts sending an X-Powered-By: PHP header on 22 October 2026; about 1.31 million advertise a version in the affected range that is not a fixed release.
- Apache mod_php (and FrankenPHP, which behaves like it) is the highest-risk setup, but PHP-FPM is not safe: the session upload-progress path was validated on both mod_php and FPM.
- Exploitation is not trivial: published chains need precise heap tuning, real addresses and often thousands of requests. Red Hat rates both CVEs Important (CVSS 3.1 8.1, AC:H). No public exploit reliably bypasses ASLR on x86-64.
- Amazon observed an exploitation attempt on 17 October, four days before disclosure. Hunt from that date.
- Official PHP Docker images answer the memory-exhaustion request with HTTP 200 and log nothing, because they ship without a php.ini. With logging on, parse-time out-of-memory leaves 'in Unknown on line 0'. The memory-leak stage creates no process, so EDR alone won't see it.
- The fix adds a memory_hard_limit_percent setting. If ini_get() returns anything other than false, the build is patched, even when the version string looks old.
- Patch and restart every PHP worker. Disabling session.upload_progress removes the main trigger in the meantime; post_max_size alone does not help.

*Original research by Filip Olszak, detqt research. Canonical source: https://detqt.com/blog/php2shell*

On 21 October the PHP project shipped emergency releases for two flaws in the interpreter itself: **CVE-2026-109618** and **CVE-2026-109619**, now widely called PHP2Shell. Both score 9.2 under CVSS 4.0. In the worst case, an unauthenticated attacker gets code execution on a web server with a pair of crafted multipart requests, sent over and over until they land on the same worker.

What makes this one different is where the bugs live. They are not in WordPress or Drupal, and they are not in your code. They sit in the part of PHP that parses a request before any script gets to run. On Apache with `mod_php`, the setup researchers found easiest to exploit, it barely matters what application is installed. According to the teams that found the bugs, nearly every PHP release since 5.4 carries at least one of them.

*Figure: Hosts sending `X-Powered-By: PHP`, split by advertised version. Source: Shodan host counts, 22 October 2026; details under [Exposure](#exposure-shodan).* 1.31 million internet-facing hosts advertise a PHP version in the affected range that is not a fixed release, out of 2,195,981 hosts sending an X-Powered-By PHP header. 4,517 advertise a fixed release, 121,702 run PHP 5.2 or 5.3, and 757,007 show no recognisable version.

The disclosure was messy. The CVE IDs leaked a day early, a proof-of-concept video went out by mistake, and patched images shipped before the embargo lifted. Amazon has since reported an exploitation attempt on 17 October, four days before the advisories. Patch now, and hunt from the 17th.

*PHP2Shell at a glance, as of 22 October 2026.*

|  | Detail |
| --- | --- |
| CVEs | CVE-2026-109618 (RCE via out-of-memory corruption), CVE-2026-109619 (RCE via stale global state) |
| Severity | CVSS 4.0 9.2 for both. Red Hat scores them CVSS 3.1 8.1 with high attack complexity, NVD 9.8. The 10.0 figure that circulated during the leak is wrong. |
| Affected | PHP below 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Researchers say almost every version back to 5.4.0. |
| Highest risk | Apache `mod_php`, and FrankenPHP, which the advisory says behaves like it. PHP-FPM, php-cgi and LiteSpeed are affected too. |
| Authentication | None. No user interaction. |
| Exploitation | Attempts seen from 17 October. Working exploits need heap tuning and often thousands of requests. No confirmed compromises publicly reported yet. |
| CISA KEV | Not listed as of 22 October. |
| Fix | Upgrade, then restart every PHP worker. |

## The two bugs

Underneath the name are four separate memory-safety issues, grouped into two CVEs. They share a root idea. When PHP runs out of memory or hits a fatal error, it does not unwind carefully. It calls `longjmp()` and jumps straight to the request shutdown path. The engine calls this a bailout. Anything that was half-built at that moment stays half-built, and anything that pointed into the request's memory keeps pointing there after that memory is freed.

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

Some PHP data structures become reachable before they are fully initialised. Calif's write-up uses the hash table insert path as the example: `_zend_hash_str_add_or_update_i` bumps the element counters before it allocates the new bucket's key. If that allocation is the one that crosses `memory_limit`, shutdown walks a table that claims to hold an element it never finished writing. With control over when the out-of-memory lands, an attacker can steer which half-built object gets touched.

### CVE-2026-109619: stale state across requests

Long-lived workers, such as a prefork Apache child or a PHP-FPM pool process, handle many requests in a row. Some global state (session, error, object and extension state) can be left pointing into a request heap that a fatal error has already freed. The next request on the same worker then reads that pointer before normal initialisation resets it. The advisory confirms two places that read the stale state: **session upload progress** handling, and INI settings applied per directory before the request starts (`.htaccess` or `php_value` on mod_php). What leaves the state behind varies. On PHP 8.5 the new `fatal_error_backtraces` feature does it, and that is the basis of the confirmed `mod_php` code execution.

> **Why it bites before your code runs.** Multipart parsing and session upload progress both run before PHP hands control to your script. That is why a site with no upload form, no `session_start()` and no vulnerable plugin can still be exposed.

## Timeline

The bug class is old. The first version of it was reported in 2004; the modern variant was spotted in 2024 and stalled until three teams found a trigger this summer.

- **2004-07-27: CVE-2004-0594.** Stefan Esser's `memory_limit` bug. An out-of-memory abort during `zend_hash_init` overwrites a destructor pointer. Same class, PHP 4 era. ([source](https://nvd.nist.gov/vuln/detail/CVE-2004-0594))
- **2019-11-28: PHP bug #78876 (CVE-2019-11048).** Long multipart variables push PHP into out-of-memory and leave temp files behind. An early sign that the multipart parser can be used to exhaust memory. ([source](https://bugs.php.net/bug.php?id=78876))
- **2025-04-10: php-src audit report published.** Quarkslab's audit of php-src is published. While fixing one of its findings, a core developer contracted by the PHP Foundation later spots the out-of-memory problem. An exploit is theorised, no trigger is found, and competing fixes stall. ([source](https://thephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/))
- **2026-08-11: Root cause reported.** Calif and OpenAI independently report the out-of-memory to RCE root cause to the PHP security team. Calif shares a working PoC within days. ([source](https://calif.io/php2shell))
- **2026-08-24: Wiz reports more paths.** Wiz, having confirmed unauthenticated RCE against Drupal and WordPress, reports its findings. OpenAI reports further vectors on 18 September. ([source](https://www.wiz.io/blog/php2shell-php-rce))
- **2026-10-17: First attempt in the wild.** Amazon's MadPot honeypots log an exploitation attempt from an anonymisation network, four days before disclosure. How the actor got the details is unknown. ([source](https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/))
- **2026-10-20: The leak.** CVE IDs appear online around 15:00 UTC with the wrong score and the wrong release time. PHP pre-announces at 19:33 and moves the release forward a day. A PoC video is published by mistake around 20:00. ([source](https://news-web.php.net/php.announce/509))
- **2026-10-21: Public PoC and early patches.** Amazon sees the first public PoC around 02:00 UTC. Cloudflare's WAF rules go live at 03:02. Patched container images ship at 07:41, which gives attackers a diff to work from. ([source](https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/))
- **2026-10-21: PHP releases fixes.** PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 ship at 14:00 UTC, with source backports for 7.4, 8.0 and 8.1. Calif and Wiz publish technical write-ups. National CERTs issue guidance. ([source](https://www.php.net/archive/2026.php))
- **2026-10-22: Current status.** Attempts are being reported by several sensor networks. Neither CVE is in CISA's KEV catalogue yet. ([source](https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json))

*Figure: The last 28 hours before the patch, in UTC, with the 17 October attempt on the left. Red markers are exploitation events. Sources: Amazon, PHP Foundation, Cloudflare.* A timeline from 17 October to 21 October 2026. The first exploitation attempt was seen on 17 October. On 20 October the CVE IDs leaked at 15:00, PHP pre-announced at 19:33 and a PoC video leaked at 20:00. On 21 October a public PoC appeared at 02:00, WAF rules went live at 03:02, patched images shipped early at 07:41 and PHP released fixes at 14:00.

## How exploitation works

At a conceptual level, the published chains have two stages, and both happen inside PHP's input handling.

*Figure: The two-request pattern described by Calif, Wiz and Imperva. The outcomes in the second row are separate primitives that researchers chain together.* Diagram of the PHP2Shell attack. Step one sends a multipart body tuned to hit memory_limit. Step two is the out-of-memory bailout, which frees the heap but leaves pointers behind. Step three sends a second request to the same worker with the upload-progress field and no file. Step four reads the stale pointer before initialisation. Outcomes are memory disclosure in the PHPSESSID cookie, worker crash, file read through OPcache, and code execution. The memory leak feeds back into code execution by defeating ASLR.

### Stage one: exhausting memory during parsing

The multipart parser allocates for every field. Deeply nested field names such as `a[x][x][x]...` produce many allocations per byte, so a body well under the default 8 MB `post_max_size` can still exhaust the default 128 MB `memory_limit`. Where size limits get in the way, Calif describes going around them with `Transfer-Encoding: chunked` on HTTP/1.1, HTTP/2 bodies without a length, or a configured decompression filter. The attacker tunes the body so that a particular allocation is the one that fails.

### Stage two: reusing the worker

A second request carries a session cookie and the `PHP_SESSION_UPLOAD_PROGRESS` field, but no file. Upload progress tracking runs before the session is normally initialised and before the application, and it consumes the stale state. Imperva counts three of the five demonstrated techniques using this trigger. Attackers don't control which worker answers, so expect the pair to be sprayed many times.

*Request shape, placeholders only*

```http
# Request 1: drive the worker into memory_limit while parsing input
POST /<any .php endpoint> HTTP/1.1
Host: <target>
Transfer-Encoding: chunked
Content-Type: multipart/form-data; boundary=<B>

--<B>
Content-Disposition: form-data; name="<f>[<x>][<x>][<x>]...<deep nesting>"; filename=""
...thousands of small or empty parts...
--<B>--

# Request 2: same worker, session cookie, upload-progress field, no file
POST /<any .php endpoint> HTTP/1.1
Cookie: PHPSESSID=<id>
Content-Type: multipart/form-data; boundary=<B>

--<B>
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

<value>
--<B>--
```

The responses give it away too. Look for a 500 or a dropped connection on the first request, then a `Set-Cookie: PHPSESSID=` that runs to kilobytes of URL-encoded data, heavy with `%XX` escapes. That cookie is the memory disclosure. Imperva measured roughly 67 KB of process memory per response, and Wiz describes the region as attacker-sized, so treat 67 KB as an example rather than a ceiling. That is enough to carry credentials, tokens and keys, and it hands the attacker the addresses needed to beat ASLR.

### mod_php vs. PHP-FPM

For the out-of-memory bug (CVE-2026-109618), the SAPI matters a lot. On Apache `mod_php` the request destructor still runs after a startup failure, so the first stage works against any PHP URL with no help from the application. FrankenPHP behaves the same way. On PHP-FPM a startup failure kills the worker, which pushes the attacker onto later paths that usually need the application to reach `session_start()`, `session_decode()` or `unserialize()`. The advisory calls FPM susceptible by source analysis, without a complete exploit.

The stale-state bug (CVE-2026-109619) is a different story. Its advisory says the session lifecycle RCE was validated against both mod_php and FPM, and that no call to `session_start()` is needed. FPM can also be exposed through INI values that nginx or `mod_proxy_fcgi` pass to it. Patch FPM hosts on the same schedule as mod_php.

*Fixed versions by branch. Official advisories cover 8.2 to 8.5; the wider range comes from the researchers.*

| Branch | Fixed in | Status |
| --- | --- | --- |
| 8.5 | 8.5.12 | Supported |
| 8.4 | 8.4.27 | Supported |
| 8.3 | 8.3.36 | Security fixes only |
| 8.2 | 8.2.35 | Security fixes only, end of life 31 December 2026 |
| 7.4, 8.0, 8.1 | Source backport branches, no official binaries | End of life |
| 5.4 to 7.3 | No upstream fix (some distributions backport) | End of life, believed affected |

## Exposure

PHP runs about 69% of websites whose server-side language is known (W3Techs). Black Kite's Shodan study on 21 October found about 1.37 million hosts advertising an affected version, 72.5% of them on end-of-life branches with no official binaries. The most common build was PHP 5.4.16, on about 125,000 hosts. For much of that long tail, isolation is more realistic than patching.

### Shodan, 22 October

A Shodan count on 22 October found 2,195,981 hosts sending an `X-Powered-By: PHP` header. About 1.44 million name a branch. Of those, 1.31 million name a version in the range researchers consider affected (5.4 to 8.5) that is not a fixed release, Another 121,702 run PHP 5.2 or 5.3, which predate that range and lost support more than a decade ago. The breakdown is in the chart at the top of this post, and only 4,517 hosts so far advertise a fixed release.

*Figure: Hosts by advertised PHP branch. Branches 8.2 to 8.5 are supported; everything older is end of life.* Column chart of hosts by advertised PHP branch: 5.2 22,940; 5.3 98,762; 5.4 50,655; 5.5 8,734; 5.6 150,276; 7.0 32,217; 7.1 14,657; 7.2 175,954; 7.3 28,927; 7.4 71,249; 8.0 76,018; 8.1 108,204; 8.2 220,784; 8.3 235,229; 8.4 83,506; 8.5 60,862.

*Figure: Hosts sending `X-Powered-By: PHP` by country, top ten. Source: Shodan, 22 October 2026.* Bar chart of hosts sending an X-Powered-By PHP header by country: United States 574,275; China 189,356; Germany 151,487; Japan 140,002; Hong Kong 67,981; France 66,660; Indonesia 63,750; United Kingdom 53,695; India 52,813; Netherlands 51,793.

Read these as a lower bound and a rough guide. Hosts that hide the header aren't counted. Shodan collects banners over weeks, so few hosts could show a fixed version a day after release, and distribution builds keep old version strings after patching (see the patch check under Our testing).

## How hard is it to exploit?

Harder than a 9.2 suggests. Every team that has published a working chain had to do real heap work, and several did it in a lab set up to make that work easier.

*Published reproductions, as of 22 October 2026.*

| Source | What was shown | Conditions |
| --- | --- | --- |
| PHP advisories | Code execution on mod_php for both CVEs, and on PHP-FPM for the session upload path of CVE-2026-109619 | No complete FPM exploit for CVE-2026-109618 |
| [Datadog Security Labs](https://securitylabs.datadoghq.com/articles/php2shell-critical-vulnerabilities-in-php/) | A shell from one unauthenticated POST (CVE-2026-109618, PHP 8.5.11) | mod_php on ARM64, `memory_limit=16M`, a single worker, OPcache off |
| [CrowdStrike](https://www.crowdstrike.com/en-us/blog/crowdstrike-protects-customers-from-php-rce-zero-days/) | An exploit that works on x86-64 and ARM64 and survives worker restarts | mod_php. ASLR defeated over HTTP on ARM hosts |
| [VulnCheck](https://www.vulncheck.com/blog/php2shell-vulnerabilities) | Memory leak chained to code execution (CVE-2026-109618, PHP 8.4.26) | x86-64 with ASLR on and `display_errors=Off`. Not public |
| Calif, via [heise](https://www.heise.de/en/news/php2shell-Critical-flaws-allow-code-injection-in-PHP-11494897.html) | Command execution on a Laravel site in 38 seconds | Thousands to tens of thousands of requests per success |

Three things slow an attacker down. The out-of-memory has to land on one particular allocation, and where that is depends on the build, the extensions and the configuration. The exploit needs real addresses, which come either from the `PHPSESSID` memory leak or from guessing, and every wrong guess kills a worker with a segfault. And the attacker can't choose which worker answers, so the second request gets sprayed. On ARM64 the address space is small enough to brute-force. On x86-64, VulnCheck reviewed hundreds of GitHub PoCs and found very few real exploits, and none that gets past ASLR without help.

[Red Hat](https://access.redhat.com/security/vulnerabilities/RHSB-2026-013) takes the cautious view. It rates both CVEs Important, at 8.1 with `AC:H`, because "reliable exploitation requires precise heap manipulation and timing". It also notes that standard RHEL installs use PHP-FPM, whose workers "exit on early initialization failures before reaching the corrupted structures". That argument covers the out-of-memory path. It doesn't cover the session upload path of CVE-2026-109619, which PHP validated on FPM, so it isn't a reason to patch RHEL hosts later.

In practice, a reliable spray-and-pray exploit that works against any PHP site isn't public yet. A capable attacker who knows the target's build can already get code execution on mod_php or FrankenPHP. The memory leak alone is enough to steal credentials, and it is the easier half.

## Exploitation in the wild

Attempts, yes. Confirmed compromises, not publicly, at the time of writing. Amazon describes PHP2Shell as being broadly exploited and has the pre-disclosure sighting. Hadrian told SecurityWeek it saw exploitation in the wild at scale, mostly abusing session upload progress. Cloudflare, Patchstack and Vega have all seen attack traffic. watchTowr sees attempts on its honeypots but no successes. CISA, Huntress and SentinelOne had not observed confirmed exploitation as of 21 October, and the PHP security team found no functional public exploit at release.

VulnCheck's canaries show the attempts in detail, and they show what most of the traffic is today. On 21 October a scanner with the user agent `php2shell-scan/109618` hit 188 of its hosts, with uploads too small to exhaust memory. One source in China tried to drop a webshell through a session file, and every follow-up request for the file returned 404. Expect better attempts soon, now that a patch diff, a leaked video and detailed write-ups are out. Be careful with "PoC" repositories as well. Most are not real exploits, and CyberScoop has warned that some may carry malware.

## Our testing

Patched builds only (official PHP images, Debian and Alpine packages; Apache with mod_php, nginx with PHP-FPM), on localhost, without exploit code. The aim was to see what ends up in logs.

### Official Docker images don't log stage one

The official `php` images (8.2.35 to 8.5.12) ship without a php.ini, so `display_errors` is on and `log_errors` is off. Pushing request parsing into the memory limit on `php:8.4.27-apache` returned **HTTP 200** with the error in the response body. The error log stayed empty and the access log showed an ordinary `200`. With `php.ini-production`, the same request returned a 500 and was logged. Tests used a 16 MB `memory_limit` to keep runs short.

*Stage one against patched 8.4.27, as recorded by default logs.*

| Setup | Client sees | Error logs | Access log |
| --- | --- | --- | --- |
| Official Apache image, no php.ini | 200, error text in the body | Nothing | `POST /index.php 200` |
| Same image with php.ini-production | 500, empty body | `[php:error] ... in Unknown on line 0` | `POST /index.php 500` |
| nginx + official FPM image | 200 (errors displayed) or 502 | FPM: `child exited with code 70` | `200` or `502` |
| nginx + Debian php8.4-fpm | 502 | nginx: `FastCGI sent in stderr: "PHP message: ..."` | `502` |

### Memory amplification

Empty-filename parts nested 63 deep used about 127 times their size in memory: 243 KB of request, 30.8 MB peak. Default caps (`max_input_nesting_level` 64, `max_input_vars` 1,000, `max_multipart_body_parts` 1,020) kept every request under 128 MB. With `max_input_vars` at 5,000, a common WooCommerce setting, about 1 MB reached the limit.

*Figure: Peak memory while parsing one multipart request on patched PHP 8.4.27, 22 October 2026.* Bar chart of peak memory for three request shapes against the 128 MB memory limit. 1,000 fields nested 64 deep, 187 KB sent, peaked at 10.5 MB. 1,020 empty-filename parts nested 63 deep, 243 KB sent, peaked at 30.8 MB. 4,500 parts with max_input_vars raised to 5,000, about 1 MB sent, reached the 128 MB limit.

The patch also made nested input cheaper (about 80 bytes per level, down from 216 on 8.4.26), so unpatched servers fill up faster than this. For WAF limits, part count and nesting depth matter more than body size.

### Chunked requests and nginx

Under mod_php a chunked request reaches PHP without a Content-Length, so the up-front size check is skipped; the access log recorded a 140 MB chunked upload as `200 713`. nginx re-frames the body and rejects chunked bodies over 1 MB by default. Its default buffers also turned any `Set-Cookie` over about 4 KB into a 502 (`upstream sent too big header`), which would stop a 67 KB leaked cookie. Apache with mod_php passed it through. Raising `fastcgi_buffer_size` or `proxy_buffer_size` removes that protection.

### Stage two creates new sessions

A no-file multipart POST with `PHP_SESSION_UPLOAD_PROGRESS` and any `PHPSESSID` cookie got a fresh session cookie back from a page that never calls `session_start()`, and left an empty `sess_*` file each time. On a patched host the cookie has a normal length, so length-based rules miss it. A session cookie from a path that never uses sessions does not.

### Checking whether a build is patched

The fix adds a `memory_hard_limit_percent` setting. It was present on every patched build checked, including Ubuntu 24.04's fixed package (which reports 8.3.6), and absent on 8.4.26 and Alpine 3.22. Absence is not proof of exposure: Ubuntu 22.04's fixed PHP 8.1 is a reduced backport without it.

*Is this PHP build patched?*

```bash
# Run it with the same PHP package that serves traffic.
php -r 'var_dump(ini_get("memory_hard_limit_percent") !== false);'
# true  -> the fix is present, whatever the version string says
# false -> unpatched, or a reduced backport (Ubuntu 22.04's PHP 8.1): check the changelog
```

### Distribution patch status

*Checked against each security tracker at 04:46 UTC on 22 October 2026.*

| Distribution | Status |
| --- | --- |
| Debian 13 (trixie) | Fixed in 8.4.27-1~deb13u1 (DSA-6574-1) |
| Debian 12 (bookworm) | Fixed in 8.2.35-1~deb12u1 (DLA-4848-1) |
| Debian forky and sid | Not yet (8.4.26-1) |
| Ubuntu 26.10, 26.04, 24.04, 22.04 | Fixed. 24.04 still reports 8.3.6 and 22.04 reports 8.1.2 |
| Ubuntu Pro (ESM) | Fixes back to PHP 5.5.9 on 14.04 |
| Alpine 3.24 and edge | Fixed |
| Alpine 3.22 and 3.23 | Not yet |
| Red Hat | Rated Important (CVSS 3.1 8.1), no errata yet |

Scanners comparing `8.3.6` with `8.3.36` will flag fixed Ubuntu hosts. Upstream has no fix for 5.x; Ubuntu Pro does.

## Detection

The most important point first: **the memory-leak stage starts no process**. An attacker can pull secrets out of a worker without ever spawning a shell, so EDR alone will not see it. The early stages only show up in WAF, CDN, proxy and web server logs, and default logs often miss them. Fix the logging first, then start the hunt on 17 October.

*Figure: Where each stage tends to be visible, from the published technique descriptions and lab logs. PHP error logs only see stage one when `log_errors` is on, which it isn't on official Docker images.* Matrix of attack stages against telemetry sources. The memory exhaustion request is clearly visible in WAF or CDN logs and PHP error logs, and partially in access logs. The upload-progress trigger is clear in WAF logs and partial in access and error logs. The memory leak in the cookie is only visible in WAF or CDN logs. Code execution is clear in EDR process telemetry and partial in access logs, error logs and file integrity monitoring. Persistence is clear in access logs and file integrity monitoring, and partial in WAF and EDR.

### WAF, CDN and access logs

- Multipart POSTs containing a `PHP_SESSION_UPLOAD_PROGRESS` field, **especially without a file part** and especially to endpoints that have no upload feature. Amazon calls this string out specifically.
- Multipart bodies with an unusual number of parts, field names nested more than about 20 brackets deep, or many empty `filename=""` parts.
- Not only multipart. Datadog's exploit sends an `application/x-www-form-urlencoded` POST with a field name of about 3.5 MB containing raw null bytes. Alert on oversized field names and null bytes in any form body.
- Multipart POSTs to PHP endpoints that use `Transfer-Encoding: chunked`, HTTP/2 without a content length, or `Content-Encoding: gzip`/`deflate`.
- Bursts of POSTs from one source to the same PHP URL, which is how an attacker lands on the same worker. On a properly configured server they line up with 500s or 502s. On official images with no php.ini they can all be 200s.
- A `Set-Cookie: PHPSESSID` from a path that never uses sessions; in testing this flagged every stage-two request. A value far longer than the usual 26 to 32 characters, full of `%XX` escapes, is the memory leak itself.

If you are behind Cloudflare, filter Security Events on the rule IDs listed in [Cloudflare's post](https://blog.cloudflare.com/php-vulnerabilities-waf-protection). The managed rules have been on by default since 03:02 UTC on 21 October. On paid plans, check that the ruleset action is set to block.

### PHP and web server error logs

- `Allowed memory size of <N> bytes exhausted ... in Unknown on line 0`. The `Unknown on line 0` part means the failure happened while parsing input, before any script ran.
- FPM workers exiting with `child <N> exited with code 70` (stage one), with nginx logging `upstream sent unexpected FastCGI record: 3`. No segfaults appeared in testing.
- `Hard memory limit of ... bytes exhausted` followed by exit code 137, from the patch's new hard limit. It looks like a kernel OOM kill, so don't dismiss it as one.
- `Actual POST length does not match Content-Length, and exceeds`, `Multipart body parts limit exceeded` and `Input variables exceeded`, which show requests pushing against the parser's limits.
- FPM pools respawning workers much faster than their normal baseline.

*Splunk: parser exhaustion and worker exits*

```spl
index=web ("in Unknown on line 0" OR "Hard memory limit of"
    OR "exited with code 70" OR "exited with code 137"
    OR "upstream sent unexpected FastCGI record: 3"
    OR "Actual POST length does not match Content-Length"
    OR "Multipart body parts limit exceeded" OR "Input variables exceeded")
| bin _time span=5m
| stats count AS signals, values(source) AS sources BY host, _time
| where signals >= 5
```

### Logging configuration

Default access logs record none of the request properties that matter here: body length, Transfer-Encoding, content type, or whether a session cookie went out. These formats surfaced every stage-one and stage-two test request. On official Docker images, also add a php.ini with `log_errors = On` and `display_errors = Off`. On distribution FPM, set `catch_workers_output = yes`, or the new hard-limit message is lost.

*nginx*

```nginx
# Flags session minting and oversized session cookies without logging the ID.
map $sent_http_set_cookie $php_sid_class {
    default                    "-";
    "~*PHPSESSID=[^;]{129,}"   "PHPSESSID_LONG";
    "~*PHPSESSID="             "PHPSESSID";
}

log_format php2shell '$remote_addr [$time_iso8601] "$request" $status $body_bytes_sent '
                     'req_len=$request_length cl="$http_content_length" '
                     'te="$http_transfer_encoding" ce="$http_content_encoding" '
                     'ct="$http_content_type" up_status="$upstream_status" '
                     'set_cookie=$php_sid_class';

access_log /var/log/nginx/access.log php2shell;
```

*Apache*

```apache
# Needs mod_logio for %I. Put CustomLog inside the VirtualHost: a server-level
# CustomLog is overridden by the vhost in the official images.
# %{Set-Cookie}o writes the session ID itself, so restrict access to this log.
LogFormat "%h %t \"%r\" %>s %b in=%I cl=\"%{Content-Length}i\" te=\"%{Transfer-Encoding}i\" ce=\"%{Content-Encoding}i\" ct=\"%{Content-Type}i\" dur_us=%D pid=%P set_cookie=\"%{Set-Cookie}o\"" php2shell
CustomLog ${APACHE_LOG_DIR}/access.log php2shell
```

*PHP-FPM pool*

```ini
; FPM pool. %{bytes}M reports memory in 2 MB steps and can carry over
; from the worker's previous request, so treat it as a rough signal.
access.log = /proc/self/fd/2
access.format = "%R %t \"%m %r\" %s pid=%p peak_mem=%{bytes}M req_len=%l dur_ms=%{mili}d te=\"%{HTTP_TRANSFER_ENCODING}e\""
catch_workers_output = yes
```

### Post-exploitation process activity

After execution this looks like any other web RCE. Watch for `httpd`, `apache2`, `php-fpm`, `php-cgi`, `lsphp`, `frankenphp` or `w3wp` spawning shells, interpreters, `curl`, `wget`, `nc`, `socat` or `certutil`. One detail trips up a lot of rules: PHP's `system()`, `exec()`, `shell_exec()`, `passthru()`, `popen()` and `mail()` all go through `/bin/sh -c`, so `id`, `whoami` or `curl` usually show up as children of `sh`, one level below the worker. The rule below keys on the shell for that reason. Two more traps: on Alpine images every applet, `sh` included, resolves to `/bin/busybox`, and Debian's FPM binary is versioned (`/usr/sbin/php-fpm8.4`). The rule below covers both, matched in all four tested setups and passes `sigma check`. Expect `mail()` to make some noise. Beyond the tree itself, look at command lines containing `/dev/tcp/`, `mkfifo` or `base64 -d |`, binaries executed from `/tmp` or `/dev/shm` by the web user, and the web user reading `wp-config.php`, `.env` or `settings.php` from outside the normal application path.

*Sigma: web worker spawns a shell (tested against four setups)*

```yaml
title: Web worker spawns a shell or tool (PHP2Shell follow-on)
status: experimental
description: >
  PHP's system(), exec(), shell_exec(), passthru() and popen() run commands
  through /bin/sh -c, so the worker's direct child is the shell. Covers
  versioned distro FPM binaries and busybox images, where every applet
  resolves to /bin/busybox.
logsource:
  product: linux
  category: process_creation
detection:
  parent_fixed:
    ParentImage|endswith:
      - '/php-fpm'
      - '/php-cgi'
      - '/httpd'
      - '/apache2'
      - '/lsphp'
      - '/frankenphp'
  parent_versioned:
    ParentImage|re: '/(php-fpm|php-cgi|lsphp)[0-9.]+$'
  child:
    Image|endswith:
      - '/sh'
      - '/bash'
      - '/dash'
      - '/busybox'
      - '/python3'
      - '/perl'
      - '/nc'
      - '/ncat'
      - '/socat'
      - '/curl'
      - '/wget'
  condition: 1 of parent_* and child
falsepositives:
  - CMS tasks that shell out (ImageMagick, wkhtmltopdf, sendmail, git)
level: high
```

*KQL (Defender for Endpoint, Sentinel): web worker child processes*

```kql
DeviceProcessEvents
| where Timestamp > datetime(2026-10-17)
| where InitiatingProcessFileName matches regex @"(?i)^(php-fpm.*|php-cgi.*|httpd(\.exe)?|apache2|lsphp.*|frankenphp|w3wp\.exe)$"
| where FileName in~ ("sh","bash","dash","busybox","cmd.exe","powershell.exe","python3","perl",
                      "nc","ncat","socat","curl","wget","certutil.exe","whoami","id","uname")
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName,
          ProcessCommandLine, AccountName
```

### Network detection

Hunting rules, tested on Suricata 7.0.10 against a labelled lab capture. Suricata never puts `Set-Cookie` in `http.header`, so response-cookie rules must use `http.cookie`. A bare `;` inside a pcre breaks rule parsing, so it's written as `\x3b`. And with the default 100 KB `request-body-limit`, an upload-progress field placed after 200 KB of padding was missed, so raise it to at least 1 MB on PHP servers. The "no file part" check fits in one rule with a negative lookahead. The field name can also be changed through `session.upload_progress.name`.

*Suricata: request shape and leaked cookie*

```suricata
# Upload-progress field with no file part anywhere in the inspected body
alert http any any -> $HTTP_SERVERS any (msg:"PHP2Shell hunt: upload-progress field without file part"; \
  flow:established,to_server; http.method; content:"POST"; \
  http.content_type; content:"multipart/form-data"; nocase; \
  http.request_body; content:"PHP_SESSION_UPLOAD_PROGRESS"; nocase; \
  pcre:"/^(?![\s\S]*filename=)[\s\S]*name=\"PHP_SESSION_UPLOAD_PROGRESS\"/i"; \
  classtype:attempted-admin; sid:9000051; rev:1;)

# Deeply nested multipart field names
alert http any any -> $HTTP_SERVERS any (msg:"PHP2Shell hunt: deeply nested multipart field names"; \
  flow:established,to_server; http.content_type; content:"multipart/form-data"; nocase; \
  http.request_body; content:"name"; \
  pcre:"/name\s*=\s*[\"']?[^\"'\[\x3b\r\n]*(?:\[[^\]\r\n]*\]){20,}/i"; \
  classtype:attempted-admin; sid:9000002; rev:3;)

# Chunked multipart POST
alert http any any -> $HTTP_SERVERS any (msg:"PHP2Shell hunt: chunked multipart POST"; \
  flow:established,to_server; http.method; content:"POST"; \
  http.content_type; content:"multipart/form-data"; nocase; \
  http.header_names; content:"|0d 0a|Transfer-Encoding|0d 0a|"; nocase; \
  classtype:attempted-admin; sid:9000061; rev:1;)

# Oversized session cookie in a response. Set-Cookie is only in http.cookie,
# and a bare ';' inside pcre breaks rule parsing, so it is written as \x3b.
alert http $HTTP_SERVERS any -> any any (msg:"PHP2Shell hunt: oversized PHPSESSID in response"; \
  flow:established,to_client; http.cookie; content:"PHPSESSID="; nocase; \
  pcre:"/PHPSESSID=[^\x3b]{512,}/i"; \
  classtype:attempted-recon; sid:9000023; rev:1;)

# Any session cookie issued; pair with an allowlist of paths that use sessions
alert http $HTTP_SERVERS any -> any any (msg:"PHP2Shell hunt: PHPSESSID issued"; \
  flow:established,to_client; http.cookie; content:"PHPSESSID="; nocase; \
  classtype:attempted-recon; sid:9000043; rev:1;)
```

*KQL: oversized session cookies, if your WAF logs response headers*

```kql
// WafLogs stands in for whichever table holds your WAF or CDN response headers.
// No standard Azure WAF table logs them, so this needs a CDN or proxy that does.
WafLogs
| where TimeGenerated > datetime(2026-10-17)
| where ResponseHeaders contains "PHPSESSID="
| extend sid = extract(@"PHPSESSID=([^;\s,]+)", 1, ResponseHeaders)
| where strlen(sid) > 512   // matches the Suricata rule; tune from your baseline
| summarize hits = count() by ClientIP, Host, bin(TimeGenerated, 10m)
```

### File system and persistence

- New or modified `.php`, `.phtml`, `.phar` or `.sh` files in web roots and upload directories, written by the web server user.
- Changes to `.htaccess` or `.user.ini`, and `auto_prepend_file` in particular, which is a quiet way to load a webshell on every request.
- New cron entries, accounts or SSH keys belonging to the web user.

### Indicators

Amazon and VulnCheck published these addresses. Amazon notes that its list includes shared anonymisation infrastructure, so legitimate traffic comes from some of them too. Use them to prioritise an investigation, not to declare one closed.

| Address | Observed | Note |
| --- | --- | --- |
| `103.216.220.92` | 17 October 2026 | Observed before disclosure |
| `31.223.184.166` | 21 October 2026 | Post-leak attempts |
| `37.19.200.5` | 21 October 2026 | Post-leak attempts |
| `134.209.159.64` | 21 October 2026 | Post-leak attempts |
| `103.114.163.188` | 21 October 2026 | Post-leak attempts |
| `95.217.98.37` | 21 October 2026 | Post-leak attempts |
| `85.11.167.243` | 21 October 2026 | Scanner, user agent `php2shell-scan/109618` (VulnCheck) |
| `45.198.224.169` | 21 October 2026 | Scanner, same user agent (VulnCheck) |
| `221.14.159.134` | 21 October 2026 | Webshell drop through a session file, unsuccessful (VulnCheck) |

### False positives

- `PHP_SESSION_UPLOAD_PROGRESS` is a legitimate feature behind upload progress bars. Baseline which applications use it. The missing file part and the request rate are what matter.
- Out-of-memory errors and 500s happen on healthy systems during big imports and reports. Correlate them with request shape and worker exits before paging anyone.
- Plenty of applications shell out on purpose: ImageMagick, wkhtmltopdf, git, sendmail, WP-CLI. Allowlist by parent script and command line, not by binary name.
- Some frameworks use long or custom session IDs. Set the cookie length threshold from your own baseline.

## Finding vulnerable PHP servers

Inventory every PHP runtime, not just the websites. PHP hides in appliances, hosting control panels, Windows software that bundles an interpreter, container base images and CI runners. The UK NCSC points to NCSC-NL's list of products with PHP dependencies, which is a good place to start the conversation with vendors.

*Host and package checks*

```bash
# Every SAPI has its own ini. Check the one that serves traffic, not just the CLI.
php -v
php -i | grep -E 'session.upload_progress.enabled|memory_limit|post_max_size|fatal_error_backtraces'
# FPM binaries are usually versioned (php-fpm8.3) and live in /usr/sbin.
# -i shows the FPM php.ini but not pool overrides, so grep the pools too.
/usr/sbin/php-fpm* -i 2>/dev/null | grep -E 'session.upload_progress.enabled|memory_limit'
grep -rE 'php_(admin_)?(value|flag)' /etc/php*/*/fpm/pool.d/ /etc/php-fpm.d/ 2>/dev/null

# Packages (distribution backports keep old version strings, check the tracker)
dpkg -l 'php*' 2>/dev/null; rpm -qa 'php*' 2>/dev/null; apk info -v 2>/dev/null | grep php

# Externally, where the banner is still on
curl -sI https://<host> | grep -i x-powered-by
```

- Scan container images and SBOMs for PHP versions, and remember that a patched host can still run an unpatched container.
- Check for the `memory_hard_limit_percent` setting (see Our testing) rather than trusting version strings.
- Don't treat a missing `X-Powered-By` banner as proof of safety. More than a quarter of the internet-facing PHP hosts in Black Kite's data hide their version.
- Tenable has published plugin 369483, with web app scanning checks 115719 to 115722.
- Prioritise internet-facing `mod_php` first, then FPM running session-heavy applications, then the end-of-life branches that have no official fix.

From an attack-path point of view, the question to ask is what that PHP worker can reach once it is compromised. A forgotten admin panel on PHP 5.6 with database credentials in memory and a route to the internal network is a bigger problem than a patched marketing site, even if a scanner gives them the same score.

## Mitigation

**Patching is the only complete fix.** The advisories, the researchers and Amazon all say so. Upgrade to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or to your distribution's backport, and then **restart every PHP worker**. A new binary on disk does nothing for the processes already in memory. If you can't update or disable a system straight away, the NCSC asks you to check it for signs of compromise, including backdoors, before you upgrade or mitigate. Patching won't remove an attacker who is already in.

If you can't patch today, these settings reduce exposure without removing it. Set them in `php.ini` or the FPM pool, not with `ini_set()` or `.user.ini`.

*Interim hardening*

```ini
; php.ini, or the FPM pool as php_admin_value / php_admin_flag.
; Not ini_set() or .user.ini.
session.upload_progress.enabled = 0
log_errors = On                   ; official Docker images ship with it off
display_errors = Off
fatal_error_backtraces = 0        ; PHP 8.5 only, default is 1
max_input_nesting_level = 16      ; default 64
max_multipart_body_parts = 200    ; default -1, size it to the application
; Then restart every PHP worker.
```

- Disabling `session.upload_progress.enabled` removes the main trigger for CVE-2026-109619. If you don't use it, PHP suggests leaving it off after patching too.
- Don't lean on `post_max_size`. Both defaults are already in the range the attack fits under, and PHP checks the declared Content-Length, so chunked and HTTP/2 bodies walk around it. `max_input_vars` doesn't count empty file parts either. `max_multipart_body_parts` is the setting that caps the part count.
- At the proxy or WAF, enforce limits on decoded body size, part count, field count and nesting depth, **including requests without a Content-Length**. Block multipart entirely on applications that never accept it.
- `memory_limit = -1` is sometimes suggested for CVE-2026-109618. It is incomplete, because `mmap()` failure can still reach the bug, and it raises the risk of denial of service. Do not use `RLIMIT_AS` or `RLIMIT_DATA` as a substitute.
- Turning off OPcache removes the OPcache primitives (Imperva's file read, and the type confusion Wiz used in its WordPress chain) at a heavy performance cost. Neither advisory recommends it. The advisories do suggest `pm.max_requests = 1` to limit worker reuse, if you can afford it.
- Rotate secrets that a PHP worker could have held in memory if the leak path may have been hit.
- Where none of this is possible, take the host offline or put it behind a VPN until it can be fixed.

## FAQ

**What is PHP2Shell?**

PHP2Shell is the nickname for CVE-2026-109618 and CVE-2026-109619, two memory-safety vulnerabilities in the PHP interpreter disclosed and patched on 21 October 2026. They let an unauthenticated attacker crash PHP workers, leak process memory and, in some configurations, execute code by sending crafted multipart requests.

**Which PHP versions are affected by PHP2Shell?**

Officially, every 8.2, 8.3, 8.4 and 8.5 release before 8.2.35, 8.3.36, 8.4.27 and 8.5.12. The researchers who found the bugs say nearly every version back to PHP 5.4.0 is affected by at least one of them. Branches 7.4, 8.0 and 8.1 have source-only backports, and 5.x has no fix.

**Is my WordPress, Drupal or Laravel site vulnerable?**

If it runs on an affected PHP build, assume yes. The flaws are in PHP's request handling, not in the application. Wiz demonstrated unauthenticated code execution against the latest WordPress, Drupal and Joomla container images, and Calif lists targets including MediaWiki, Magento, Nextcloud, Laravel and Symfony. Updating the CMS does not help; updating PHP does.

**Does PHP-FPM protect me?**

No. Apache `mod_php` is the easiest target for the out-of-memory bug, but the advisory for CVE-2026-109619 says its session lifecycle RCE was validated on both mod_php and FPM, with no `session_start()` call needed. Patch FPM hosts on the same schedule.

**How easy is PHP2Shell to exploit?**

Not easy. Working exploits from Datadog, CrowdStrike, VulnCheck and Calif need precise heap tuning, valid memory addresses and often thousands of requests, and failed guesses crash workers. Apache `mod_php` is the most reliable target, and ARM64 hosts are easier because ASLR can be brute-forced. Red Hat rates both CVEs Important (CVSS 3.1 8.1, high attack complexity). The memory leak is easier to reach than code execution.

**Is PHP2Shell being exploited in the wild?**

Attempts, yes: since 17 October 2026, four days before disclosure. As of 22 October no confirmed compromise had been publicly reported, and neither CVE was in CISA's KEV catalogue.

**How do I check whether my PHP build is patched?**

Run `php -r 'var_dump(ini_get("memory_hard_limit_percent") !== false);'` with the package that serves traffic. The fix adds that setting, so true means patched even when the version string looks old (Ubuntu 24.04's fixed package still reports 8.3.6). False means unpatched or a reduced backport such as Ubuntu 22.04's PHP 8.1, so check the distribution changelog.

**How do I detect PHP2Shell exploitation?**

Watch web and WAF logs for no-file multipart POSTs with `PHP_SESSION_UPLOAD_PROGRESS`, deeply nested field names and chunked uploads; error logs for `in Unknown on line 0` and FPM exit codes 70 or 137; responses for session cookies from paths that never use sessions; and EDR for web workers spawning shells. Official Docker images log none of the early stages until `log_errors` is on.

**What is the fastest mitigation if I cannot patch?**

Set `session.upload_progress.enabled = 0` in php.ini or the FPM pool, cap `max_multipart_body_parts`, enforce body and multipart limits at the proxy (including chunked and HTTP/2 requests, which bypass `post_max_size`), and restart workers. This reduces exposure but is not a fix.

This post is updated as KEV status, exploitation reports and vendor guidance change. Corrections and detection results are welcome at [hello@detqt.com](mailto:hello@detqt.com).

## Sources

1. [PHP security advisory GHSA-w8hw-w9fx-j79w (CVE-2026-109618)](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w)
2. [PHP security advisory GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp)
3. [PHP 2026 news archive (release announcements)](https://www.php.net/archive/2026.php)
4. [php.announce #509: security release pre-announcement](https://news-web.php.net/php.announce/509)
5. [oss-security: PHP security release](https://www.openwall.com/lists/oss-security/2026/10/21/6)
6. [PHP Foundation: today's PHP security release and how we're strengthening PHP security](https://thephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/)
7. [NVD: CVE-2026-109618](https://nvd.nist.gov/vuln/detail/CVE-2026-109618)
8. [NVD: CVE-2026-109619](https://nvd.nist.gov/vuln/detail/CVE-2026-109619)
9. [CISA Known Exploited Vulnerabilities feed (checked 22 October 2026)](https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json)
10. [Calif: PHP2Shell](https://calif.io/php2shell)
11. [Wiz Research: PHP2Shell](https://www.wiz.io/blog/php2shell-php-rce)
12. [Amazon Threat Intelligence: PHP2Shell exploitation prior to public disclosure](https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/)
13. [Datadog Security Labs: PHP2Shell detection, analysis and remediation](https://securitylabs.datadoghq.com/articles/php2shell-critical-vulnerabilities-in-php/)
14. [Red Hat security bulletin RHSB-2026-013](https://access.redhat.com/security/vulnerabilities/RHSB-2026-013)
15. [Red Hat: CVE-2026-109618](https://access.redhat.com/security/cve/cve-2026-109618)
16. [Red Hat: CVE-2026-109619](https://access.redhat.com/security/cve/cve-2026-109619)
17. [VulnCheck: PHP2Shell vulnerabilities](https://www.vulncheck.com/blog/php2shell-vulnerabilities)
18. [CrowdStrike: protecting customers from PHP2Shell](https://www.crowdstrike.com/en-us/blog/crowdstrike-protects-customers-from-php-rce-zero-days/)
19. [heise: php2shell, critical flaws allow code injection in PHP](https://www.heise.de/en/news/php2shell-Critical-flaws-allow-code-injection-in-PHP-11494897.html)
20. [Cloudflare: WAF protection for the PHP vulnerabilities](https://blog.cloudflare.com/php-vulnerabilities-waf-protection)
21. [Imperva: PHP2Shell vulnerability updates](https://www.imperva.com/blog/php2shell-vulnerability-updates/)
22. [Huntress: PHP2Shell, leaked critical PHP vulnerabilities](https://www.huntress.com/blog/php2shell-leaked-critical-php-vulnerabilities)
23. [watchTowr: CVE-2026-109618 FAQ](https://watchtowr.com/intelligence/php-cve-2026-109618-faq/)
24. [Tenable: PHP2Shell FAQ](https://www.tenable.com/blog/php2shell-cve-2026-109618-cve-2026-109619-frequently-asked-questions-about-critical-php-remote)
25. [Black Kite: PHP2Shell exposure analysis](https://blackkite.com/blog/php2shell-two-critical-remote-code-execution-vulnerabilities-in-the-php-core-)
26. [SentinelOne: stopping PHP2Shell](https://www.sentinelone.com/blog/stopping-php2shell-how-sentinelone-protects-customers-from-this-attack-technique/)
27. [UK NCSC: critical security issue affecting PHP](https://www.ncsc.gov.uk/news/critical-security-issue-affecting-php)
28. [SecurityWeek: PHP2Shell patches land as first exploitation attempts hit sensors](https://www.securityweek.com/php2shell-patches-land-as-first-exploitation-attempts-hit-sensors/)
29. [CyberScoop: PHP emergency patches for PHP2Shell](https://cyberscoop.com/php-emergency-patches-php2shell-vulnerabilities/)
30. [NVD: CVE-2004-0594](https://nvd.nist.gov/vuln/detail/CVE-2004-0594)
31. [PHP bug #78876](https://bugs.php.net/bug.php?id=78876)
32. [PHP Foundation: PHP core security audit results (2025)](https://thephp.foundation/blog/2025/04/10/php-core-security-audit-results/)
33. [Quarkslab: security audit of php-src](https://blog.quarkslab.com/security-audit-of-php-src.html)

## Revision history

- 2026-10-21: First published, following the PHP security releases.
- 2026-10-22: Updated with internet exposure data, exploitation reports that came in after release and CISA KEV status as of 22 October. Clarified that FPM is exposed through the session path and replaced interim settings that were already defaults. Added Shodan exposure counts, lab findings (silent official images, the 'in Unknown on line 0' signature, FPM exit codes, memory amplification, session minting, a patch fingerprint and distribution status), logging formats, and Sigma, Suricata and Splunk rules rewritten and tested against captured lab traffic. Added a section on how hard exploitation is, drawing on Datadog, CrowdStrike, VulnCheck, Calif and Red Hat, plus VulnCheck's honeypot indicators and the URL-encoded request variant.

## Cite this article

Filip Olszak, "PHP2Shell: how an old PHP bug class came back, and how to catch it", detqt research, October 21, 2026. https://detqt.com/blog/php2shell

Written by Filip Olszak. Published by detqt research (https://detqt.com/blog).

*Original research by Filip Olszak, detqt research. Canonical source: https://detqt.com/blog/php2shell*
