Detection rules › Kusto
Malicious web application requests linked with Microsoft Defender for Endpoint (formerly Microsoft Defender ATP) alerts
'Takes Microsoft Defender for Endpoint (formerly Microsoft Defender ATP) alerts where web scripts are present in the evidence and correlates with requests made to those scripts in the WCSIISLog to surface new alerts for potentially malicious web request activity. The lookback for alerts is set to 1h and the lookback for W3CIISLogs is set to 7d. A sample set of popular web script extensions has been provided in scriptExtensions that should be tailored to your environment.'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | T1505 Server Software Component |
Rule body kusto
id: fbfbf530-506b-49a4-81ad-4030885a195c
name: Malicious web application requests linked with Microsoft Defender for Endpoint (formerly Microsoft Defender ATP) alerts
description: |
'Takes Microsoft Defender for Endpoint (formerly Microsoft Defender ATP) alerts where web scripts are present in the evidence and correlates with requests made to those scripts in the WCSIISLog to surface new alerts for potentially malicious web request activity.
The lookback for alerts is set to 1h and the lookback for W3CIISLogs is set to 7d. A sample set of popular web script extensions has been provided in scriptExtensions that should be tailored to your environment.'
severity: Medium
requiredDataConnectors:
- connectorId: MicrosoftDefenderAdvancedThreatProtection
dataTypes:
- SecurityAlert
- connectorId: AzureMonitor(IIS)
dataTypes:
- W3CIISLog
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1505
query: |
let alertTimeWindow = 1h;
let logTimeWindow = 7d;
// Define script extensions that suit your web application environment - a sample are provided below
let scriptExtensions = dynamic([".php", ".jsp", ".js", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
let alertData = materialize(SecurityAlert
| where TimeGenerated > ago(alertTimeWindow)
| where ProviderName == "MDATP"
// Parse and expand the alert JSON
| extend alertData = parse_json(Entities)
| mvexpand alertData);
let fileData = alertData
// Extract web script files from MDATP alerts - our malicious web scripts - candidate webshells
| where alertData.Type =~ "file"
| where alertData.Name has_any(scriptExtensions)
| extend FileName = tostring(alertData.Name), Directory = tostring(alertData.Directory);
let hostData = alertData
// Extract server details from alerts and map to alert id
| where alertData.Type =~ "host"
| project HostName = tostring(alertData.HostName), DnsDomain = tostring(alertData.DnsDomain), SystemAlertId
| distinct HostName, DnsDomain, SystemAlertId;
// Join the files on their impacted servers
let webshellData = fileData
| join kind=inner (hostData) on SystemAlertId
| project TimeGenerated, FileName, Directory, HostName, DnsDomain;
webshellData
| join (
// Find requests that were made to this file on the impacted server in the W3CIISLog table
W3CIISLog
| where TimeGenerated > ago(logTimeWindow)
// Restrict to accesses to script extensions
| where csUriStem has_any(scriptExtensions)
| extend splitUriStem = split(csUriStem, "/")
| extend FileName = splitUriStem[-1], HostName = sComputerName
// Summarize potential attacker activity
| summarize count(), StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), RequestUserAgents=make_set(csUserAgent), ReqestMethods=make_set(csMethod), RequestStatusCodes=make_set(scStatus), RequestCookies=make_set(csCookie), RequestReferers=make_set(csReferer), RequestQueryStrings=make_set(csUriQuery) by AttackerIP=cIP, SiteName=sSiteName, ShellLocation=csUriStem, tostring(FileName), HostName
) on FileName, HostName
| project StartTime, EndTime, AttackerIP, RequestUserAgents, Computer = HostName, SiteName, ShellLocation, ReqestMethods, RequestStatusCodes, RequestCookies, RequestReferers, RequestQueryStrings, RequestCount = count_
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: AttackerIP
version: 1.0.4
kind: Scheduled
metadata:
source:
kind: Scheduled
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Parameters
let alertTimeWindow = 1h;
let logTimeWindow = 7d;
let scriptExtensions = dynamic([".php", ".jsp", ".js", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
Let binding: hostData
let hostData = alertData
| where alertData.Type =~ "host"
| project HostName = tostring(alertData.HostName), DnsDomain = tostring(alertData.DnsDomain), SystemAlertId
| distinct HostName, DnsDomain, SystemAlertId;
Derived from alertData.
The stages below define let webshellData (the rule's main pipeline source).
Stage 1: source
SecurityAlert
Stage 2: where
| where TimeGenerated > ago(alertTimeWindow)
Stage 3: where
| where ProviderName == "MDATP"
Stage 4: extend
| extend alertData = parse_json(Entities)
Stage 5: mv-expand
| mvexpand alertData
Stage 6: where
| where alertData.Type =~ "file"
Stage 7: where
| where alertData.Name has_any(scriptExtensions)
Stage 8: extend
| extend FileName = tostring(alertData.Name), Directory = tostring(alertData.Directory)
Stage 9: join
| join kind=inner (hostData) on SystemAlertId
Stage 10: project
| project TimeGenerated, FileName, Directory, HostName, DnsDomain
The stages below run on webshellData (the outer pipeline).
Stage 11: join
webshellData
| join (
W3CIISLog
| where TimeGenerated > ago(logTimeWindow)
| where csUriStem has_any(scriptExtensions)
| extend splitUriStem = split(csUriStem, "/")
| extend FileName = splitUriStem[-1], HostName = sComputerName
| summarize count(), StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), RequestUserAgents=make_set(csUserAgent), ReqestMethods=make_set(csMethod), RequestStatusCodes=make_set(scStatus), RequestCookies=make_set(csCookie), RequestReferers=make_set(csReferer), RequestQueryStrings=make_set(csUriQuery) by AttackerIP=cIP, SiteName=sSiteName, ShellLocation=csUriStem, tostring(FileName), HostName
) on FileName, HostName
Stage 12: project
| project StartTime, EndTime, AttackerIP, RequestUserAgents, Computer = HostName, SiteName, ShellLocation, ReqestMethods, RequestStatusCodes, RequestCookies, RequestReferers, RequestQueryStrings, RequestCount = count_
Stage 13: extend
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
Stage 14: extend
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
HostNameDomain =DomainIndex != -1substring(Computer, (DomainIndex + 1))ComputerStage 15: summarize
summarize by AttackerIP, SiteName, ShellLocation, HostName
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
Name | match |
|
ProviderName | eq |
|
Type | eq |
|
csUriStem | match |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
AttackerIP | summarize |
HostName | summarize |
ShellLocation | summarize |
SiteName | summarize |