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

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 used in Stage 13

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

Let binding: CommonUA used in Stage 6

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.

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
BrowserHosts1dsummarize
BrowserHostsLookbacksummarize
RareHostssummarize
RequestClientApplicationsummarize
SourceUserNamesummarize