What php2shell Actually Taught Us: Fleet Data From Disclosure Day
Oct 21, 2026 · @Thomas J. Raef
What happened
PHP shipped an out-of-band release on 21 October 2026 at 14:00 UTC fixing two critical remote code execution flaws, CVE-2026-109618 and CVE-2026-109619, both rated CVSS v4.0 9.2.
The details leaked a day early, which gave everyone an unusual thing: roughly 20 hours of warning with no patch available. That gap is what made the day worth studying. We dug in immediately and started analyzing the leaked details.
Neither bug is a parsing mistake you can filter. CVE-2026-109618 is about PHP using longjmp() for out-of-memory errors and leaving request memory in an invalid state that shutdown then acts on. CVE-2026-109619 is about request-owned global state surviving past the request that owned it, so a later request on the same worker reads a dangling pointer. Both are reached through ordinary multipart form submissions.
Read that again.
Out-of-memory errors and leaving request memory in an invalid state that shutdown then acts on.
Request-owned global state surviving past the request that owned it, so a later request on the same worker reads a dangling point.
Fixed releases are 8.2.35, 8.3.36, 8.4.27 and 8.5.12. Anything below those, on any branch, is vulnerable. Anything below 8.2 gets no fix at all.
The natural experiment
We worked three client fleets first, on disclosure day. Same CVE, same tooling, same engineer, same hours. The outcomes had almost nothing to do with what we did that day.
| Fleet | Servers | Sites | OS | Outcome by end of day |
|---|---|---|---|---|
| A | 1 | 78 | Ubuntu 18.04 | Cannot be patched. Ever. |
| B | 15 | 385 | Mixed 20.04 / 22.04 / 24.04 | 8 patchable, 6 stuck, 22 sites on dead PHP branches |
| C | 15 | 82 | All Ubuntu 22.04 | Fully patched and verified |
Fleet C was fixed. Not mitigated, fixed, with every PHP branch at or above the fixed release and workers restarted to prove it.
Fleet A received the same nginx mitigations, the same analysis and considerably more of our attention. It ended the day exactly as exposed as it started, because no patched package exists for it and none ever will. We did add some configuration differences to help narrow the infection opportunities.
The difference was not effort or expertise. It was decided years earlier, by whoever did or did not schedule an OS upgrade.
Patchability is the security control
The single most useful thing you can do for your next critical CVE is make sure you are able to install the fix. If you don’t migrate to the latest Ubuntu when it’s released, you risk not being able to have time to migrate, when something like this happens.
Ubuntu 18.04 left standard support in May 2023. Ubuntu 20.04 left in May 2025. The PHP packaging most WordPress hosts rely on, Ondřej Surý’s repository, stopped publishing for both. A server on either release has silently opted out of future PHP security updates, and nothing announces this. The server keeps working. Sites keep loading. Nobody notices until the day a fix exists and cannot be installed.
On Fleet B we found 20.04 servers running PHP 8.3.21 while the current release was 8.3.36. Fifteen releases behind, on a branch that is still supported upstream, because the repository stopped serving that OS. Those servers had been accumulating unpatched PHP CVEs for months before anyone had heard of php2shell.
Ubuntu Pro does not rescue this. Its Expanded Security Maintenance covers packages in Ubuntu’s own main repository. PHP installed from a third-party repository was never covered, so paying for ESM on a box whose PHP comes from Surý buys you a patched kernel and an unpatched interpreter.
The practical version: an OS upgrade is not maintenance you defer when it is inconvenient. It is the thing that determines whether you have the option to respond at all. It’s like waiting for the “perfect” time to update WordPress, or plugins. If you don’t commit to updating when it’s available, you may not have the time to update when the site is infected.
You cannot defend what you have not inventoried
We spent the first hours of disclosure day discovering what was actually running. That is time you do not have when a weaponised exploit is circulating.
What turned up on one 78-site server:
- Eight PHP versions installed: 5.6, 7.0, 7.1, 7.2, 7.3, 7.4, 8.0 and 8.1
- Five FPM daemons running, two of them serving zero sites
- No site anywhere on a PHP branch that could receive the fix
Across another fleet, 33 unpatchable FPM daemons were running with no sites attached to them at all.
The pattern worth internalising is that “installed” and “in use” drift apart quietly. A PHP version stays installed after the last site moves off it. The daemon keeps starting at boot. The socket keeps listening. Nothing in a control panel highlights it, because from the panel’s point of view nothing is wrong.
The inventory that matters is not a list of sites. It is, for every server: OS release and its support horizon, every PHP branch installed, which branch each site actually runs on, and the current patch level of each. Four facts. Knowing them on an ordinary Tuesday is the difference between triage and archaeology.
The cheapest security work is deleting things
Every item below was live, internet-facing, running unpatchable PHP, and serving no business purpose:
- Around 35 staging and development subdomains on a single production server, all public
- 33 PHP-FPM daemons across one fleet with zero site pools attached
- A phpMyAdmin instance on an end-of-life PHP branch
- A staging site running PHP 7.4 while its own production site ran 8.2, two major versions apart, cloned from production and forgotten
That last one deserves attention. It was a full copy of a live site, with the same plugins and presumably a copy of the database, sitting two major PHP versions behind, protected by a renamed login page. When we asked, it was turned off in the control panel. The pool file stayed, the daemon kept running, and the site kept answering requests.
Staging sites are the clearest case. They exist to be broken, they rarely get updated, they are usually clones of production data, and almost nobody puts them behind an IP allowlist. On the fleet where we counted, they were roughly 45% of the sites on the box.
Before you harden anything, work out what you could simply remove. It is faster than mitigation and the risk reduction is total rather than partial.
Architecture set the ceiling
None of the sites we manage were successfully exploited. The most honest explanation is a decision nobody made for security reasons.
The confirmed, reliable exploit path runs against Apache with mod_php. There, a single unauthenticated request triggers the flaw before any application code executes, because mod_php invokes the request shutdown procedure even when request startup has failed. One request, no account, no application dependency.
PHP-FPM behaves differently. It terminates the child process when request startup fails, which destroys the corrupted state before shutdown can act on it. PHP’s own advisory for CVE-2026-109618 records that no complete FPM exploit was demonstrated. For CVE-2026-109619 an FPM path was validated, but it needs an attacker-reachable application operation that publishes state and then hits a fatal error, and it needs the producing and consuming requests to land on the same worker.
Every fleet discussed above runs nginx with PHP-FPM, because that is what the hosting platform uses. That choice, made for performance and multi-tenancy years ago, is why the demonstrated single-request RCE did not apply.
We also monitor a separate population of servers running OpenLiteSpeed, which uses LSAPI rather than FPM. None of those showed signs of a successful breach either. PHP’s advisories place LSAPI in much the same category: for CVE-2026-109618 it models FPM, with application-specific exploits described as likely but unvalidated, and for CVE-2026-109619 it is a conditional candidate because it can apply INI values from the environment early in the request. Neither is the confirmed application-independent path.
Two different process models, same result. That is a stronger signal than one stack getting lucky, and it points at the same conclusion: the confirmed reliable exploitation needed mod_php’s behaviour of running request shutdown unconditionally.
The transferable lesson is not “use FPM”. It is that the decisions constraining blast radius are architectural and are made long before the incident: process model, per-site system users, per-site pools, whether one compromised site can reach another. On the day, those are fixed. You are simply finding out what you chose.
Detect execution, not just files
This is the gap we would most want closed before the next one.
File integrity monitoring is excellent at what it does. It catches dropped webshells, modified core files, injected redirects. We ran it against every server we examined and it is a large part of why we can say nothing landed.
It is also blind to an entire category of outcome. A memory-corruption RCE gives an attacker code execution inside the PHP worker. If what they do is read wp-config.php, connect to the database, dump the customers table and disconnect, nothing is written to disk. The file monitor sees a quiet day.
The cheap complement is alerting when a PHP worker spawns a child process it has no business spawning: sh, bash, curl, wget, nc, python, perl. In normal WordPress operation this almost never happens. When it does, it is nearly always the moment after something went wrong.
We also looked for crashed or restarted FPM workers, since both of these bugs work by corrupting the interpreter. A segfault in php-fpm during a burst of multipart POSTs is a strong signal, and it costs nothing to watch for.
If you take one operational change from this, make it process-spawn alerting. It is a handful of rules, it works on any stack, and it catches the thing your file monitor structurally cannot.
What did not help
Worth naming, because these are what people reach for first.
disable_functions. The servers we looked at already had thorough lists blocking exec, system, passthru, shell_exec, proc_open, popen and the rest. It makes no difference here. The exploit chain reaches code execution by forging a function-call structure in freed heap memory and jumping to the handler, so PHP never consults the blocklist. Keep it, because it raises the cost of a PHP-level foothold, but do not count it against this class of bug.
Hidden login URLs. One site had wp-login.php renamed via a security plugin. Both CVEs trigger during request parsing, before WordPress loads. An unauthenticated POST to the homepage reaches the vulnerable interpreter whether or not a human could find the login page.
WAF signatures. There is no distinctive payload to match. The trigger is a multipart request shaped to exhaust memory or to leave state behind. Cloudflare reported attackers altering payloads to evade their rules within hours. Rate limiting multipart POSTs helps by reducing volume; signature matching does not.
Security software that was never switched on. One server had ModSecurity installed, the OWASP Core Rule Set deployed, and SecRuleEngine On configured. Not a single site had it enabled. It had been sitting there since 2021. Installed is not deployed, and nothing in the stack will tell you the difference.
What the attack traffic actually looked like
We added multipart request logging to every server mid-morning, which gave us a record of what arrived.
The clearest case named itself. A scanner sent multipart POSTs with the boundary string ------------------------bissa_cve_boundary. Nobody doing serious exploitation puts the CVE in a header. That is inventory building, or a research scanner, roughly 100 minutes after the advisory went public.
A second source was more deliberate: paced POSTs every two to four seconds, 60-character random hex boundaries no browser produces, and a different browser user agent on every single request. Chrome 131, Edge 130, Firefox 133, Chrome on macOS, within twelve seconds from one address. Rotating user agents is a tell that something is working to avoid rate limiting and fingerprinting.
A third looked hostile and turned out to be form spam hitting a Gravity Forms contact page. Short hex boundaries, consistent timing, a legitimate endpoint. Worth saying because the first instinct on disclosure day is to read every anomaly as exploitation.
Across everything we examined: no spawned shells, no new PHP files, no modified uploads directories, no FPM crashes, nothing written outside the security plugins’ own logs.
Industry-wide, watchTowr reported active exploitation attempts of both CVEs and Cloudflare saw attacks before the patch. As of this writing no confirmed compromise of a specific server has been published anywhere.
Two cautions against reading too much into that. It has been hours, not weeks, and breaches surface late. And our confidence rests largely on file-based evidence, which is exactly the evidence a memory-only exploit would not produce.
Before the next one
In rough order of how much they decided the outcome this time.
- Get every server onto a supported OS. Check the release and its support end date today. If it is Ubuntu 18.04 or 20.04, you cannot patch PHP, whatever your host tells you. Budget the migration now rather than discovering it mid-incident.
- Record four facts per server. OS release, PHP branches installed, which branch each site runs on, current patch level of each. Keep it current automatically. This is a Tuesday task, not a disclosure-day task.
- Delete what nothing uses. Staging clones, forgotten subdomains, PHP daemons with no sites, database admin tools. Full removal beats partial mitigation and takes less time.
- Alert on PHP spawning processes.
sh,bash,curl,wget,nc,python,perlfrom a worker. This is the detection your file monitor cannot give you. - Verify patches rather than trusting them. One client told us their fleet was fully updated. All 14 servers were still running pre-fix PHP on every branch. Check the running version, and check that FPM was restarted rather than reloaded, because a reload leaves the old binary in the existing workers.
- Check every PHP install, not just the obvious one. OpenLiteSpeed keeps its interpreters under
/usr/local/lsws/lsphp*/bin/php, packaged by LiteSpeed on its own release schedule. Tooling that inspects/etc/phpfinds nothing there and reports nothing, which reads as clean when it means unexamined. - Build the rollout path before you need it. Being able to patch and verify a fleet in one command is itself a control. The window between disclosure and patch is where the risk lives.
- Turn on what you already pay for. Audit whether your WAF, your monitoring and your scanning are actually enabled per site, not merely installed.
One generic measure earned its place here: recycling PHP-FPM workers frequently, via a low pm.max_requests. Both confirmed paths for CVE-2026-109619 require the producing and the consuming request to reach the same worker. That defence was not designed for this bug, which is rather the point. Hardening that does not depend on knowing the vulnerability is the kind that works on the day you have never heard of it.
