Detection rules › Kusto
Request for single resource on domain
'This will look for connections to a domain where only a single file is requested, this is unusual as most modern web applications require additional recources. This type of activity is often assocaited with malware beaconing or tracking URL's delivered in emails. Developed for Zscaler but applicable to any outbound web logging.'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule body
id: 4d500e6d-c984-43a3-9f39-7edec8dcc04d
name: Request for single resource on domain
description: |
'This will look for connections to a domain where only a single file is requested, this is unusual as most modern web applications require additional recources. This type of activity is often assocaited with malware beaconing or tracking URL's delivered in emails. Developed for Zscaler but applicable to any outbound web logging.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1102
- T1071
query: |
let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml"]);
//The number of URI's seen to be suspicious, higher = less likely to be suspicious
let uriThreshold = 1;
CommonSecurityLog
// Only look at connections that were allowed through the web proxy
| where DeviceVendor =~ "Zscaler" and DeviceAction =~ "Allowed"
// Only look where some data was exchanged.
| where SentBytes > 0 and ReceivedBytes > 0
// Extract the Domain
| extend Domain = iff(countof(DestinationHostName,'.') >= 2, strcat(split(DestinationHostName,'.')[-2], '.',split(DestinationHostName,'.')[-1]), DestinationHostName)
| extend GetData=iff(RequestURL == "?", 1, 0)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), makelist(RequestURL), makelist(DestinationIP), makelist(SourceIP), numOfConnections = count(), make_set(RequestMethod), max(GetData), max(RequestContext) by Domain
// Determine the number of URIs that have been visited for the domain
| extend destinationURI = arraylength(list_RequestURL)
| where destinationURI <= uriThreshold
| where tostring(list_RequestURL) has_any(scriptExtensions)
//Remove matches with referer
| where max_RequestContext == ""
//Keep requests where data was trasferred either in a GET with parameters or a POST
| where set_RequestMethod in~ ("POST") or max_GetData == 1
//Defeat email click tracking, may increase FN's while decreasing FP's
| where list_RequestURL !has "click" and set_RequestMethod !has "GET"
| mvexpand list_RequestURL, list_DestinationIP
| extend RequestURL = tostring(list_RequestURL), DestinationIP = tostring(list_DestinationIP), ClientIP = tostring(list_SourceIP)
//Extend custom entitites for incidents
| project-away list_RequestURL, list_DestinationIP, list_SourceIP, destinationURI, Domain, StartTimeUtc, EndTimeUtc, max_GetData, max_RequestContext
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: DestinationIP
version: 1.0.5
kind: Scheduled
Stages and Predicates
Parameters
let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml"]);
let uriThreshold = 1;
Stage 1: source
CommonSecurityLog
Stage 2: where
| where DeviceVendor =~ "Zscaler" and DeviceAction =~ "Allowed"
Stage 3: where
| where SentBytes > 0 and ReceivedBytes > 0
Stage 4: extend
| extend Domain = iff(countof(DestinationHostName,'.') >= 2, strcat(split(DestinationHostName,'.')[-2], '.',split(DestinationHostName,'.')[-1]), DestinationHostName)
Domain =if
/* macro: (countof(DestinationHostName, '.') >= 2) */strcat(split(DestinationHostName, '.')[(- 2)], '.', split(DestinationHostName, '.')[(- 1)])else
DestinationHostNameStage 5: extend
| extend GetData=iff(RequestURL == "?", 1, 0)
GetData =if
RequestURL matches regex "."1else
0Stage 6: summarize
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), makelist(RequestURL), makelist(DestinationIP), makelist(SourceIP), numOfConnections = count(), make_set(RequestMethod), max(GetData), max(RequestContext) by Domain
Stage 7: extend
| extend destinationURI = arraylength(list_RequestURL)
Stage 8: where
| where destinationURI <= uriThreshold
Stage 9: where
| where tostring(list_RequestURL) has_any(scriptExtensions)
Stage 10: where
| where max_RequestContext == ""
Stage 11: where
| where set_RequestMethod in~ ("POST") or max_GetData == 1
Stage 12: where
| where list_RequestURL !has "click" and set_RequestMethod !has "GET"
Stage 13: mv-expand
| mvexpand list_RequestURL, list_DestinationIP
Stage 14: extend
| extend RequestURL = tostring(list_RequestURL), DestinationIP = tostring(list_DestinationIP), ClientIP = tostring(list_SourceIP)
Stage 15: project-away
| project-away list_RequestURL, list_DestinationIP, list_SourceIP, destinationURI, Domain, StartTimeUtc, EndTimeUtc, max_GetData, max_RequestContext
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
list_RequestURL | match | click | excludes:list_RequestURL field:"list_RequestURL" value:"click" |
set_RequestMethod | match | GET | excludes:set_RequestMethod field:"set_RequestMethod" value:"GET" |
Indicators
These rows show field, operator, and value matches.
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
numOfConnections | summarize |
ClientIP | extend |
DestinationIP | extend |
RequestURL | extend |