Detecting Web Shells (T1505.003): KQL and SPL Queries for IIS, Apache, and Tomcat
A web shell is one of the most durable footholds an attacker can plant. After exploiting a public-facing application (T1190), an adversary drops a small script — an .aspx, .php, or .jsp file — into a directory the web server already renders. From that moment they have an interactive command channel that survives reboots, blends into normal web traffic, and requires no additional malware on disk. MITRE ATT&CK tracks this as T1505.003 (Server Software Component: Web Shell), and it remains one of the highest-value detections a SOC can build because it sits directly on the path between initial access and hands-on-keyboard operations.
This guide walks through three complementary detection strategies with working KQL for Microsoft Sentinel / Defender and SPL for Splunk. No single query catches every web shell — the strength comes from layering process behavior, file creation, and log anomalies so an attacker has to evade all three at once.
How web shells actually behave
The web shell file itself is easy to obfuscate, so signature-only approaches age poorly. What is far harder for an attacker to hide is the behavior that follows. When an operator interacts with a shell, the web server worker process — w3wp.exe on IIS, httpd.exe or php-cgi.exe on Apache/PHP, java.exe or tomcat*.exe on Tomcat — becomes the parent of a command interpreter it should never spawn. That parent-child relationship is the single most reliable web shell signal available, and it maps cleanly to T1059.003 (Windows Command Shell).
Detection 1: web server processes spawning a shell
A healthy IIS or Tomcat worker serves content; it does not launch cmd.exe to run whoami. Alert whenever a web service process spawns a shell or a reconnaissance binary. In Microsoft Defender / Sentinel:
DeviceProcessEvents
| where InitiatingProcessFileName in~ ('w3wp.exe','httpd.exe','nginx.exe','php-cgi.exe','tomcat9.exe','java.exe')
| where FileName in~ ('cmd.exe','powershell.exe','pwsh.exe','net.exe','net1.exe','whoami.exe','systeminfo.exe','ipconfig.exe','nltest.exe','tasklist.exe')
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName, ProcessCommandLine, InitiatingProcessCommandLine, AccountName
| order by Timestamp descThe equivalent in Splunk using Sysmon Event ID 1 (process creation):
index=windows source="WinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1
ParentImage IN ("*\w3wp.exe","*\httpd.exe","*\nginx.exe","*\php-cgi.exe","*\tomcat*.exe","*\java.exe")
Image IN ("*\cmd.exe","*\powershell.exe","*\net.exe","*\whoami.exe","*\systeminfo.exe","*\nltest.exe")
| table _time host ParentImage Image CommandLine UserThis is high-fidelity by design. Legitimate cases exist (some CI/CD tooling or diagnostic pages shell out), so baseline your specific servers before promoting it to a page — but on a typical production web host, a hit here warrants immediate triage.
Detection 2: new executable content written to the webroot
Catch the drop itself. Web shells must be written somewhere the server will execute them: inetpub\wwwroot, htdocs, Tomcat webapps, and similar. Critically, the process writing the file is often the web worker itself (via an upload flaw or exploit), which is deeply abnormal — worker processes serve files, they rarely create new server-side scripts. This ties to T1105 (Ingress Tool Transfer).
DeviceFileEvents
| where ActionType == 'FileCreated'
| where FolderPath has_any ('wwwroot','inetpub','htdocs','webapps','www')
| where FileName endswith '.aspx' or FileName endswith '.asp' or FileName endswith '.php'
or FileName endswith '.jsp' or FileName endswith '.jspx' or FileName endswith '.ashx' or FileName endswith '.php5'
| where InitiatingProcessFileName in~ ('w3wp.exe','php-cgi.exe','java.exe','tomcat9.exe','httpd.exe','nginx.exe')
| project Timestamp, DeviceName, FolderPath, FileName, InitiatingProcessFileName, InitiatingProcessAccountName
| order by Timestamp descIn Splunk with Sysmon Event ID 11 (file create):
index=windows source="WinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=11
(TargetFilename="*\wwwroot\*" OR TargetFilename="*\htdocs\*" OR TargetFilename="*\webapps\*" OR TargetFilename="*\www\*")
(TargetFilename="*.aspx" OR TargetFilename="*.asp" OR TargetFilename="*.php" OR TargetFilename="*.jsp" OR TargetFilename="*.ashx")
| stats min(_time) as first_write values(Image) as writers by host TargetFilename
| convert ctime(first_write)Deployment pipelines legitimately write scripts to the webroot, so exclude your known deployment service accounts and build-agent hosts rather than the file paths — narrowing on who writes keeps coverage of attacker-driven drops intact.
Detection 3: access-log anomalies for newly seen script pages
When endpoint telemetry is missing on a server, the web access logs still tell the story. Operators reach a web shell with repeated POST requests to a script page that has no history in your environment — a page that appeared out of nowhere and is now receiving commands. This overlaps with T1071.001 (Web Protocols). In Sentinel with W3CIISLog:
W3CIISLog
| where csMethod == 'POST'
| where csUriStem endswith '.aspx' or csUriStem endswith '.asp' or csUriStem endswith '.php'
or csUriStem endswith '.jsp' or csUriStem endswith '.ashx'
| summarize FirstSeen = min(TimeGenerated), Hits = count(), Clients = dcount(cIP) by csUriStem
| where FirstSeen > ago(24h) and Hits > 3
| order by Hits descA page whose very first request appeared in the last day and is already fielding multiple POSTs from few source IPs is a classic web shell interaction pattern. The Splunk version pivots on first-seen per page and client IP:
index=iis sourcetype="ms:iis:auto" cs_method=POST
| rex field=cs_uri_stem "(?[^/]+\.(?:aspx|asp|php|jsp|ashx))$"
| stats earliest(_time) as first_seen count as hits dc(c_ip) as clients by page
| where first_seen > relative_time(now(),"-1d") AND hits > 3
| convert ctime(first_seen)
| sort - hitsTuning and false positives
- Baseline deployment accounts. Suppress your CI/CD and admin service accounts on the file-write rule, not the paths themselves.
- Watch legitimate shell-outs. Some enterprise apps and monitoring pages spawn
cmd.exe. Build a per-application allowlist of known-good command lines before alerting broadly. - Correlate, don't isolate. A file-write hit followed within minutes by a process-lineage hit on the same host is a near-certain confirmation — chain the two into a single incident.
- Handle short-lived names. Attackers rename or delete shells quickly; retain access logs long enough that the first-seen logic still has history to compare against.
From detection to response
When one of these fires, move fast: pull the suspect script and hash it, capture the parent-process command lines, and review the source IPs hitting the page. Because a web shell almost always follows exploitation of the application itself, treat every confirmed hit as an active intrusion — isolate the host, preserve the web and endpoint logs, and hunt for follow-on command execution and lateral movement. Layered together, these three detections give you coverage from the moment the shell lands through the first command an attacker types, which is exactly where you want to catch T1505.003.