
Repository for PHP2Shell exposure hunting and related analysis
Defender XDR exposure hunting for PHP2Shell (CVE-2026-109618 / CVE-2026-109619).
v3.3 · 22 Oct 2026
Purpose: Find onboarded devices actively running PHP that could be exposed to PHP2Shell.
Scope: Defender XDR Advanced Hunting · onboarded Windows and Linux endpoints · 30-day lookback by default · active PHP only. Not covered: dormant installs and unmanaged devices. Not runnable in Sentinel, because the installed-software (TVM) table is only queryable in Defender XDR.
| Hunt | Windows | Linux | Question |
|---|---|---|---|
| 1 | windows/01-any-php.kql | linux/01-any-php.kql | Any PHP? Yes/no per device. |
| 2 | windows/02-evidence-breakout.kql | linux/02-evidence-breakout.kql | Evidence breakout with versions. |
| 3 | windows/03-safety-net.kql | linux/03-safety-net.kql | Safety net: devices serving on web ports, the listening process, and PHP status. |
All event checks use one lookback window, let Lookback = 30d; at the top of each hunt. IP addresses come from the latest report per adapter in the last 7 days. Installed software is the current TVM snapshot. Only onboarded endpoints are checked (OnboardingStatus == "Onboarded", DeviceCategory == "Endpoint").
A device counts as PHP seen if any one of these matches.
How names are matched:
^…$), so it must match the whole file name, never part of one.startswith "php", plus frankenphp on both OSes and lsphp on Linux. The regex then runs only on what's left.php9.dll) match automatically.patterns.test.js runs each regex against these examples. If you change a regex, update the examples there.Windows
| Check | Table | Matches | Why it matters |
|---|---|---|---|
| PHP executables started | DeviceProcessEvents | Regex ^(php(-cgi|-win)?|frankenphp)\.exe$, which allows exactly four names: php.exe (CLI), php-cgi.exe (FastCGI worker), php-win.exe (CLI, no console), frankenphp.exe (PHP embedded in a web server). phpstorm64.exe and other php* tools don't match | Proves PHP ran. Hunt 2 shows the version from the binary's version information (ProcessVersionInfoProductVersion) |
| PHP engine DLLs loaded | DeviceImageLoadEvents | Regex ^php\d*(ts|isapi|apache[\w_]*)?\.dll$: php, any version number, an optional ts / isapi / apache… suffix, then .dll. Examples are in the DLL table below. Extension DLLs (php_curl.dll and other php_*.dll) don't match, because the underscore isn't allowed straight after php | The only way to see PHP running inside another process, such as IIS or Apache, where no php*.exe starts. Hunt 2 shows the DLL and the process that loaded it |
| Installed software | DeviceTvmSoftwareInventory | Not a regex: SoftwareName has "php" (php as a whole word in the name) or SoftwareVendor =~ "php" (exact, case-insensitive) | Catches registered installs. Zip installs (the usual Windows method) normally don't register |
Examples of PHP engine DLLs the Windows pattern matches
| DLL | What it is | Typical host process (shown in hunt 2) |
|---|---|---|
php8.dll, php7.dll | Engine, non-thread-safe build | php-cgi.exe (FastCGI under IIS, nginx or Apache mod_fcgid), php.exe |
php8ts.dll, php7ts.dll, php5ts.dll | Engine, thread-safe build | httpd.exe (Apache module), php.exe |
php8apache2_4.dll, php7apache2_4.dll, php5apache2_2.dll | Apache module loader | httpd.exe |
php5isapi.dll | Legacy IIS ISAPI module (PHP 5.2 and earlier, unsupported) | w3wp.exe |
The table lists common names, not an allow-list: any name fitting the pattern matches. The DLL name gives the major version only. The exact version comes from a php-cgi.exe or php.exe process row, or from installed software, on the same device.
Linux
| Check | Table | Matches | Why it matters |
|---|---|---|---|
| PHP binaries started | DeviceProcessEvents | Regex ^(php[\d.]*|php-(fpm|cgi)[\d.]*|lsphp[\d.]*|frankenphp)$: php, php-fpm, php-cgi or lsphp (LiteSpeed), each with an optional version made of digits and dots, plus frankenphp. Examples: php, php8.3, php83, php-fpm8.3, php-cgi8.2, lsphp83. Tools such as phpunit, phpize and php-config don't match, because only digits and dots may follow the base name | Proves PHP ran. Hunt 2 shows a major.minor hint, extracted from the name, path or command line by a second regex, php-?(?:fpm|cgi)?/?(\d+\.\d+) (for example php-fpm8.3 or /etc/php/8.3/ gives 8.3.x) |
| Installed packages | DeviceTvmSoftwareInventory | Not a regex: SoftwareName contains "php" (substring, so versioned names such as php8.3-fpm and libapache2-mod-php8.3 match) or SoftwareVendor =~ "php" | The only check for Apache mod_php and for php-fpm running since before the window: Linux has no library-load check here, and forked php-fpm workers don't create process starts |
| Check | Table | Matches | Why it matters |
|---|---|---|---|
| Active web serving | DeviceNetworkEvents | InboundConnectionAccepted on a fixed list of ports: 80, 443, 8080, 8443. Loopback is excluded (RemoteIPType != "Loopback"). Shows distinct and public sources, plus the accepting process (via <process>) | Proves a device is serving, even if its web server started before the lookback window. ntoskrnl.exe = HTTP.sys (IIS, ADFS, RD Gateway, Windows Admin Center, SSTP) |
| Web server started | DeviceProcessEvents | Not a regex: a fixed list of exact names, case-insensitive (in~). Windows: w3wp.exe, httpd.exe, nginx.exe, caddy.exe, lighttpd.exe, frankenphp.exe. Linux: httpd, apache2, nginx, lshttpd, litespeed, openlitespeed, caddy, lighttpd, frankenphp | A web server that started in the window and loaded no PHP gives "Not seen" with high confidence. Windows also shows IIS app pool names, extracted from the w3wp.exe command line by the regex -ap\s+"?([^"\s]+) |
PHP column values in hunt 3
| Value | Meaning |
|---|---|
PHP seen (see hunt 2) | At least one PHP detection check matched. |
Not seen | No PHP evidence, and either a web server started in the window (high confidence) or the device isn't serving web. |
Not seen - no web server start in <N>d (N = Lookback) | Serving on web ports, but no web-server process start to inspect. This is the residual blind spot: check the via process on the host. |