PHP2Shell: finding every copy of PHP on Windows and Linux before you patch it
Updated October 26, evening: CISA added both CVEs to its Known Exploited Vulnerabilities catalog.
Updated October 26: PHP2Shell is now exploited in the wild. Added what’s been seen so far and how to look for a web shell that was dropped before you patched.
Updated October 23, evening: PHP announced CVE-2026-115560, another session upload-progress bug that also affects the fixed versions, with no patch until the next regular release (see below). Also corrected: a .user.ini file does turn upload progress off under PHP-FPM; the post said it didn’t.
Updated October 23: PHP did publish source patches for 7.4, 8.0 and 8.1 (no new releases). The post said 8.1 and older got no fix at all; that was wrong.
Updated October 21: fixed packages from Ubuntu, Debian and Red Hat, Red Hat’s take on PHP-FPM, and a section on PHP-FPM and mod_php, which keep running the old PHP until they restart.
On October 21 the PHP project shipped an out-of-band release for two bugs in PHP itself, CVE-2026-109618 and CVE-2026-109619. Both are rated critical. Wiz Research reported them, and the IDs leaked a day early.
Either one lets an attacker run code on a server running PHP by sending it an ordinary HTTP request. No login, no special configuration, no particular app needed. The code runs as whatever account the web server runs PHP as. Wiz says every PHP version back to 5.4 is affected by at least one of the two.
The fixed versions are 8.5.12, 8.4.27, 8.3.36 and 8.2.35. PHP 8.1 and older get no new release. They’re past end of life. Because the bugs are so serious, the PHP security team did publish minimal source patches for 7.4, 8.0 and 8.1, for people who build PHP themselves, and some distributions and long-term-support vendors ship their own backported fixes. PHP 7.3 and older get nothing at all. A build from those patches still reports an old version number (8.1.34 and 8.0.30, the same as the last releases, and 7.4.31-dev), so no version check can tell it from an unpatched one. Keep a note of which ones you patched.
It’s being exploited now
Cloudflare saw early attacks on release day, hours before the fix shipped. Amazon has since said its honeypots caught a PHP2Shell attempt on October 17, four days before disclosure, then attempts from several different attackers within hours of it. watchTowr has seen exploitation attempts against both bugs in its own honeypots.
Most of those early attempts didn’t get code running. On October 24 one did. Perplexity’s detection lead Adel Karimi recorded a working attack against one of his honeypots, Apache with mod_php on PHP 8.4.26, and Marcus Hutchins at Expel verified it. He’s careful to say that honeypot was set up to be easy (a single Apache worker), so it doesn’t prove how reliable the exploit is anywhere else. Later on October 26, CISA added both CVEs to its Known Exploited Vulnerabilities catalog, which means US federal agencies now have to prioritize patching them.
If a server was unpatched and reachable from the internet at any point since October 17, patch it and then check it for anything that was dropped (see If you think something already got hit).
There’s no config-file fix
PHP’s own developers were blunt about it: patching is the only fix, and “ini settings won’t save you.” PHP confirmed code execution through the session upload-progress feature on both Apache with mod_php and PHP-FPM, and that feature is on by default.
For PHP versions that will never get a patch, turning upload progress off is a stopgap. It closes the confirmed path, not every variant, so it’s something to do while you upgrade or retire the server, not instead of it. It only works where PHP reads it before the request is parsed: php.ini, or the PHP-FPM pool config:
php_admin_value[session.upload_progress.enabled] = 0
Then restart PHP-FPM (or Apache, for mod_php). ini_set() in code can’t change this setting at all. A .user.ini file in the web root did turn it off under PHP-FPM when I tested it, but anything that can write to the web root can turn it back on, so put it in the pool or server config instead.
Patching is the easy part. Finding every copy of PHP is the hard part.
October 23: CVE-2026-115560, the same feature again, and no patch yet
Two days after the PHP2Shell releases, PHP announced a third code-execution bug in session upload progress, CVE-2026-115560. The fixed versions above are affected too. PHP isn’t shipping an emergency release for it: the fix comes in the next regular release, with a full advisory then.
It only applies to an unusual setup. All three of these have to be true:
session.serialize_handleris set tophp_binaryorphp_serializein the configuration,- the application switches it back to
phpwithini_set()before callingsession_start(), - and session upload progress is still on (it’s on by default).
No exploitation has been reported, and PHP calls it unlikely in practice. If you know your apps never change the session serializer at runtime, PHP says you can leave upload progress on. If you don’t know, the mitigation is the same line as above, and now it applies to patched PHP as well, until the next release:
php_admin_value[session.upload_progress.enabled] = 0
That’s for a PHP-FPM pool. For Apache with mod_php, in the Apache configuration:
php_admin_flag session.upload_progress.enabled off
Then restart PHP-FPM or Apache. I tested both on Ubuntu 26.04: a page under each saw the setting off afterwards. Application code can’t switch it back on, because ini_set() can’t change this setting at all (it returned false even with nothing configured). The php_admin_ form also stops a .htaccess file from turning it back on.
Why your scanner probably missed some
On Windows, PHP is almost never “installed”. It’s a folder that something else brought with it: XAMPP or WAMP on a developer’s box, a C:\php folder an admin unzipped for IIS years ago, or a copy buried inside a vendor product with a web console. None of those show up in Add/Remove Programs, so anything that inventories software that way never sees them.
On Linux, the version number lies in the other direction. Debian, Ubuntu and RHEL patch PHP without changing its version number. A fully patched Ubuntu server can still say 8.5.4, which is lower than 8.5.12, so a version check flags it forever. The reliable answer is the package’s own changelog: once your distribution ships the fix, the changelog lists the two CVE numbers.
Check one Windows machine
Run this in an elevated PowerShell window. It searches the whole C: drive, so give it a few minutes on a big server:
Get-ChildItem C:\ -Recurse -Include php.exe, php-cgi.exe -ErrorAction SilentlyContinue |
Select-Object FullName, @{ n = 'Version'; e = { $_.VersionInfo.ProductVersion } } | Format-Table -AutoSize
Anything below the fixed version for its branch needs updating, and anything at 8.1 or older needs replacing (the source patches for those come with no Windows builds). Check other drives the same way (D:\ and so on).
To see which PHP IIS is actually running:
& "$env:windir\system32\inetsrv\appcmd.exe" list config /section:system.webServer/fastCgi
Each fullPath in that output is a PHP that IIS runs, which makes it the one an attacker can reach.
Check one Linux machine
For distribution packages, ask the changelog. On Debian and Ubuntu:
zgrep -lE 'CVE-2026-109618|CVE-2026-109619' /usr/share/doc/php*-common/changelog.Debian.gz
On RHEL, Rocky, Alma and Fedora:
rpm -q --changelog php-common | grep -E 'CVE-2026-109618|CVE-2026-109619'
No output means your installed package doesn’t have the fix yet. Where things stood on the afternoon of October 21 (Pacific):
- Ubuntu: fixed. On 26.04 that’s
8.5.4-0ubuntu1.5, still “8.5.4”, and its changelog lists both CVEs. - Debian: fixed in bookworm (
8.2.35-1~deb12u1, DLA-4848-1) and trixie (8.4.27-1~deb13u1, DSA-6574-1). Forky and sid weren’t fixed yet. - Red Hat: fixing stream by stream. RHEL 9’s
php:8.2module gotphp-8.2.35-1.module+el9.8.0+...(RHSA-2026:87072) and RHEL 8’sphp:7.4module gotphp-7.4.33-8.module+el8.10.0+...(RHSA-2026:87080), with more in progress. Red Hat rates both CVEs Important rather than Critical, because the first one needs a precise out-of-memory condition to trigger. - AlmaLinux: fixed packages for every stream on 8, 9 and 10, in its testing repository.
- Rocky: a Rocky 9.8 lab machine still had nothing new.
- RHEL 8 trap: a plain
dnf install phpthere still gives you PHP 7.2. Upstream PHP won’t fix 7.2; only your distribution’s package can.
Install your distribution’s update as soon as it ships, then run the check again.
PHP you built yourself, or got from a third-party repository that tracks PHP’s own version numbers, you can check by version:
php -v
Containers carry their own PHP, so check each running one too:
docker ps -q | xargs -r -I{} docker exec {} php -v
For containers, updating means rebuilding the image with docker build --pull and recreating the container. Restarting the old one just runs the old PHP again.
PHP-FPM and mod_php: patched isn’t fixed until it restarts
Red Hat considers PHP-FPM less exposed than mod_php, because FPM workers exit on an early initialization failure before they reach the corrupted state. PHP itself still confirmed code execution through FPM when the application gives an attacker a way to make an operation fail, so patch both.
PHP-FPM workers and Apache with mod_php keep running the PHP they started with. Upgrading the package replaces the files on disk; the running processes don’t notice until they’re restarted.
On Debian and Ubuntu, the packages do that for you. On a lab machine I reinstalled Ubuntu’s fixed PHP-FPM package under a running PHP-FPM, and separately the mod_php package under a running Apache. Both packages restarted their service themselves, and nothing was left running the old files. If you built PHP yourself, run several PHP-FPM versions side by side, or your packages don’t restart services, restart each one (and Apache fully, not a graceful reload):
sudo systemctl restart php8.3-fpm
This lists any process still running a PHP binary or library that has since been replaced. No output means everything running has loaded what’s on disk now:
sudo grep -lE '/[^ ]*(php|libphp)[^ ]* \(deleted\)$' /proc/[0-9]*/maps
Each line it prints is /proc/<pid>/maps, and that PID needs a restart. Some guides use a broader version of this check that matches any deleted file under /usr. On a lab machine that flagged GNOME, fwupd and half a dozen other unrelated processes, which is why this one only looks for PHP.
If you think something already got hit
Huntress’s list of what to look for is the right starting point: shells (sh, bash, cmd.exe, powershell.exe) or download tools (curl, wget, certutil.exe) started by the PHP worker process, new files in the web root, new cron jobs or scheduled tasks, and new local accounts. Patching closes the hole, but it doesn’t remove anything that was dropped through it.
The usual thing to drop is a web shell, a small PHP file in the web root that runs whatever command arrives in the request. It’s often hidden as an image (avatar.php.jpg, or a real GIF with PHP code after the header) to get past upload filters. Look for PHP files changed since October 17 that take a command from the request, and image files with PHP code inside.
On Linux:
sudo find /var/www /srv -xdev -type f -newerct '2026-10-17' \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' \) -print0 |
sudo xargs -0 -r grep -liE '(eval|assert|system|passthru|shell_exec|popen|proc_open|exec)[[:space:]]*\([[:space:]]*@?(base64_decode|\$_(POST|GET|REQUEST|COOKIE))'
sudo find /var/www /srv -xdev -type f -newerct '2026-10-17' \( -iname '*.jpg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.ico' \) -print0 |
sudo xargs -0 -r grep -la '<?php'
-newerct goes by the file’s change time, not its modified time. touch can set the modified time back to make a file look old, but it can’t set the change time back, so a back-dated shell still shows up. In my lab a shell I’d back-dated to 2025 was still found this way.
On Windows, point it at your IIS site folders and any XAMPP or WAMP web root:
$since = Get-Date '2026-10-17'
$files = Get-ChildItem C:\inetpub\wwwroot, C:\xampp\htdocs -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -ge $since -or $_.CreationTime -ge $since }
$files | Where-Object Extension -in '.php', '.phtml', '.phar' |
Select-String '(eval|assert|system|passthru|shell_exec|popen|proc_open|exec)\s*\(\s*@?(base64_decode|\$_(POST|GET|REQUEST|COOKIE))' -List | Select-Object Path
$files | Where-Object Extension -in '.jpg', '.png', '.gif', '.ico' | Select-String '<?php' -SimpleMatch -List | Select-Object Path
Windows has no change time an attacker can’t touch, so a shell with both of its dates set back slips past this one. In my lab it did.
Anything these print is a lead, not proof. A plugin update since October 17 changes plenty of PHP files, and some legitimate code does pass request data to exec(). Open each file and read it. If you want to plant a fake shell to test the commands first, do it on the server, not your own PC: Defender quarantined mine as Backdoor:PHP/Chopper the moment I saved it.
Doing this across a fleet
The RFF Script Library has PHP2Shell (CVE-2026-109618/109619) - PHP Exposure Check for Windows and for Linux. On Windows it finds every PHP folder on every fixed drive, reads each copy’s version from the file, and tells you which ones IIS runs and which are running right now. On Linux it judges distribution packages by their changelog and everything else by version, and checks running Docker and Podman containers. On any machine with PHP, both also run the web-shell hunt above, plus a check for a shell started by PHP or the web server right now and (on Linux) a crontab for the web server account. Both are read-only.
RFF is free for your first 100 endpoints: getrff.com.
Sources
- PHP: php.net security announcement (fixed releases, the 7.4/8.0/8.1 source backports, and the October 23 notice for CVE-2026-115560)
- CERT-EU: Critical Vulnerabilities in PHP (CVE-2026-115560 mitigation)
- CISA: CISA Adds Two Known Exploited Vulnerabilities to Catalog (October 26)
- Amazon: Amazon Threat Intelligence identifies PHP2Shell exploitation prior to public disclosure in MadPot
- watchTowr: PHP2Shell RCE exploited
- BleepingComputer: PHP2Shell security flaws now successfully exploited in the wild
- Datadog Security Labs: PHP2Shell detection, analysis and remediation
- Huntress: Rapid Response: php2shell, leaked critical PHP vulnerabilities
- Tenable: php2shell (CVE-2026-109618, CVE-2026-109619) FAQ
- Help Net Security: PHP2Shell: critical PHP vulnerabilities patched, update now
- Sansec: PHP2Shell
- AlmaLinux: php2shell: patched PHP packages available in testing
- Mattias Geniar: PHP CVE-2026-109618 and CVE-2026-109619: how to apply the patches
- Red Hat: RHSB-2026-013, CVE-2026-109619, RHSA-2026:87072, RHSA-2026:87080
- Debian security tracker: CVE-2026-109618
Comments
Comments use GitHub Discussions, so you'll need a GitHub account to post. Corrections are especially welcome.