1. How A Quiet Week Turned Into An Emergency
On the evening of 20 October, a post started going around claiming that two PHP bugs would be disclosed in two days. That is the kind of message that wrecks a release schedule. PHP published an advance notice the same night and pulled its security release forward. Around the same time, one of the research teams reportedly posted a video of its working proof of concept by accident.
The fixes landed on 21 October at 14:00 UTC. A few hours later, watchTowr reported exploitation attempts in its honeypots. Cloudflare reported attack traffic, and attackers changing their payloads to get around its firewall rules. Five days after the release, CISA added both bugs to its Known Exploited Vulnerabilities (KEV) catalog, which means exploitation in the wild is confirmed. US federal agencies were told to fix them by 29 October.
The bugs are CVE-2026-109618 and CVE-2026-109619, and researchers started calling the pair PHP2Shell. Both are rated 9.2 out of 10. Neither needs an account or a password. A crafted request is enough to run code on the server.
Here is what makes this one different from a normal PHP point release: where the bugs live. Not in WordPress. Not in Laravel. Not in some plugin. They live in PHP, the runtime underneath all of them. You can patch every application you own and still be wide open.
Most teams also hit the same wall within an hour. Where is PHP running in our company? It sits behind old marketing sites, internal tools, a control panel someone installed years ago, a container image nobody rebuilt. This post follows the questions in the order you have to answer them: what broke, why cleanup is harder than it looks, and how we find and fix it in Vajra. If you just want the checklist, jump to Section 6.
The timeline
17 October: Amazon's threat intelligence team says it saw an exploitation attempt, four days before the public fix, as relayed by Tenable. We could not confirm that independently.
20 October: a post claims the bugs will be disclosed in two days. PHP publishes an advance notice and pulls the release forward.
21 October, 14:00 UTC: the fixes ship. Honeypot and firewall vendors report attacks within hours.
23 October: PHP publishes an informational advisory for a third bug, CVE-2026-115560, with no fix yet.
24 October: a researcher reports code execution on three of his honeypots running PHP 8.4.26, matching CVE-2026-109618. The setup was a single-worker Apache
mod_phpserver that favours the exploit, so it says little about how reliable the attack is elsewhere.26 October: CISA adds both CVEs to the KEV catalog.
Look at the gaps. Between "a fix exists" and "people are using the bug" there was almost nothing. If your plan was to patch next sprint, this is the incident that shows why that is risky.
2. What Actually Broke
2.1 Bug one: running out of memory at the worst possible moment
CVE-2026-109618 lives in what PHP does when it runs out of memory in the middle of a request. It has to stop. The way it stops is a C function called longjmp(). Think of it as an emergency exit: PHP drops whatever it was doing and jumps out to a recovery point.
The trouble is timing. If that exit happens while PHP is still halfway through setting up the request's data, that data is only partly built. Request shutdown then runs over it as though it were complete, and acts on invalid state. The failed allocation does not even have to be related to the half-built data. Any allocation that fails at the wrong moment can trigger it, and an attacker can shape a request so that one does.
PHP's advisory (GHSA-w8hw-w9fx-j79w) lists several ways to get there through a multipart upload request: no limit on the number of headers per part, empty file controls, and deeply nested field names. Against Apache with mod_php and default settings, the advisory reports unauthenticated code execution. It says FrankenPHP and LiteSpeed's LSAPI are likely affected too, and that PHP-FPM is open to the same flaw in principle, although a full FPM exploit has not been shown.
The fix removes longjmp() from the out-of-memory path. There is now a "soft" limit that lets the request finish cleanly, and a "hard" limit, a bit higher, that kills the process outright.
2.2 Bug two: the request that remembers the last one
A PHP worker handles many requests one after another. To do that it keeps some global state around for errors, sessions, objects and extensions. That state is supposed to be reset between requests.
In CVE-2026-109619 it is not always reset. A fatal error can leave pointers behind that still refer to memory from a request that has already finished. The next request handled by the same worker can then touch that memory before PHP has initialized things properly. That is a use-after-free: the program uses memory after it was handed back, and an attacker can arrange for something of their choosing to be sitting there.
The advisory (GHSA-6wmf-4rv5-p3rp) names three ways this has been shown. The strongest case uses fatal_error_backtraces, a PHP 8.5 feature, against mod_php. The attacker's first request leaves the stale state, and a second request that lands on the same worker uses it. The other two are the session upload progress feature and diagnostic messages raised before the request has started. Session upload progress is on by default, and the advisory says it was validated on PHP-FPM as well as mod_php, so FPM-only teams should not relax. Researchers have also shown that the bug can leak the memory of a PHP worker, which can hold passwords, tokens and API keys.
2.3 The third bug nobody has patched yet
Two days after the release, PHP published a third notice. It is easy to miss, and it is not fixed by anything above.
The good news is that it is the same setting that already helps with bug two, so one change covers both.
2.4 Who is affected, and what fixes it
Both PHP2Shell bugs are rated 9.2 on the CVSS v4.0 scale. The score stops short of 10 because PHP rates the attack as complex to pull off. On the older CVSS 3.1 scale, NVD scores CVE-2026-109618 at 9.8 and CVE-2026-109619 at 8.1, and Ubuntu rates the first at 9.8. Do not read much into the gaps. Both mean "fix now".
Fixed in: PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35.
Vulnerable: anything older on those branches. PHP 7.4, 8.0 and 8.1 are affected too and have no official release, although PHP has published minimal source backports for them. PHP 7.3 and older have no fix from PHP at all. The earliest affected code that researchers found goes back to PHP 5.4.
Which setups: anything that serves web requests. That covers Apache
mod_php, PHP-FPM,php-cgi, FrankenPHP and LiteSpeed. PHP considersmod_phpthe riskiest, because the demonstrated exploit runs before any of your application code does.CLI-only PHP (cron jobs, Composer, artisan commands) does not take web requests, so it is the lower-priority part of the job. Update it anyway.
PHP's position is that updating is the only complete fix for these two bugs. Configuration changes lower the risk but do not remove it. We cover them in Section 5 for servers you cannot patch today.
3. Why "We Already Updated" Is Not The End
3.1 You probably do not know where all the PHP is
Qualys looked at data from more than 10,000 of its customers and counted about 167 million PHP installations. Of those with a known version, roughly 73 percent were on internet-facing assets and about 71 percent ran an end-of-life version. Only about 199,000 had been patched within 12 hours of disclosure. These are vendor numbers and they count installations, not companies, so read them as a rough sketch: a lot of PHP, a lot of it old, and a lot of it forgotten.
3.2 The version number lies
On Debian, Ubuntu and Red Hat systems, the distribution does not move to PHP 8.3.36. It takes the fix and applies it to the version it already ships, then bumps its own package revision. That is called a backport.
Here is Ubuntu 24.04. The fix shipped on 21 October in package version 8.3.6-0ubuntu0.24.04.13. If you run php -v on that machine you will see something like PHP 8.3.6. A scanner that compares that against "fixed in 8.3.36" will flag a server that is actually patched. Some hosting stacks that ship their own PHP builds reportedly do the same, so ask your provider rather than reading the number.
It can fool you in the other direction as well. PHP's updated Windows builds, such as 8.5.12-1 and 8.4.27-1, were rebuilt for a Brotli fix and print the same version as the first 8.5.12 and 8.4.27 builds. The version string alone cannot tell them apart.
The only reliable source is what the vendor says about their own package. For Ubuntu, that is USN-8948-1:
Ubuntu 22.04 LTS php8.1 8.1.2-1ubuntu2.28
Ubuntu 24.04 LTS php8.3 8.3.6-0ubuntu0.24.04.13
Ubuntu 26.04 LTS php8.5 8.5.4-0ubuntu1.5
Ubuntu 26.10 php8.5 8.5.9-0ubuntu2.1Ubuntu has also published fixes for 20.04, 18.04, 16.04 and 14.04 through Ubuntu Pro. Red Hat covers the whole set in bulletin RHSB-2026-013. It rates the issue Important, says fixes are being released as they are ready, and notes that RHEL 7 is only affected by CVE-2026-109619. On RHEL, Rocky and Alma, check what your system has with dnf updateinfo list --cve CVE-2026-109618 rather than waiting for a version number to show up in a blog.
3.3 The patch is installed, the bug is still running
PHP-FPM and Apache load the PHP code into memory when they start and keep it there. You can install the fixed package, see "upgraded successfully", and still have every worker running the old, vulnerable code until the service is restarted. On many setups the package scripts restart the service for you. On many others they do not, either by policy or because someone disabled it years ago to avoid downtime. Do not assume.
Google Cloud and Mandiant add one more point: if a server shows signs of probing, capture the memory of the PHP workers before you restart. A restart wipes out evidence of what was in memory.
3.4 The branches nobody wanted to think about
PHP 8.1 and older are past end of life. PHP 8.2 receives security fixes only until 31 December 2026, and 8.3 is already in security-only mode. Fixes like 8.2.35 will stop coming for that branch soon. If your estate is on 7.4 or 8.0, this incident is the moment to plan a move, not just to find a patch.
3.5 One known side effect
PHP confirmed that 8.4.27 and 8.5.12 can crash PHP processes in some applications that use the SimpleXML extension (php-src issue 24488). This comes from a separate garbage collector change in the same releases, not from the security patches. A fix is merged in the source but, as of 26 October, had not shipped in a release. Our advice is to update anyway and test SimpleXML-heavy apps on staging first. Rolling back leaves you exposed to a bug that is being exploited right now.
3.6 While you are in there
Several smaller PHP security fixes landed in the months before this release. If a server has been on an older 8.5.x build, the update to 8.5.12 picks these up too. The version numbers below are for the 8.5 branch. Other branches fixed the same bugs in their own point releases, so check the changelog for yours.
8.5.9 (July): SQL injection in the PostgreSQL extension through a backslash escape in
E'...'strings, CVE-2026-17543.8.5.11 (September): an IPv6 access list bypass in PHP-FPM's
listen.allowed_clients, CVE-2026-91768. If you use that setting to restrict who can talk to FPM, it was weaker than you thought.8.5.11 (September): two OpenSSL extension bugs. One is a heap overflow when checking wildcard certificate names (CVE-2026-91767). The other is TLS hostname verification falling back to the common name after a mismatch on the SAN field (CVE-2026-91769).
26 October: updated PHP builds for Windows that carry a fix for a Brotli library flaw, CVE-2026-111497. It only matters if you use the cURL extension with automatic decompression turned on, which is off by default.
None of these is as loud as PHP2Shell. They are the kind of bugs that sit in a fleet for a year because nobody reads point-release notes.
4. Finding And Fixing It With Vajra
Everything above comes down to three questions. Where is PHP? Which of those copies are actually vulnerable? Did the fix really reach the running process? Vajra answers all three: it inventories PHP on every node, matches each copy against the CVEs with the Vajra Vulnexus engine, and patches affected hosts from the official repositories or from custom-built signed bundles. Here is how each step works.
4.1 Where PHP shows up in your inventory
The Vajra agent runs osquery on every node and reports installed software on a schedule. PHP lands in a different table depending on the operating system:
Debian and Ubuntu:
deb_packages. Look forphp8.3-cli,php8.3-fpm,php8.3-cgiandlibapache2-mod-php8.3(the number changes with the version).RHEL, Rocky, Alma, Fedora:
rpm_packages. Look forphp-cli,php-fpmandphp-cgi. The Apache module ships inside the plainphppackage. Third-party repositories use their own names, such asphp83-php-fpm(Remi) andea-php83-php-fpm(cPanel).Windows:
programs, the Add/Remove Programs list. PHP shows up here when it comes inside something that registers itself, such as XAMPP, WAMP or a hosting control panel.
The agent reports a new row on every check-in, so one package on one host appears many times. Vajra keeps only the newest row per host, package name and architecture, so you never see stale versions.
4.2 How Vulnexus decides what is vulnerable
Vajra Vulnexus is our CVE correlation engine. It keeps a local mirror of the NVD (the US national CVE database), a CPE dictionary built from it (CPE is the standard naming scheme for software products), and the distribution security data. It uses a different method per platform, because one method does not work everywhere:
Ubuntu and Debian: matched against the distribution's OVAL feeds. OVAL is a machine-readable format in which distributions publish which package versions are vulnerable and which are fixed.
RHEL family: matched mostly against Red Hat's OVAL and CSAF data (CSAF is Red Hat's newer advisory format), with NVD CPE matching as a fallback.
Windows programs: matched by CPE against the NVD.
For Linux packages, each match stores a patched_version: the version the distribution says contains the fix. This is what solves the backport problem from Section 3.2. A host is compared against the distribution's fixed version, not against PHP's upstream number, so on Ubuntu 24.04 anything below 8.3.6-0ubuntu0.24.04.13 is vulnerable and that build or newer is not.
Vulnexus also downloads the CISA KEV feed and refreshes it before scans. It writes the result into the cve_match field of every Windows program, Debian and Ubuntu package, and RPM package. Each CVE entry carries a kev object with whether it is in the catalog, the date added, the due date, the required action and whether it is known to be used in ransomware campaigns. The program or package gets a has_kev_cve flag when any of its CVEs is on the list. Since CISA put both PHP2Shell CVEs on the list, that is how you tell which affected hosts have a federal due date attached.
None of this needed a special rule for PHP2Shell. Once the feeds carry the CVE, the next correlation run attaches it to every host that has an affected package, on Linux and Windows alike.
4.3 What you see in the console
Vulnerabilities, then CVE list. Search for
CVE-2026-109618andCVE-2026-109619. Each row shows severity and how many hosts are affected. Opening a CVE shows the affected hosts with the package, the installed version and thepatched_version. The page also shows which of those hosts can take the fix from a signed bundle and lets you push it to them (more on that in Section 4.6). Updates straight from the official repositories are listed per host on the Available Updates page.Nodes, then a host, then the Programs tab. This lists every package on that host with its CVEs. Where a package's CVE is on the CISA KEV list, on Linux and Windows alike, it shows a CISA KEV marker with the date added, the due date and the required action.
4.4 The same data in your own dashboards
Everything the console shows is stored in ordinary Postgres tables, so you can also point Grafana or psql at it. Inventory sits in deb_packages, rpm_packages and programs. The CVE matches sit in vulnexus_package_cves, vulnexus_rpm_package_cves and vulnexus_host_vulnerabilities, and on Linux each match carries the patched_version to move to. A patch list is one join between the two, filtered to the two PHP2Shell CVEs:
Host and package:
web-01,php8.3-fpmInstalled: the version the host reports
Fixed in:
8.3.6-0ubuntu0.24.04.13CVE: CVE-2026-109618 and CVE-2026-109619, one row each
That is the shape of one result, with a made-up host name, not real output. Every row is a package still below the distribution's fixed version, and it drops out of the list by itself once the host reports the fixed build. The same join exposes the CISA KEV details Vulnexus attached, so one more column gives you the federal due date for each host.
4.5 Did the fix reach the running process?
A package on disk tells you nothing about whether it is running, or whether anyone can reach it. Threat Hunting runs live osquery queries on the host itself, so you can ask the machine directly. Here is the one that matters most after a patch, the "installed but not restarted" case from Section 3.3. A PHP-FPM worker with on_disk = 0 is running a file that no longer exists:
SELECT pid, name, path, datetime(start_time, 'unixepoch') AS started
FROM processes
WHERE (name LIKE 'php-fpm%' OR name IN ('php-cgi', 'frankenphp', 'lsphp'))
AND on_disk = 0;A healthy, restarted host returns no rows. Any row is a server that took the package but is still running old code. Restart the service.
Two more live queries are worth keeping next to it. For Apache with mod_php, the binary does not change but the PHP library inside it does, so look for a worker that still maps a deleted libphp file. And listening_ports joined to processes shows which web servers are listening, which separates a host serving the internet from one that only has PHP installed. That shows what is listening, not what is exposed. Only your firewall and network layout tell you that. Put the results next to the CVE list and you have a working order: mod_php servers that take outside traffic first, FPM second, internal-only third, CLI-only last.
4.6 Patching: official repositories or your own signed bundle
Vajra patches affected hosts two ways, and you can mix them across the fleet. Both are limited to Super Admins, applying a patch asks for an MFA code, and every job is written to the audit trail.
Official repositories. Vajra tells the agent to check for updates with the host's own package manager and then to upgrade. On apt, dnf and yum hosts you pick the exact PHP packages to upgrade, so a web server gets the fixed
php8.3-fpmand nothing else moves. The list of what is available per host is on the Available Updates page. This is the simplest route when hosts can reach their vendor repositories, and the packages come straight from your distribution with its own signatures.Custom-built signed bundles. For hosts that cannot reach a repository, or where you want to roll out one exact, tested set of packages, you build a bundle yourself. Copy the vendor's
.debor.rpmfiles into the staging folder and ask Vajra to build. The server reads each package, records its SHA-256 and metadata, signs the manifest with the bundle key and checks the signature again before it stores anything. A bundle with a bad signature is never stored. You can also build the bundle offline with the signing script and upload it. Vajra then lines up what each host has installed against what the bundle offers, and the CVE page shows which affected hosts can take the update, with a push-patch button for them.
A sensible order for the PHP2Shell rollout:
Open the CVE page for CVE-2026-109618 for the list of affected hosts. Rank them by exposure using the checks in Section 4.5.
Patch a small, low-risk group first. Watch the job in Patch Job History, where each host moves from queued to dispatched, then to success, failed or timed out.
Test your SimpleXML-heavy applications on that first group (see Section 3.5).
Roll out to the rest in waves. Internet-facing
mod_phphosts should not wait for the end.Restart PHP-FPM, Apache or whatever serves PHP on each host once the job reports success, unless you know the package restarts it for you.
For Windows, PHP that comes inside an installed product is patched by updating that product. Vajra's Windows software management pushes vendor installers with a SHA-256 check, and the agent confirms the result independently by reading the installed version back from the registry instead of trusting the installer's exit code.
That covers the two PHP2Shell bugs end to end: Vajra finds the affected hosts, patches them from official repositories or your own bundle, and gives you the check to prove the fix reached the running process. CVE-2026-115560 has no patch to push yet. For that one, the fix is the session.upload_progress.enabled=0 setting in Section 5.
4.7 PHP that did not come from a package
Most PHP on a managed server comes from the distribution, and Sections 4.1 to 4.6 cover it. A few other places are worth a deliberate look during an incident like this:
PHP built from source or unpacked from a zip. Plain PHP on Windows that was copied into a folder, or a
phpbinary compiled by hand on Linux, has no package record. Find these with a live query forphp.exe,php-cgi.exeandphp-fpmbinaries, and compare the running processes from Section 4.5 against your package inventory. A running PHP process with no matching package is your answer. Replace it with a packaged build, or rebuild it from a fixed source release.Containers. PHP inside a Docker image lives in the image, not in the host's package list. Rebuild those images from a patched base and redeploy them. Updating the host does nothing for the image.
End-of-life PHP. For 7.3 and older there is no fix from PHP. The answer is an upgrade plan, or a vendor that still supports that version.
You do not need to reboot for this fix. You need a service restart.
5. If You Cannot Patch Today
Sometimes a patch has to wait for a change window. These settings reduce the risk until it happens. They do not remove it, and some of them need care, so read the notes under the block first. Apply them, then restart every worker.
; Settings to apply now. The first group is only for servers you cannot patch yet.
; CVE-2026-109618, interim only
; Use this ONLY if memory is capped outside PHP (see notes below).
; Set it where applications cannot override it: php_admin_value in an FPM pool
; or Apache config, not ini_set or .htaccess.
memory_limit = -1
; CVE-2026-109618, interim: shrink what a request can ask for
; Pick sizes your application can live with. Many apps need more than this.
post_max_size = 8M
upload_max_filesize = 2M
max_input_nesting_level = 16
; CVE-2026-109619 and CVE-2026-115560: keep this after patching too
session.upload_progress.enabled = 0
; CVE-2026-109619, PHP 8.5 only, interim
fatal_error_backtraces = 0Notes on those settings:
memory_limit = -1looks backwards, so here is the reasoning. The first bug lives in the code that runs when PHP hits its own memory limit, and removing the limit takes away the predictable trigger the exploit relies on. It is not a complete fix, because a failed system memory allocation can still reach the same path. Only use it where something outside PHP caps memory, such as a container or cgroup limit. Without one, a runaway request can use all the memory on the machine. Also check that no pool, virtual host or.htaccessfile puts a finite limit back. And note that killing a container can take the master process and every worker with it, unless each worker has its own cgroup.post_max_sizeis not a reliable guard on its own. It can be bypassed when the request has no usable declared length, and many small headers or empty file fields can use far more memory than their size on the wire. Also enforce limits on the decoded request size, the number of parts, headers per part and nesting depth at your web server or proxy. If your application does not take multipart uploads at all, block them there.The sizes above will break real upload features. Red Hat suggests 8M, or 32M where uploads are needed. Check what your application accepts before you copy them.
After you patch, review
memory_limitandpost_max_sizeagain. The fix adds its own hard limit that ends the process when it is reached.For FPM, setting
pm.max_requests = 1limits how far stale state can travel between requests, at a cost in speed. AvoidRLIMIT_ASandRLIMIT_DATAas a substitute memory limit. They can trigger the vulnerable path themselves.
Cloudflare and other WAF vendors have rules for the known request shapes. Take them as a stopgap. They only cover traffic that goes through that provider, and attackers were already changing payloads to get around them.
6. What To Do Today
Where you stand depends on how you run PHP.
If a hosting provider runs your PHP
Ask them what version, or what patched build, you are on, and whether they have restarted their workers. Do not accept "we are working on it" for more than a day or two.
Change your own passwords and API keys after they confirm the fix, as a cheap precaution.
If you run your own servers
Update to the fixed package from your vendor, or to 8.5.12, 8.4.27, 8.3.36 or 8.2.35 if you build from source.
Restart every PHP worker and Apache. Installing the package is not enough.
Use the process check in Section 4.5 to confirm nothing is still running old code.
Rebuild container images from a patched base.
Set
session.upload_progress.enabled = 0as well (see the third bug in Section 2.3), and leave it that way after patching unless you really use upload progress.If you cannot patch today, apply the settings in Section 5.
For security teams
Treat any internet-facing PHP server that was unpatched at any point since 17 October, and certainly since 21 October, as possibly compromised until proven otherwise. The earliest reported attempt came before the fix existed.
Look in web and PHP logs for unusual multipart requests followed by memory limit errors or repeated HTTP 500 responses, oversized
PHPSESSIDcookies, and PHP workers that restarted without anyone asking them to. PHP's advisory notes that a clean log does not prove a server is safe.Since memory disclosure has been demonstrated, rotate database passwords, API keys and signing secrets on any exposed server that was unpatched for any length of time.
Before restarting a server that looks suspicious, capture worker memory and the relevant logs. Then restart.
Use a live query in Threat Hunting to look for web server and PHP processes that spawn shells (
sh,bash,curl,wget,nc), which is what a successful exploit usually leads to. It sees what is running right now, so on exposed hosts run it on a schedule to catch short-lived shells too.Set an alert on the same pattern if you do not already have one. A PHP worker starting
shorcurlis rare on a healthy server, and noisy for the few apps that do it on purpose, so tune it per host group.
7. Closing Thoughts
The bugs themselves are the kind of thing PHP has had before: memory handling that goes wrong on an unusual path. What stands out is the clock. Exploitation attempts were reported within hours of the release, and attackers had code execution on a researcher's honeypot within three days.
That changes what a good patch process looks like. A vulnerability scan that runs weekly and produces a spreadsheet is not fast enough for a bug like this. The whole job is to know where PHP runs before the advisory lands, trust the vendor's fixed version over the upstream number, and check the running process, not just the package. The third bug is a reminder to read the follow-up notices too, because the first patch is rarely the last word.
"We updated the app" and "we updated the platform under the app" are two separate jobs. If your team owns the first and a hosting provider, a control panel or an old image owns the second, make sure someone is looking at the second.
If you only do three things today: find every PHP that serves web requests, update it to a vendor-fixed build, and restart the workers.



