Chapter 21

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.

Scenario

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

PowerShellwebshell-hunt.ps1
# 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.

PowerShellwebshell-log-analysis.ps1
# 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.

PowerShellwebserver-lateral-move.ps1
# 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

ActionCommand / methodPurpose
Remove web shell fileDelete the file; confirm with Get-FileHash that the correct file is removed; verify hash is recorded before deletion for forensic recordEliminate the access mechanism
Restore from known-good application backupDeploy the application from source control or a clean build; don't just delete the shell — verify all application files match known-good hashesEliminate any additional shells or backdoors in application files
Disable script execution in upload directoriesIIS → 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 permissionsIIS 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 executeDefense 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."