Detection rules › Kusto
Beacon Traffic Based on Common User Agents Visiting Limited Number of Domains
This query searches web proxy logs for a specific type of beaconing behavior by joining a number of sources together: - Traffic by actual web browsers - by looking at traffic generated by a UserAgent that looks like a browser and is used by multiple users to visit a large number of domains. - Users that make requests using one of these actual browsers, but only to a small set of domains, none of which are common domains. - The traffic is beacon-like; meaning that it occurs during many different hours of the day (i.e. periodic).
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule body
id: 6345c923-99eb-4a83-b11d-7af0ffa75577
name: Beacon Traffic Based on Common User Agents Visiting Limited Number of Domains
description: |
This query searches web proxy logs for a specific type of beaconing behavior by joining a number of sources together:
- Traffic by actual web browsers - by looking at traffic generated by a UserAgent that looks like a browser and is used by multiple users to visit a large number of domains.
- Users that make requests using one of these actual browsers, but only to a small set of domains, none of which are common domains.
- The traffic is beacon-like; meaning that it occurs during many different hours of the day (i.e. periodic).
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Zscaler
dataTypes:
- CommonSecurityLog
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071.001
query: |
let timeframe = 1d; // Timeframe during which to search for beaconing behavior.
let lookback = 7d; // Look back period to find if browser was used for other domains by user.
let min_requests=50; // Minimum number of requests to consider it beacon traffic.
let min_hours=8; // Minimum number of different hours during which connections were made to consider it beacon traffic.
let trusted_user_count=10; // If visited by this many users a domain is considered 'trusted'.
let max_sites=3; // Maximum number of different sites visited using this user-agent.
// Client-specific query to obtain 'browser-like' traffic from proxy logs.
let BrowserTraffic = (p:timespan) {
CommonSecurityLog
| where DeviceVendor == "Zscaler" and DeviceProduct == "NSSWeblog"
| where TimeGenerated >ago(p)
| project TimeGenerated, SourceUserName, DestinationHostName, RequestClientApplication
| where (RequestClientApplication startswith "Mozilla/" and RequestClientApplication contains "Gecko")
};
let CommonDomains = BrowserTraffic(timeframe)
| summarize source_count=dcount(SourceUserName) by DestinationHostName
| where source_count>trusted_user_count
| project DestinationHostName;
let CommonUA = BrowserTraffic(timeframe)
| summarize source_count=dcount(SourceUserName), host_count=dcount(DestinationHostName) by RequestClientApplication
| where source_count>trusted_user_count and host_count > 100 // Normal browsers are browsers used by many people and visiting many different sites.
| project RequestClientApplication;
// Find browsers that are common, i.e. many users use them and they use them to visit many different sites,
// but some users only use the browser to visit a very limited set of sites.
// These are considered suspicious, since they might be an attacker masquerading a beacon as a legitimate browser.
let SuspiciousBrowers = BrowserTraffic(timeframe)
| where RequestClientApplication in(CommonUA)
| summarize BrowserHosts=make_set(DestinationHostName),request_count=count() by RequestClientApplication, SourceUserName
| where array_length(BrowserHosts) <= max_sites and request_count >= min_requests
| project RequestClientApplication, SourceUserName,BrowserHosts;
// Just reporting on suspicious browsers gives too many false positives.
// For example, users that have the browser open on the login screen of 1 specific application.
// In the suspicious browsers we can search for 'beacon-like' behavior.
// Get all browser traffic by the suspicious browsers.
let PotentialAlerts=SuspiciousBrowers
| join BrowserTraffic(timeframe) on RequestClientApplication, SourceUserName
// Find beaconing-like traffic - i.e. contacting the same host in many different hours.
| summarize hour_count=dcount(bin(TimeGenerated,1h)), BrowserHosts=any(BrowserHosts), request_count=count() by RequestClientApplication, SourceUserName, DestinationHostName
| where hour_count >= min_hours and request_count >= min_requests
// Remove common domains like login.microsoft.com.
| join kind=leftanti CommonDomains on DestinationHostName
| summarize RareHosts=make_set(DestinationHostName), TotalRequestCount=sum(request_count), BrowserHosts=any(BrowserHosts) by RequestClientApplication, SourceUserName
// Remove browsers that visit any common domains.
| where array_length(RareHosts) == array_length(BrowserHosts);
// Look back for X days to see if the browser was not used to visit more hosts.
// This is to get rid of someone that started up the browser a long time ago, and left only a single tab open.
PotentialAlerts
| join BrowserTraffic(lookback) on SourceUserName, RequestClientApplication
| summarize RareHosts=any(RareHosts),BrowserHosts1d=any(BrowserHosts),BrowserHostsLookback=make_set(DestinationHostName) by SourceUserName, RequestClientApplication
| where array_length(RareHosts) == array_length(BrowserHostsLookback)
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: SourceUserName
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let timeframe = 1d;
let lookback = 7d;
let min_requests = 50;
let min_hours = 8;
let trusted_user_count = 10;
let max_sites = 3;
let BrowserTraffic, let SuspiciousBrowers and let PotentialAlerts are inlined into the numbered stages below.
Let binding: CommonDomains
let CommonDomains = BrowserTraffic(timeframe)
| summarize source_count=dcount(SourceUserName) by DestinationHostName
| where source_count>trusted_user_count
| project DestinationHostName;
Let binding: CommonUA
let CommonUA = BrowserTraffic(timeframe)
| summarize source_count=dcount(SourceUserName), host_count=dcount(DestinationHostName) by RequestClientApplication
| where source_count>trusted_user_count and host_count > 100
| project RequestClientApplication;
Stages 1 to 15 define let PotentialAlerts (the rule's main pipeline source); stages 16 to 18 run on it.
Stage 1: source
CommonSecurityLog
Stage 2: where
| where DeviceVendor == "Zscaler" and DeviceProduct == "NSSWeblog"
Stage 3: where
| where TimeGenerated >ago(p)
Stage 4: project
| project TimeGenerated, SourceUserName, DestinationHostName, RequestClientApplication
Stage 5: where
| where (RequestClientApplication startswith "Mozilla/" and RequestClientApplication contains "Gecko")
Stage 6: where
| where RequestClientApplication in(CommonUA)
Stage 7: summarize
| summarize BrowserHosts=make_set(DestinationHostName),request_count=count() by RequestClientApplication, SourceUserName
Stage 8: where
| where array_length(BrowserHosts) <= max_sites and request_count >= min_requests
Stage 9: project
| project RequestClientApplication, SourceUserName,BrowserHosts
Stage 10: join
| join BrowserTraffic(timeframe) on RequestClientApplication, SourceUserName
Stage 11: summarize
| summarize hour_count=dcount(bin(TimeGenerated,1h)), BrowserHosts=any(BrowserHosts), request_count=count() by RequestClientApplication, SourceUserName, DestinationHostName
Stage 12: where
| where hour_count >= min_hours and request_count >= min_requests
Stage 13: join (negated)
| join kind=leftanti CommonDomains on DestinationHostName
Stage 14: summarize
| summarize RareHosts=make_set(DestinationHostName), TotalRequestCount=sum(request_count), BrowserHosts=any(BrowserHosts) by RequestClientApplication, SourceUserName
Stage 15: where
| where array_length(RareHosts) == array_length(BrowserHosts)
Stage 16: join
PotentialAlerts
| join BrowserTraffic(lookback) on SourceUserName, RequestClientApplication
Stage 17: summarize
| summarize RareHosts=any(RareHosts),BrowserHosts1d=any(BrowserHosts),BrowserHostsLookback=make_set(DestinationHostName) by SourceUserName, RequestClientApplication
Stage 18: where
| where array_length(RareHosts) == array_length(BrowserHostsLookback)
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DeviceProduct | eq | NSSWeblog | excludes:DeviceProduct field:"DeviceProduct" value:"NSSWeblog" |
DeviceVendor | eq | Zscaler | excludes:DeviceVendor field:"DeviceVendor" value:"Zscaler" |
RequestClientApplication | contains | Gecko | excludes:RequestClientApplication field:"RequestClientApplication" value:"Gecko" |
RequestClientApplication | starts_with | Mozilla/ | excludes:RequestClientApplication field:"RequestClientApplication" value:"Mozilla/" |
source_count | gt | 10 | excludes:source_count field:"source_count" value:"10" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceProduct | eq |
| field:"DeviceProduct" kind:eq value:"NSSWeblog" |
DeviceVendor | eq |
| field:"DeviceVendor" kind:eq value:"Zscaler" |
RareHosts | cross_field_compare |
| field:"RareHosts" kind:cross_field_compare |
RequestClientApplication | contains |
| field:"RequestClientApplication" kind:contains value:"Gecko" |
RequestClientApplication | in |
| field:"RequestClientApplication" kind:in value:"CommonUA" |
RequestClientApplication | starts_with |
| field:"RequestClientApplication" kind:starts_with value:"Mozilla/" |
hour_count | ge |
| field:"hour_count" kind:ge value:"8" |
request_count | ge |
| field:"request_count" kind:ge value:"50" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
BrowserHosts1d | summarize |
BrowserHostsLookback | summarize |
RareHosts | summarize |
RequestClientApplication | summarize |
SourceUserName | summarize |