Adversaries may be using a web shell to execute arbitrary commands on a compromised server, leveraging the web shell as a persistent access point. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify and mitigate potential long-term persistence and lateral movement threats.
KQL Query
let scriptExtensions = dynamic([".php", ".jsp", ".js", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
let lookupWindow = 1m;
let lookupBin = lookupWindow / 2.0;
let distinctIpThreshold = 3;
let alerts = SecurityAlert
| extend alertData = parse_json(Entities), recordGuid = new_guid();
let shellAlerts = alerts
| where ProviderName =~ "MDATP"
| mvexpand alertData
| where alertData.Type =~ "file" and alertData.Name =~ "w3wp.exe"
| distinct SystemAlertId
| join kind=inner (alerts) on SystemAlertId;
let alldata = shellAlerts
| mvexpand alertData
| extend Type = alertData.Type;
let filedata = alldata
| extend id = tostring(alertData.$id)
| extend ImageName = alertData.Name
| where Type =~ "file" and ImageName != "w3wp.exe"
| extend imagefileref = id;
let commanddata = alldata
| extend CommandLine = tostring(alertData.CommandLine)
| extend creationtime = tostring(alertData.CreationTimeUtc)
| where Type =~ "process"
| where isnotempty(CommandLine)
| extend imagefileref = tostring(alertData.ImageFile.$ref);
let hostdata = alldata
| where Type =~ "host"
| project HostName = tostring(alertData.HostName), DnsDomain = tostring(alertData.DnsDomain), SystemAlertId
| distinct HostName, DnsDomain, SystemAlertId;
let commandKeyedData = filedata
| join kind=inner (
commanddata
) on imagefileref
| join kind=inner (hostdata) on SystemAlertId
| project recordGuid, TimeGenerated, ImageName, CommandLine, TimeKey = bin(TimeGenerated, lookupBin), HostName, DnsDomain
| extend Start = TimeGenerated;
let baseline = W3CIISLog
| project-rename SourceIP=cIP, PageAccessed=csUriStem
| summarize dcount(SourceIP) by PageAccessed
| where dcount_SourceIP <= distinctIpThreshold;
commandKeyedData
| join kind=inner (
W3CIISLog
| where csUriStem has_any(scriptExtensions)
| extend splitUriStem = split(csUriStem, "/")
| extend FileName = splitUriStem[-1] | extend firstDir = splitUriStem[-2] | extend TimeKey = range(bin(TimeGenerated-lookupWindow, lookupBin), bin(TimeGenerated, lookupBin),lookupBin)
| mv-expand TimeKey to typeof(datetime)
| summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated) by Site=sSiteName, HostName=sComputerName, AttackerIP=cIP, AttackerUserAgent=csUserAgent, csUriStem, filename=tostring(FileName), tostring(firstDir), TimeKey
) on TimeKey, HostName
| where (StartTime - EndTime) between (0min .. lookupWindow)
| extend IPCustomEntity = AttackerIP, timestamp = StartTime
| extend attackerP = pack(AttackerIP, AttackerUserAgent)
| summarize Site=make_set(Site), Attacker=make_bag(attackerP) by csUriStem, filename, tostring(ImageName), CommandLine, HostName, IPCustomEntity, timestamp
| project Site, ShellLocation=csUriStem, ShellName=filename, ParentProcess=ImageName, CommandLine, Attacker, HostName, IPCustomEntity, timestamp
| join kind=inner (baseline) on $left.ShellLocation == $right.PageAccessed
id: d2e6f31b-add1-4f44-b54d-1975a5605c1d
name: Web shell command alert enrichment
description: |
'Extracts MDATP Alerts that indicate a command was executed by a web shell. Uses time window based querying to idneitfy the potential web shell location on the server, then enriches with Attacker IP and User Agent'
requiredDataConnectors:
- connectorId: MicrosoftDefenderAdvancedThreatProtection
dataTypes:
- SecurityAlert
- connectorId: AzureMonitor(IIS)
dataTypes:
- W3CIISLog
tactics:
- PrivilegeEscalation
- Persistence
query: |
let scriptExtensions = dynamic([".php", ".jsp", ".js", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
let lookupWindow = 1m;
let lookupBin = lookupWindow / 2.0;
let distinctIpThreshold = 3;
let alerts = SecurityAlert
| extend alertData = parse_json(Entities), recordGuid = new_guid();
let shellAlerts = alerts
| where ProviderName =~ "MDATP"
| mvexpand alertData
| where alertData.Type =~ "file" and alertData.Name =~ "w3wp.exe"
| distinct SystemAlertId
| join kind=inner (alerts) on SystemAlertId;
let alldata = shellAlerts
| mvexpand alertData
| extend Type = alertData.Type;
let filedata = alldata
| extend id = tostring(alertData.$id)
| extend ImageName = alertData.Name
| where Type =~ "file" and ImageName != "w3wp.exe"
| extend imagefileref = id;
let commanddata = alldata
| extend CommandLine = tostring(alertData.CommandLine)
| extend creationtime = tostring(alertData.CreationTimeUtc)
| where Type =~ "process"
| where isnotempty(CommandLine)
| extend imagefileref = tostring(alertData.ImageFile.$ref);
let hostdata = alldata
| where Type =~ "host"
| project HostName = tostring(alertData.HostName), DnsDomain = tostring(alertData.DnsDomain), SystemAlertId
| distinct HostName, DnsDomain, SystemAlertId;
let commandKeyedData = filedata
| join kind=inner (
commanddata
) on imagefileref
| join kind=inner (hostdata) on SystemAlertId
| project recordGuid, TimeGenerated, ImageName, CommandLine, TimeKey = bin(TimeGenerated, lookupBin), HostName, DnsDomain
| extend Start = TimeGenerated;
let baseline = W3CIISLog
| project-rename SourceIP=cIP, PageAccessed=csUriStem
| summarize dcount(SourceIP) by PageAccessed
| where dcount_SourceIP <= distinctIpThreshold;
commandKeyedData
| join kind=inner (
W3CIISLog
| where csUriStem has_any(scriptExtensions)
| extend splitUriStem = split(csUriStem, "/")
| extend FileName = splitUriStem[-1] | extend firstDir = splitUriStem[-2] | extend TimeKey = range(bin(TimeGenerated-lookupWindow, lookupBin), bin(TimeGenerated, lookupBin),lookupBin)
| mv-expand TimeKey to typeof(datetime)
| summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated) by Site=sSiteName, HostName=sComputerName, AttackerIP=cIP, AttackerUserAgent=csUserAgent, csUriStem, filename=tostring(FileName), tostring(firstDir), TimeKey
) on TimeKey,
| Sentinel Table | Notes |
|---|---|
SecurityAlert | Ensure this data connector is enabled |
W3CIISLog | Ensure this data connector is enabled |
Scenario: Legitimate scheduled job execution via web shell interface
Description: A scheduled job (e.g., cron job or Windows Task Scheduler) is configured to run a script via a web interface that uses a web shell for automation.
Filter/Exclusion: Exclude commands that match known scheduled job patterns (e.g., */5 * * * *, schtasks.exe /run, or crontab -l). Use process.name or command_line to identify known job management tools.
Scenario: Admin task execution using a web shell for remote management
Description: An administrator uses a web shell (e.g., phpinfo.php, cmd.exe via IIS) to run maintenance tasks like database backups or log rotations.
Filter/Exclusion: Exclude commands that match common admin tasks (e.g., mysqldump, net use, icacls, or robocopy). Use process.name or command_line to identify administrative tools.
Scenario: Web application update via a web shell
Description: A developer uses a web shell (e.g., upload.php, upload.aspx) to deploy a new version of a web application.
Filter/Exclusion: Exclude commands that match known deployment scripts (e.g., git pull, npm install, composer install, or mvn package). Use process.name or command_line to identify deployment tools.
Scenario: Legitimate file upload via a web shell for content management
Description: A content manager uploads a file (e.g., an image or document) using a web shell that allows file uploads.
Filter/Exclusion: Exclude file upload commands that match known CMS or content management tools (e.g., wp-cli, drush, or `upload.php