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.


On this page
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.
X-Powered-By: PHP, split by advertised version. Source: Shodan host counts, 22 October 2026; details under Exposure.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.
| 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.
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.
CVE-2004-0594
Stefan Esser's
memory_limitbug. An out-of-memory abort duringzend_hash_initoverwrites a destructor pointer. Same class, PHP 4 era. SourcePHP 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
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
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
Wiz reports more paths
Wiz, having confirmed unauthenticated RCE against Drupal and WordPress, reports its findings. OpenAI reports further vectors on 18 September. Source
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
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
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
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
Current status
Attempts are being reported by several sensor networks. Neither CVE is in CISA's KEV catalogue yet. Source
How exploitation works
At a conceptual level, the published chains have two stages, and both happen inside PHP's input handling.
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 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.
| 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.
X-Powered-By: PHP by country, top ten. Source: Shodan, 22 October 2026.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.
| 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 | 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 | An exploit that works on x86-64 and ARM64 and survives worker restarts | mod_php. ASLR defeated over HTTP on ARM hosts |
| VulnCheck | 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 | 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 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.
| 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.
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.
# 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 changelogDistribution patch status
| 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.
log_errors is on, which it isn't on official Docker images.WAF, CDN and access logs
- Multipart POSTs containing a
PHP_SESSION_UPLOAD_PROGRESSfield, 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-urlencodedPOST 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, orContent-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: PHPSESSIDfrom 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%XXescapes, is the memory leak itself.
If you are behind Cloudflare, filter Security Events on the rule IDs listed in Cloudflare's post. 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. TheUnknown on line 0part 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 loggingupstream sent unexpected FastCGI record: 3. No segfaults appeared in testing. Hard memory limit of ... bytes exhaustedfollowed 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 exceededandInput variables exceeded, which show requests pushing against the parser's limits.- FPM pools respawning workers much faster than their normal baseline.
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 >= 5Logging 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.
# 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;# 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; 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 = yesPost-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.
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: highDeviceProcessEvents
| 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, AccountNameNetwork 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.
# 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;)// 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,.pharor.shfiles in web roots and upload directories, written by the web server user. - Changes to
.htaccessor.user.ini, andauto_prepend_filein 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_PROGRESSis 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.
# 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_percentsetting (see Our testing) rather than trusting version strings. - Don't treat a missing
X-Powered-Bybanner 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_phpfirst, 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.
; 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.enabledremoves 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_varsdoesn't count empty file parts either.max_multipart_body_partsis 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 = -1is sometimes suggested for CVE-2026-109618. It is incomplete, becausemmap()failure can still reach the bug, and it raises the risk of denial of service. Do not useRLIMIT_ASorRLIMIT_DATAas 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 = 1to 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.
Sources
- PHP security advisory GHSA-w8hw-w9fx-j79w (CVE-2026-109618)github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w
- PHP security advisory GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp
- PHP 2026 news archive (release announcements)www.php.net/archive/2026.php
- php.announce #509: security release pre-announcementnews-web.php.net/php.announce/509
- oss-security: PHP security releasewww.openwall.com/lists/oss-security/2026/10/21/6
- PHP Foundation: today's PHP security release and how we're strengthening PHP securitythephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/
- NVD: CVE-2026-109618nvd.nist.gov/vuln/detail/CVE-2026-109618
- NVD: CVE-2026-109619nvd.nist.gov/vuln/detail/CVE-2026-109619
- CISA Known Exploited Vulnerabilities feed (checked 22 October 2026)www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json
- Calif: PHP2Shellcalif.io/php2shell
- Wiz Research: PHP2Shellwww.wiz.io/blog/php2shell-php-rce
- Amazon Threat Intelligence: PHP2Shell exploitation prior to public disclosureaws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/
- Datadog Security Labs: PHP2Shell detection, analysis and remediationsecuritylabs.datadoghq.com/articles/php2shell-critical-vulnerabilities-in-php/
- Red Hat security bulletin RHSB-2026-013access.redhat.com/security/vulnerabilities/RHSB-2026-013
- Red Hat: CVE-2026-109618access.redhat.com/security/cve/cve-2026-109618
- Red Hat: CVE-2026-109619access.redhat.com/security/cve/cve-2026-109619
- VulnCheck: PHP2Shell vulnerabilitieswww.vulncheck.com/blog/php2shell-vulnerabilities
- CrowdStrike: protecting customers from PHP2Shellwww.crowdstrike.com/en-us/blog/crowdstrike-protects-customers-from-php-rce-zero-days/
- heise: php2shell, critical flaws allow code injection in PHPwww.heise.de/en/news/php2shell-Critical-flaws-allow-code-injection-in-PHP-11494897.html
- Cloudflare: WAF protection for the PHP vulnerabilitiesblog.cloudflare.com/php-vulnerabilities-waf-protection
- Imperva: PHP2Shell vulnerability updateswww.imperva.com/blog/php2shell-vulnerability-updates/
- Huntress: PHP2Shell, leaked critical PHP vulnerabilitieswww.huntress.com/blog/php2shell-leaked-critical-php-vulnerabilities
- watchTowr: CVE-2026-109618 FAQwatchtowr.com/intelligence/php-cve-2026-109618-faq/
- Tenable: PHP2Shell FAQwww.tenable.com/blog/php2shell-cve-2026-109618-cve-2026-109619-frequently-asked-questions-about-critical-php-remote
- Black Kite: PHP2Shell exposure analysisblackkite.com/blog/php2shell-two-critical-remote-code-execution-vulnerabilities-in-the-php-core-
- SentinelOne: stopping PHP2Shellwww.sentinelone.com/blog/stopping-php2shell-how-sentinelone-protects-customers-from-this-attack-technique/
- UK NCSC: critical security issue affecting PHPwww.ncsc.gov.uk/news/critical-security-issue-affecting-php
- SecurityWeek: PHP2Shell patches land as first exploitation attempts hit sensorswww.securityweek.com/php2shell-patches-land-as-first-exploitation-attempts-hit-sensors/
- CyberScoop: PHP emergency patches for PHP2Shellcyberscoop.com/php-emergency-patches-php2shell-vulnerabilities/
- NVD: CVE-2004-0594nvd.nist.gov/vuln/detail/CVE-2004-0594
- PHP bug #78876bugs.php.net/bug.php?id=78876
- PHP Foundation: PHP core security audit results (2025)thephp.foundation/blog/2025/04/10/php-core-security-audit-results/
- Quarkslab: security audit of php-srcblog.quarkslab.com/security-audit-of-php-src.html
Revision history
- First published, following the PHP security releases.
- 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.