Detection rules › Kusto

Beacon Traffic Based on Common User Agents Visiting Limited Number of Domains

Status
available
Severity
medium
Time window
7d
Group by
DestinationHostName, RequestClientApplication, SourceUserName
Source
github.com/Azure/Azure-Sentinel

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

Rule body kusto

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 binding: BrowserTraffic

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 binding: CommonDomains

let CommonDomains = BrowserTraffic(timeframe)
| summarize source_count=dcount(SourceUserName) by DestinationHostName
| where source_count>trusted_user_count
| project DestinationHostName;

Derived from timeframe, trusted_user_count, BrowserTraffic.

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;

Derived from timeframe, trusted_user_count, BrowserTraffic.

The stages below define let PotentialAlerts (the rule's main pipeline source).

Stage 1: source

BrowserTraffic(timeframe)

Stage 2: where

| where RequestClientApplication in(CommonUA)

References CommonUA (defined above).

Stage 3: summarize

| summarize BrowserHosts=make_set(DestinationHostName),request_count=count() by RequestClientApplication, SourceUserName
Threshold
le 3

Stage 4: where

| where array_length(BrowserHosts) <= max_sites and request_count >= min_requests

Stage 5: project

| project RequestClientApplication, SourceUserName,BrowserHosts

Stage 6: join

| join BrowserTraffic(timeframe) on RequestClientApplication, SourceUserName

Stage 7: summarize

| summarize hour_count=dcount(bin(TimeGenerated,1h)), BrowserHosts=any(BrowserHosts), request_count=count() by RequestClientApplication, SourceUserName, DestinationHostName
Threshold
ge 8

Stage 8: where

| where hour_count >= min_hours and request_count >= min_requests

Stage 9: join (negated)

| join kind=leftanti CommonDomains on DestinationHostName

Stage 10: summarize

| summarize RareHosts=make_set(DestinationHostName), TotalRequestCount=sum(request_count), BrowserHosts=any(BrowserHosts) by RequestClientApplication, SourceUserName

Stage 11: where

| where array_length(RareHosts) == array_length(BrowserHosts)

The stages below run on PotentialAlerts (the outer pipeline).

Stage 12: join

PotentialAlerts
| join BrowserTraffic(lookback) on SourceUserName, RequestClientApplication

Stage 13: summarize

| summarize RareHosts=any(RareHosts),BrowserHosts1d=any(BrowserHosts),BrowserHostsLookback=make_set(DestinationHostName) by SourceUserName, RequestClientApplication

Stage 14: where

| where array_length(RareHosts) == array_length(BrowserHostsLookback)

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
source_countgt10

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.

FieldKindValues
BrowserHostsle
  • 3 transforms: array_length
RareHostscross_field_compare
  • BrowserHosts transforms: op:eq, lhs:array_length, rhs:array_length
  • BrowserHostsLookback transforms: op:eq, lhs:array_length, rhs:array_length
RequestClientApplicationin
  • CommonUA transforms: cased
hour_countge
  • 8 transforms: cased
request_countge
  • 50 transforms: cased

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.

FieldSource
BrowserHosts1dsummarize
BrowserHostsLookbacksummarize
RareHostssummarize
RequestClientApplicationsummarize
SourceUserNamesummarize