PHP2Shell: what we observed and what to do
PHP2Shell is a pair of critical, unauthenticated RCE flaws in PHP core (CVE-2026-109618 and CVE-2026-109619), fixed on 21 October 2026. What the two bugs are, what to patch, and what the ELLIO Deception Network recorded on disclosure day, including a scanner that doubles as a botnet dropper.
On 21 October 2026 at 14:00 UTC the PHP project released fixes for two remote code execution flaws in PHP itself, CVE-2026-109618 and CVE-2026-109619, nicknamed PHP2Shell after a leaked exploit tool. Both are rated CVSS 9.2, and both are reached while PHP parses a request, before any application code runs. An unpatched PHP server that accepts a POST is exposed with no login and no application bug needed. The first PHP2Shell request reached ELLIO sensors at 08:36 UTC, five and a half hours before the fix was public.
We are updating this post as the situation develops. New findings go in dated updates at the end; the text above stays as written.
The two bugs
CVE-2026-109618, RCE via OOM corruption. When PHP runs out of memory mid-request it jumps straight out of the allocator, which can leave request memory half initialized, and request shutdown then runs over that memory. An attacker controls when memory runs out through the multipart form parser: a body with no usable declared length, many headers inside a single part, empty file controls that do not count toward the upload limits, or deeply nested field names. No session and no particular field is involved. PHP confirms it as exploitable against Apache mod_php with default settings.
CVE-2026-109619, RCE via stale global state. A request that dies at the wrong moment, through a fatal error, a timeout or an out-of-memory condition, can leave state behind in the PHP worker. A later request handled by that same worker can then read that state after it has been freed. Session upload progress is one of the code paths that reads it, which is why the PHP_SESSION_UPLOAD_PROGRESS form field shows up in attempts. PHP validated this path against both mod_php and PHP-FPM.
Early coverage mostly attributed that field, and the advice to switch it off, to CVE-2026-109618. In PHP's advisories the upload progress path belongs to CVE-2026-109619 alone, and the CVE-2026-109618 advisory never mentions sessions. The distinction matters: the two bugs need different stopgaps. Both advisories are worth reading in full: GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp.
What to do
- Patch. The fixed releases are PHP
8.2.35,8.3.36,8.4.27and8.5.12. PHP states that an update is the only complete fix for either bug. Patches exist only for the branches still supported, which means 8.2 and later. If you are running 8.1 or earlier, including every version of PHP 7 and PHP 5, there is no official fix for these two bugs and there will not be one. On those versions the remediation is a move to a supported branch. Plan that move this week and treat it as urgent. - For CVE-2026-109618, until you can patch, PHP's advisory puts
memory_limit=-1first, enforced so nothing can restore a finite limit, and every worker restarted afterwards. Do not substituteRLIMIT_ASorRLIMIT_DATA; use container or cgroup memory limits instead. - For CVE-2026-109619, until you can patch, set
session.upload_progress.enabled=0and restart workers. On PHP 8.5 also setfatal_error_backtraces=0. This closes the session path only and does nothing for CVE-2026-109618. - At the front end, block
multipart/form-datato applications that accept no uploads. Where uploads are needed, cap the body size, the number of parts and the number of headers per part: a size cap on its own misses bodies that stay underpost_max_size. Keep those caps after patching, because a patched PHP still terminates a worker that runs out of memory.
What our sensors saw on 21 October
The sequence of events, in UTC:
- 27 August: the reports reach PHP's security team.
- 25 September: the fix pull request opens, with the final commit on 18 October.
- 20 October: details and a working proof of concept leak, and PHP brings its release forward by a day.
- 21 October, 08:36: the first PHP2Shell request reaches ELLIO sensors, before the fix is public.
- 21 October, 14:00: PHP publishes the fixed releases and both advisories.
- 21 October, 16:05: the first requests shaped like the CVE-2026-109618 exploit arrive.
- 21 October, 20:40: a bot that sweeps for several other vulnerabilities adds PHP2Shell to its run.
The 08:36 sender was a scanner. Its user agent, php2shell-scan/109618, names CVE-2026-109618 while the request carries the session upload progress field, which belongs to CVE-2026-109619: tooling built from the leaked material rather than from PHP's advisories. In eight minutes it reached 36 sensors. The same family came back later under the user agents php2shell-109618 and php2shell-verify/1.0, from addresses in three unrelated networks sharing one multipart boundary string.
At 16:05, two hours after the release, different tooling appeared, and it matches the out-of-memory bug rather than the session path. Its requests carry the amplification shapes PHP's advisory names: hundreds of empty file fields and field names nested dozens of levels deep, packed into bodies of a few hundred kilobytes, with the upload progress field in the same request. One address sent 49 of them in twenty minutes and reached 73 sensors. It marks its own multipart boundaries with the string p2shell. That address has been in our data since December 2025 and was already classified malicious for earlier exploitation attempts.

