Web Shell Response Playbook
Finding and removing web shells from IIS, Apache, and Nginx — access log analysis to identify what the shell did, lateral movement from web servers, and hardening to prevent re-deployment.
A threat hunting query on your web application firewall logs finds POST requests to a file called logon.aspx — but your application has no file by that name. The requests come from a Tor exit node and contain encoded PowerShell in the POST body. You're dealing with an ASPX web shell on an IIS server. It may have been there for weeks. You need to: find the shell, understand what it ran, check if the web server's process account was used for lateral movement, find all other shells (there might be more), and harden the web server to prevent re-deployment.
Finding the Web Shell
# Find web shells on IIS server
# Run as Administrator on the web server
$webRoots = @(
"C:\inetpub\wwwroot",
"C:\inetpub\wwwroot\app",
"D:\websites" # add any custom web roots
)
$sinceDate = Get-Date "2026-07-01" # adjust to before suspected compromise date
foreach ($root in $webRoots) {
if (-not (Test-Path $root)) { continue }
Write-Host "`n=== Scanning: $root ===" -ForegroundColor Cyan
# 1. Files modified after compromise date
Get-ChildItem $root -Recurse -File |
Where-Object { $_.LastWriteTime -gt $sinceDate } |
Select-Object FullName, LastWriteTime, Length |
Sort-Object LastWriteTime -Descending |
Format-Table -AutoSize
# 2. Files with web shell signatures — PHP
Get-ChildItem $root -Recurse -Include "*.php" -File | ForEach-Object {
$content = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue
if ($content -match "eval\s*\(|base64_decode\s*\(|system\s*\(|passthru\s*\(|exec\s*\(") {
Write-Warning "PHP shell candidate: $($_.FullName)"
}
}
# 3. Files with web shell signatures — ASPX
Get-ChildItem $root -Recurse -Include "*.aspx","*.ashx","*.asmx" -File | ForEach-Object {
$content = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue
if ($content -match "Process\.Start|cmd\.exe|powershell|WScript\.Shell|eval\(Request") {
Write-Warning "ASPX shell candidate: $($_.FullName)"
Write-Host " Preview:" -ForegroundColor Yellow
$content | Select-String "Process\.Start|cmd\.exe|powershell|WScript\.Shell|eval\(Request" |
ForEach-Object { Write-Host " Line $($_.LineNumber): $($_.Line.Trim())" }
}
}
}
Access Log Analysis
After finding the shell, the IIS or Apache access logs tell you what it was used to do. Look for POST requests to the shell's path — every POST is a command the attacker ran.
# IIS access log analysis for web shell usage
# IIS logs: C:\inetpub\logs\LogFiles\W3SVC1\u_exYYMMDD.log
# Columns: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip sc-status bytes
$shellPath = "/logon.aspx"
$logDir = "C:\inetpub\logs\LogFiles\W3SVC1\"
Get-ChildItem $logDir -Filter "u_ex2608*.log" | ForEach-Object {
Get-Content $_.FullName |
Where-Object { $_ -notmatch "^#" -and $_ -match $shellPath } |
ForEach-Object {
$fields = $_ -split " "
[PSCustomObject]@{
Date = $fields[0]
Time = $fields[1]
Method = $fields[3]
URI = $fields[4]
Query = $fields[5]
ClientIP = $fields[8]
Status = $fields[9]
Bytes = $fields[10]
}
}
} | Where-Object { $_.Method -eq "POST" } |
Select-Object Date, Time, ClientIP, URI, Status, Bytes |
Format-Table
# Note: POST body (the actual shell commands) is NOT in IIS access logs by default
# For POST body: need application-level logging, or WAF/reverse proxy logs
# Check IIS Failed Request Tracing or ModSecurity (if configured) for request body
Web Server Lateral Movement Investigation
Web shells run commands as the IIS Application Pool identity, typically IIS APPPOOL\DefaultAppPool or a specific service account. Check what that account did after the shell was deployed.
# Find processes spawned by the IIS worker process (w3wp.exe)
# Sysmon Event 1: process creation with ParentImage = w3wp.exe
# Via Sysmon (if deployed):
Get-WinEvent -FilterHashtable @{
LogName = "Microsoft-Windows-Sysmon/Operational"
Id = 1
} -ErrorAction SilentlyContinue | ForEach-Object {
$xml = [xml]$_.ToXml()
$data = $xml.Event.EventData.Data
$parent = ($data | Where-Object Name -eq "ParentImage")."#text"
if ($parent -like "*w3wp.exe*") {
[PSCustomObject]@{
Time = $_.TimeCreated
Image = ($data | Where-Object Name -eq "Image")."#text"
CommandLine= ($data | Where-Object Name -eq "CommandLine")."#text"
User = ($data | Where-Object Name -eq "User")."#text"
}
}
} | Where-Object { $_ } | Format-List
# Red flags: cmd.exe, powershell.exe, net.exe, whoami, net user spawned by w3wp.exe
Shell Removal and Server Hardening
| Action | Command / method | Purpose |
|---|---|---|
| Remove web shell file | Delete the file; confirm with Get-FileHash that the correct file is removed; verify hash is recorded before deletion for forensic record | Eliminate the access mechanism |
| Restore from known-good application backup | Deploy the application from source control or a clean build; don't just delete the shell — verify all application files match known-good hashes | Eliminate any additional shells or backdoors in application files |
| Disable script execution in upload directories | IIS → Handler Mappings → remove ASPX/PHP handler for upload directories; or use web.config: <handlers><remove name="ISAPI-dll"/> | Prevent re-upload of shells in user-writable directories |
| Restrict AppPool identity permissions | IIS AppPool should not have write access to the web root or the ability to spawn child processes. Run AppPool as a custom service account with minimal permissions. | Limit blast radius if a shell is deployed again |
| Enable Windows Defender Application Control (WDAC) | Prevent unsigned executables from running — web shells that drop binaries will fail to execute | Defense in depth against shell-launched payloads |
Q & A
Q: You found the web shell but the web server runs a 3rd-party application you don't fully understand. How do you verify whether other files were modified without a file integrity baseline?
Four approaches in order of confidence: (1) Compare against the application vendor's published checksums — most enterprise software vendors publish file hashes for each version. (2) Restore from your most recent known-good deployment (source control, CI/CD artifact, golden image) and diff against the current state. (3) Use the web server access logs to identify all files that received POST requests — those paths are your highest-risk candidates for shells, since shells are typically accessed via POST. (4) Look for recently modified files in the web root using the date/time of the web shell discovery as the reference — any file modified in the same time window as the known shell is suspicious. If none of these approaches is feasible, the safest action is a full reimage of the web server from a clean build, which is typically faster than a comprehensive manual file integrity check on an unknown application.
Q: The web shell was on an Internet-facing server, but the attacker didn't appear to move laterally. Can you still declare eradication after removing the shell?
Verify before declaring: (1) Check if the AppPool identity had any authentication events on other systems during the compromise window — a "no lateral movement" conclusion requires positive evidence, not absence of obvious evidence. (2) Check if the web server had access to any internal APIs, databases, or file shares — even without explicit lateral movement, the attacker may have accessed internal data through the application's existing integrations. (3) Confirm no new users or services were created on the web server itself. If all three checks are clean, and the web server has no privileged access to internal resources, then yes — removing the shell and hardening the server is a complete eradication for this specific incident. Document the scope limitation explicitly: "lateral movement not detected; determination based on Sysmon process creation telemetry and DC authentication logs for the AppPool service account."