In our previous breakdown of the PHP2Shell exploitation wave, we covered how CISA added CVE-2026-109618 (Out-of-Memory Corruption) and CVE-2026-109619 (Stale Request State) to the Known Exploited Vulnerabilities (KEV) catalog with a 72-hour remediation deadline of October 29, 2026.
For security engineering and SOC teams operating under CISA Binding Operational Directive (BOD) 26-04, the single most urgent operational question is public internet exposure:
If NOT Publicly Exposed:
You have a 14-day remediation window.
If Publicly Exposed:
You have a 3-day remediation window AND a mandatory Forensic Triage requirement—meaning you must verify whether the host was compromised and capture volatile artifacts before patching or restarting the web service.
When you manage thousands of endpoints, waiting on perimeter port scans or spreadsheet attestations takes too long. If Microsoft Defender for Endpoint (MDE) is deployed across your fleet, you already have the telemetry required to separate internal dev boxes from internet-facing targets.
Use the following Microsoft Defender XDR Advanced Hunting (KQL) playbook to locate vulnerable PHP endpoints, prove public internet exposure via live network telemetry, uncover "Shadow PHP" binaries on perimeter hosts, and perform pre-patch forensic triage.
Execution Note: Run these queries directly in Microsoft Defender XDR Advanced Hunting (security.microsoft.com-> Investigation & response -> Hunting -> Advanced hunting). TheDeviceTvm*vulnerability management tables live natively in Defender XDR and are not mirrored by default into standalone Microsoft Sentinel workspaces.
Zeroing In on TVM-Flagged Internet-Facing Endpoints
Your first stop is correlating Microsoft Defender Vulnerability Management (MDVM) detections (DeviceTvmSoftwareVulnerabilities) with Defender's device exposure graph (DeviceInfo). This query brings devices flagged with IsInternetFacing == true straight to the top of your queue so you know exactly which systems fall under the 3-day BOD 26-04 mandate:
Query 1: Direct CVE Match Enriched with Public Exposure Status
DeviceTvmSoftwareVulnerabilities
| where CveId in ("CVE-2026-109618", "CVE-2026-109619", "CVE-2026-111497")
| join kind=leftouter (
DeviceInfo
| summarize arg_max(Timestamp, ExposureLevel, IsInternetFacing, PublicIP, OSPlatform, MachineGroup) by DeviceId
) on DeviceId
| summarize
CVEs = make_set(CveId),
VulnerableSoftware = make_set(strcat(SoftwareVendor, " ", SoftwareName, " ", SoftwareVersion)),
RecommendedUpdate = make_set(RecommendedSecurityUpdate)
by DeviceId, DeviceName, OSPlatform, IsInternetFacing, PublicIP, ExposureLevel, MachineGroup
| extend BOD_26_04_Timeline = iff(IsInternetFacing == true, "3 Days + Mandatory Forensic Triage", "14 Days (Internal)")
| order by IsInternetFacing desc, ExposureLevel desc, DeviceName asc
Query 2: Software Inventory & EOL Audit Scoped to Internet-Facing Assets
let InternetFacingDevices = DeviceInfo
| summarize arg_max(Timestamp, IsInternetFacing, PublicIP, ExposureLevel) by DeviceId
| where IsInternetFacing == true;
DeviceTvmSoftwareInventory
| where SoftwareName has "php" or SoftwareVendor has "php"
| join kind=inner InternetFacingDevices on DeviceId
| project DeviceName, OSPlatform, PublicIP, ExposureLevel, SoftwareVendor, SoftwareName, SoftwareVersion, EndOfSupportStatus, EndOfSupportDate
| order by SoftwareVersion asc
Ground-Truth Network Verification: Proving Public Web Exposure
Defender's IsInternetFacing flag is great for rapid triage, but what if a server sits behind a reverse proxy, load balancer, or NAT rule that obscures its DeviceInfo classification? Or what if you need hard evidence for your SOC ticket showing whether a PHP service actually accepted inbound connections from non-RFC1918 public IP addresses?
This query inspects DeviceNetworkEvents over the last 14 days to find web servers and PHP worker processes (php-fpm, php-cgi.exe, w3wp.exe, httpd, apache2, nginx, lsphp, frankenphp) that have actively accepted inbound traffic from external public IPs:
Query 3: Verify Actual Inbound Public Traffic to Web/PHP Processes
// Query 3: Verify Actual Inbound Public Traffic to Web/PHP Processes
DeviceNetworkEvents
| where Timestamp > ago(14d)
| where ActionType == "InboundConnectionAccepted"
| where InitiatingProcessFileName in~ (
"php-fpm", "php-cgi.exe", "php-cgi", "php.exe",
"w3wp.exe", "httpd.exe", "httpd", "apache2",
"nginx.exe", "nginx", "lsphp", "frankenphp"
)
| where not(ipv4_is_private(RemoteIP)) and RemoteIP !in ("127.0.0.1", "::1")
| summarize
InboundHitCount = count(),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp),
ListeningPorts = make_set(LocalPort),
DistinctPublicSources = dcount(RemoteIP),
SamplePublicIPs = make_set(RemoteIP, 20)
by DeviceId, DeviceName, InitiatingProcessFileName
| order by InboundHitCount desc
Catching "Shadow PHP" on Publicly Exposed Endpoints
One of the biggest traps with PHP2Shell is unregistered PHP: portable Windows folders (C:\php\php-cgi.exe), XAMPP/WAMP stacks, or third-party enterprise applications that bundle their own PHP runtime without registering in Add/Remove Programs, rpm, or dpkg.
By combining DeviceProcessEvents with our inbound public network telemetry, we can uncover every unmanaged PHP binary that has executed on an internet-facing endpoint over the last 30 days—and extract its exact PE product version directly from the binary metadata:
Query 4: Unmanaged/Portable PHP Runtimes Executing on Internet-Facing Hosts
// Query 4: Unmanaged/Portable PHP Runtimes Executing on Internet-Facing Hosts
let PublicWebHosts = DeviceNetworkEvents
| where Timestamp > ago(14d)
| where ActionType == "InboundConnectionAccepted"
| where not(ipv4_is_private(RemoteIP)) and RemoteIP !in ("127.0.0.1", "::1")
| distinct DeviceId;
DeviceProcessEvents
| where Timestamp > ago(30d)
| where FileName in~ ("php.exe", "php-cgi.exe", "php-win.exe", "php", "php-fpm", "lsphp", "frankenphp")
or ProcessVersionInfoOriginalFileName in~ ("php.exe", "php-cgi.exe")
or ProcessVersionInfoProductName has "PHP"
| extend IsReceivingPublicTraffic = iff(DeviceId in (PublicWebHosts), true, false)
| summarize
LastSeen = max(Timestamp),
ExecutionCount = count(),
FolderPaths = make_set(FolderPath),
EmbeddedBinaryVersions = make_set(ProcessVersionInfoProductVersion),
SampleCommandLines = make_set(ProcessCommandLine, 5)
by DeviceId, DeviceName, IsReceivingPublicTraffic, AccountName
| order by IsReceivingPublicTraffic desc, LastSeen desc
BOD 26-04 Forensic Triage Queries (Run Before You Patch!)
Remember the cardinal rule of CISA BOD 26-04: if an internet-facing PHP asset is discovered, do not immediately patch or restart the web service until you verify whether it has been exploited. Furthermore, threat intelligence from Amazon and Tenable confirms that attackers began probing for PHP2Shell on October 17, 2026—four days before public disclosure.
Run these three retroactive hunting queries back to October 17 to check for compromise on your perimeter hosts before touching their volatile memory state:
Hunt A: Web Server or PHP Worker Spawning a Command Shell (RCE Signal)
When the 2-stage PHP2Shell exploit succeeds, the compromised mod_php or php-fpm worker typically spawns a shell (sh, bash, cmd.exe, powershell.exe) or a transfer utility (curl, wget, certutil.exe) to pull down stage-two tooling:
DeviceProcessEvents
| where Timestamp >= datetime(2026-10-17T00:00:00Z)
| where InitiatingProcessFileName in~ (
"php-fpm", "php-cgi.exe", "php-cgi", "php.exe",
"w3wp.exe", "httpd", "httpd.exe", "apache2", "lsphp", "frankenphp"
)
| where FileName in~ (
"sh", "bash", "dash", "zsh", "cmd.exe", "powershell.exe", "pwsh.exe",
"curl", "curl.exe", "wget", "whoami", "whoami.exe", "id",
"net.exe", "net1.exe", "nltest.exe", "certutil.exe", "nc", "ncat", "python", "python3"
)
| project Timestamp, DeviceName, AccountName, InitiatingProcessFileName, InitiatingProcessCommandLine, FileName, ProcessCommandLine, FolderPath
| order by Timestamp desc
Hunt B: Webshell Drops & Ephemeral Staging Directory Writes
Checks if a web worker wrote a new .php, .phtml, or .phar webshell into your web root or dropped an ELF/PE payload into /tmp, /var/tmp, or /dev/shm:
DeviceFileEvents
| where Timestamp >= datetime(2026-10-17T00:00:00Z)
| where ActionType in ("FileCreated", "FileModified", "FileRenamed")
| where InitiatingProcessFileName in~ (
"php-fpm", "php-cgi.exe", "php.exe", "w3wp.exe",
"httpd", "httpd.exe", "apache2", "lsphp", "frankenphp"
)
| where FileName endswith ".php"
or FileName endswith ".phtml"
or FileName endswith ".phar"
or FileName endswith ".inc"
or FileName endswith ".aspx"
or FolderPath has_any ("/tmp/", "/var/tmp/", "/dev/shm/", "\\Temp\\")
| project Timestamp, DeviceName, ActionType, FolderPath, FileName, SHA256, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc
Hunt C: Retroactive Sweep for Known PHP2Shell Exploitation & Scanning IPs
Sweeps your perimeter connection logs back to October 17 for contact with early PHP2Shell scanning and exploitation infrastructure:
let KnownPHP2ShellIPs = dynamic([
"103.216.220.92", "31.223.184.166", "37.19.200.5",
"134.209.159.64", "103.114.163.188", "95.217.98.37",
"85.11.167.243", "45.198.224.169", "109.71.252.97"
]);
DeviceNetworkEvents
| where Timestamp >= datetime(2026-10-17T00:00:00Z)
| where RemoteIP in (KnownPHP2ShellIPs)
| project Timestamp, DeviceName, ActionType, LocalIP, LocalPort, RemoteIP, RemotePort, InitiatingProcessFileName
| order by Timestamp desc
What If You Get a Hit on an Internet-Facing Host?
If any of your perimeter PHP servers trigger Hunt A, Hunt B, or show anomalous worker crashes alongside Hunt C:
- Isolate Without Rebooting: Use Microsoft Defender for Endpoint Device Isolation to sever external attacker C2 and public HTTP traffic while keeping the MDE telemetry channel open.
- Collect Volatile Artifacts via MDE Live Response: Before applying the PHP patch or restarting
php-fpm/httpd/w3wp.exe, open a Live Response session to pull process memory dumps, active network state, and raw web server access/error logs. - Rotate Any Secrets Stored in Worker Memory: Because CVE-2026-109618 relies on an 800 KiB memory disclosure primitive to leak live heap data back to the attacker in cookie headers, patching the binary does not undo an earlier memory read. Rotate database credentials, API keys, TLS private keys, Active Directory LDAP bind passwords, and Entra ID service-principal secrets accessible to that web worker.
Did you enjoy CarlsCloud.com™ today and did I help you at all?
If so, buy me a coffee or just shoot me a note via LinkedIn to say thanks it would mean a lot! -Carl