By 21:00 UTC, 25 addresses had sent PHP2Shell requests. Shared TLS and TCP fingerprints, shared boundary strings and identical body sizes group them into a handful of tool families, several operating from unrelated hosting networks on different continents. Several send no user agent at all.
What the scanners carry
These scanners do more than fingerprint. Their bodies are padding plus 149 identical filler parts, and every one of those parts ends with a command for the target to run if the use-after-free lands on it.
One address sent two versions of the same tool about six hours apart. The first carries a loader: fetch a binary from a hard-coded address into /tmp, make it executable and run it with the name of the vector as its argument, the convention Mirai-like loaders use. A host that fell to that request would have been infected by the same request that found it, with no second visit. The second version carries no address at all, only uname -a; id and a fixed canary string the operator can look for in the response. That is the ordinary way to confirm blind code execution before spending a payload on a host.

The second-stage binary is 179,632 bytes, ELF x86-64, statically linked and stripped, with almost nothing in its strings beyond C library text and no configuration in the clear. It did not appear in the public malware sources we could query, and the staging host it comes from has never reached our sensors, so a feed built from observed scanning alone would carry neither the host nor the file. Hashes are below.
The same form field also carries an older technique unrelated to these two CVEs: write PHP into the session file through session upload progress, then include it from a later request. We tag that separately so it is not counted as PHP2Shell.
Each wave of tooling matched whatever material was public at the time. The scanner that arrived before the patch names CVE-2026-109618 in its user agent while sending the field that belongs to CVE-2026-109619, because the leaked proof of concept had folded the two together. Requests matching what the advisory actually describes for the out-of-memory bug, hundreds of empty file controls and field names nested dozens of levels deep, did not appear until two hours after PHP published. Nobody had to reverse engineer the patch: the mitigation list names the amplification vectors.
By late evening the tooling had stopped being one script. One address alternated between a validation request and a delivery request built on the same skeleton. Four addresses in unrelated networks sent byte-identical bodies within three minutes, which reads as one operator working through a host list. Another pins a predictable session identifier so a later request can find what it wrote. No public source has reported a confirmed compromise yet, but the stages of a working chain are being assembled in the open.
The widest run of the day came last. From 20:40 one address reached 123 sensors in eighteen minutes, and earlier this month we watched the same address exploit React2Shell and sweep for exposed Docker and Spring Boot endpoints. That is the part worth planning for: a new unauthenticated RCE gets picked up by the bots that were already sweeping for everything else, and those run continuously.
Indicators
In your own traffic:
- user agents
php2shell-scan/109618,php2shell-109618,php2shell-verify/1.0 - multipart boundary strings beginning
----p2s, including----p2shell - a POST whose multipart body contains a part named
PHP_SESSION_UPLOAD_PROGRESS. Sites that use PHP's upload progress feature send this field in ordinary uploads too, so treat it as a signal, not a verdict - multipart POSTs built from hundreds of empty file fields, or with field names nested many levels deep
- second-stage download
http://92.118.112.100/nfs, written to/tmpand run with the argumentphp - the validation canary
JACKALPOC, sent asuname -a; id; echo JACKALPOC - second-stage binary SHA-256
0c5486f06620e26e316f4b33f318b0d646d5e1133789331aa2fed012360d1486, MD5b61a098fc62a2c2696e196dca78da71a - request body SHA-256
43898a0a53bbd25d31ad9f5018ed3182be98a66109b22c9cab545a9fc447992a(scanner) and7bc41ed79a9ae909927a9e40701736460a8cb70fca16ce1510e58677aca764b6(out-of-memory tool)
On your own hosts:
- PHP workers exiting on memory errors in bursts
- a PHP worker process starting a shell, an interpreter or a network tool
Find it in the ELLIO Platform
tag_id: "php2shell_exploit"returns the addresses we have seen send PHP2Shell requests.cve: "CVE-2026-109618" OR cve: "CVE-2026-109619"returns every address carrying a tag mapped to either bug.http.user_agent: "*php2shell*"returns the tools that name themselves.
Results cover a rolling 90 days, and you can push matching addresses straight to a dynamic block list from platform.ellio.tech.
Update, 21 October 2026, 21:30 UTC: the second stage, taken apart
The file the scanner wanted to install is a Mirai variant compiled for 64-bit servers; Mirai itself was written for embedded devices. Four things identify it: its command parser uses Mirai's wire format, its attack option numbers match the leaked Mirai source value for value down to the gaps and oddities in that numbering, its dispatch table has the same shape, and it takes a tag from the command line and stores it the way Mirai does. That last one is why the request that plants it runs it as ./nfs php.
This build encrypts its entire configuration, 473 entries, with ChaCha20. Mirai and nearly every fork of it hide their strings behind a repeating single-byte scheme that a brute force breaks in seconds, so the usual shortcut does not work here: a first look at the sample turns up nothing but C library error text and the file reads as if it has no configuration at all. The key sits in the binary at a fixed address and the same keystream is reused for every entry, so recovering one entry recovers all of them. For anyone triaging the same family, the key is 5a3f2e7b8c9d0a6e3b4f7c8a1d5c2b0d6f1a0c3e2d8e4a091c7e5a4d3c2b6a08, the nonce is 4a1b00ff2d7c3a1200010305, and the block counter starts at 1 for every string.
On a host that runs it, the bot ignores the usual terminal signals, moves to the filesystem root, overwrites its own command line and process name, then forks and detaches from the terminal. The name it adopts comes from a short list of ordinary service names, so the name in a process listing tells you nothing, although the executable path still resolves and gives it away. It opens no listening port, only one long-lived outbound TCP connection to its command and control domain on one of fourteen high ports, picked at random per connection. The traffic is obscured with a single-byte XOR in both directions, trivial to unpick but enough to defeat a plain string match on the wire. Its first message is a profile of the machine: processor count, memory, architecture, hostname, and the tag naming the vulnerability used to plant it.
From there it waits for orders. What it can be told to do:
- flood a target, with 22 working methods: raw-socket floods giving the operator control of individual IP and TCP header fields, UDP floods both raw and connected, an ICMP flood, a GRE over Ethernet flood, and connection-exhaustion attacks
- run a layer 7 HTTP flood that rotates five browser user agents and appends cache-busting parameters, so the requests do not collapse into one cacheable URL
- attack game servers specifically, with one method for Minecraft Java on its default port 25565 and another for FiveM, the multiplayer modification for Grand Theft Auto
- spoof its source addresses to look like a chosen country, using 41 built-in per-country address tables plus a much larger dedicated table for China
- open an interactive shell on a pseudo-terminal, which makes this a full backdoor
- hunt and kill other processes, picked by generic heuristics: binaries deleted from disk, binaries running out of temporary directories, suspicious memory mappings
- replace itself on command, detecting the host architecture, checking that the target directory is writable and executable, and preserving the infection tag through the upgrade
The shell makes every compromised host directly available to the operator. The process killer is indiscriminate enough to matter to defenders: its rules target the shape of malware, so legitimate software running from a temporary directory can be killed along with the competition.
Two results favor defenders. The bot cannot spread on its own: there is no telnet or SSH password guessing, no credential list anywhere in the decrypted configuration, no port scanning and no exploit scanner. It also has no persistence, writing no startup entry, no cron job and no service unit, so a reboot removes it. This is disposable infrastructure: it gets in through someone else's scanner, joins the pool, and is replaced on the next push. The scanning described earlier in this post does all of the work.
The game-server methods and the country-by-country source spoofing point at what it is for. Minecraft and FiveM servers are the stock targets of the rent-an-attack market, so this looks like a bot built to sell capacity.
Indicators from the binary. Domain names and strings are the operator's own and are reproduced exactly, crude wording included, because detection depends on matching them character for character.
- command and control domain
thunder.godofthunder.st, filling all eight configured slots - connects to one of fourteen high ports, chosen at random:
50217, 50341, 50488, 50613, 50792, 50904, 51127, 51289, 51456, 51603, 51871, 52134, 52398, 52645 - second-stage and self-update host
hackattackkaboom.botlesscucks.st - staging host that served the file
92.118.112.100on AS215540 - sample SHA-256
0c5486f06620e26e316f4b33f318b0d646d5e1133789331aa2fed012360d1486, MD5b61a098fc62a2c2696e196dca78da71a, 179,632 bytes, statically linked and stripped - renames its process to one of
telnetd, udhcpc, upnpd, watchdog, syslogd, klogd, uhttpd, uchttpd - prints one line to standard output before detaching, from a fixed set that includes
im in the matrixandbig brother is watching: a useful string wherever startup output is captured - writes a one-byte probe to
/dev/shm/.cache.soand removes it, when it cannot create an anonymous in-memory file - falls back to eighteen hard-coded public DNS resolvers when normal resolution fails, so blocking the domain on your own resolver does not cut the channel
The binary numbers its flood methods and does not name them, so the names used above describe what each one does, read from its socket setup and protocol numbers.
The configuration holds one more thing, unused. Alongside the domains there is an Ethereum Name Service block: two blockchain names, a set of public Ethereum endpoints, and the shape of a lookup that would read command and control addresses out of a name's text records. C2 addresses published on a public blockchain would be awkward to take down. No code in this build reads any of it, so either the configuration is shared across builds with the feature switched off here, or the operator is preparing it. We are checking whether those names carry anything today.
Update, 21 October 2026, 22:10 UTC: the command and control address is a decoy
The command and control domain resolves. thunder.godofthunder.st answers 134.154.64.199 with a 300 second time to live, identically from every public resolver we asked. That address belongs to California State University East Bay.
A second domain in the same configuration gives the trick away. hik.gettingheadfromduckysmom.st answers with three addresses, two of them 224.123.45.198 and 224.124.45.198. Those are multicast addresses. Nothing can open a connection to them. A bot configured with them would sit there doing nothing, so the bot is not using the address as published.
It swaps the halves. Reverse the two 16-bit halves of 224.123.45.198 and you get 45.198.224.123; 224.124.45.198 becomes 45.198.224.124. Both are real hosts at a British VPS provider, and our sensors have recorded both of them scanning, the second as recently as 19:30 UTC today. The third address, an Australian consumer line, becomes 204.76.203.219 at a German provider, also in our data.
The reason we believe this is not pattern matching is that it closes a circle in our own telemetry. 45.198.224.169, the address that sent the Mirai loader to our sensors at 14:38 today, sits in the same /24 and the same network as the two recovered addresses. The scanner that plants the bot and the infrastructure the bot is told to call are neighbors.
The family itself is documented. Nokia Deepfield published research on it in May 2026, under the name potassium, listing the same ChaCha20 key and nonce byte for byte, the same single-byte traffic key, and the address swap described explicitly. What is not documented is this campaign: godofthunder.st was registered on 12 October, nine days before the attacks, and neither it nor the binary we pulled appears in any public source we checked.
The Ethereum fallback is real, and newer than the domain. Both names were registered on 13 October, around 21 hours after godofthunder.st. One of them carries a live text record holding an address, rewritten four times during a two-hour session on 14 October. The address it holds today sits in a range that is not announced to the internet at all, so it would not work: this reads as a mechanism being tested rather than one in service. The same name carries a second record, which says: woah woah. there is nothing to see here! please go away or else i will call the police.
What this means for blocking, and it is the opposite of the usual advice:
- Block by address, not by name. The bot carries eighteen hard-coded public resolvers, so a DNS block, an RPZ or a local sinkhole never sees the query.
- Block outbound port 53 to everything except your own resolvers. Against this family that single control is worth more than any blocklist, because it forces the bot back onto a resolver you can see.
- Alert on the published addresses, do not block them. They are a university, an Australian consumer line, and in the Ethereum records a United States Department of Defense range. Traffic to any of them on the fourteen configured ports is a high-confidence infection signal, and blocking them wholesale breaks unrelated things.
- Hunt the registration pattern rather than the names:
.stdomains registered through Sirdio or Istanco on the nameserver pairns1.istanco.comandns2.istanco.com. The next domain will almost certainly match it before anyone has seen it used.
One honest label on the above: the swap is confirmed for the two multicast addresses, because they cannot function as written, and the recovered hosts are confirmed active in our own data. Calling the university address specifically a decoy is inference from the same pattern, not proof.
Written by
A group of researchers at ELLIO, transforming insights from mass exploitation and network reconnaissance into real-world cybersecurity defenses.