Detection rules › Kusto

Fortinet - Beacon pattern detected

Severity
low
Time window
1d
Group by
DestinationIP, DestinationPort, EndTime, SourceIP, StartTime, sum_ReceivedBytes, sum_SentBytes
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

'Identifies patterns in the time deltas of contacts between internal and external IPs in Fortinet network data that are consistent with beaconing. Accounts for randomness (jitter) and seasonality such as working hours that may have been introduced into the beacon pattern. The lookback is set to 1d, the minimum granularity in time deltas is set to 60 seconds and the minimum number of beacons required to emit a detection is set to 4. Increase the lookback period to capture beacons with larger periodicities. The jitter tolerance is set to 0.2 - This means we account for an overall 20% deviation from the infered beacon periodicity. Seasonality is dealt with automatically using series_outliers. Note: In large environments it may be necessary to reduce the lookback period to get fast query times.'

MITRE ATT&CK coverage

Rule body kusto

id: 3255ec41-6bd6-4f35-84b1-c032b18bbfcb
name: Fortinet - Beacon pattern detected
description: |
  'Identifies patterns in the time deltas of contacts between internal and external IPs in Fortinet network data that are consistent with beaconing.
   Accounts for randomness (jitter) and seasonality such as working hours that may have been introduced into the beacon pattern.
   The lookback is set to 1d, the minimum granularity in time deltas is set to 60 seconds and the minimum number of beacons required to emit a  detection is set to 4.
   Increase the lookback period to capture beacons with larger periodicities.
   The jitter tolerance is set to 0.2 - This means we account for an overall 20% deviation from the infered beacon periodicity. Seasonality is dealt with  automatically using series_outliers.
   Note: In large environments it may be necessary to reduce the lookback period to get fast query times.'
severity: Low
requiredDataConnectors:
  - connectorId: Fortinet
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
  - T1571
query: |
    let starttime = 1d;
    let TimeDeltaThresholdInSeconds = 60; // we ignore beacons diffs that fall below this threshold
    let TotalBeaconsThreshold = 4; // minimum number of beacons required in a session to surface a row
    let JitterTolerance = 0.2; // tolerance to jitter, e.g. - 0.2 = 20% jitter is tolerated either side of the periodicity
    CommonSecurityLog
    | where DeviceVendor == "Fortinet"
    | where TimeGenerated > ago(starttime)
    // eliminate bad data
    | where isnotempty(SourceIP) and isnotempty(DestinationIP) and SourceIP != "0.0.0.0"
    // filter out deny, close, rst and SNMP to reduce data volume
    | where DeviceAction !in ("close", "client-rst", "server-rst", "deny") and DestinationPort != 161
    // map input fields
    | project TimeGenerated , SourceIP, DestinationIP, DestinationPort, ReceivedBytes, SentBytes, DeviceAction
    // where destination IPs are public
    | where ipv4_is_private(DestinationIP) == false
    // sort into source->destination 'sessions'
    | sort by SourceIP asc, DestinationIP asc, DestinationPort asc, TimeGenerated asc
    | serialize
    // time diff the contact times between source and destination to get a list of deltas
    | extend nextTimeGenerated = next(TimeGenerated, 1), nextSourceIP = next(SourceIP, 1), nextDestIP = next(DestinationIP, 1), nextDestPort = next(DestinationPort, 1)
    | extend TimeDeltainSeconds = datetime_diff("second",nextTimeGenerated,TimeGenerated)
    | where SourceIP == nextSourceIP and DestinationIP == nextDestIP and DestinationPort == nextDestPort
    // remove small time deltas below the set threshold
    | where TimeDeltainSeconds > TimeDeltaThresholdInSeconds
    // summarize the deltas by source->destination
    | summarize count(), StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), sum(ReceivedBytes), sum(SentBytes), makelist(TimeDeltainSeconds), makeset(DeviceAction) by SourceIP, DestinationIP, DestinationPort
    // get some statistical properties of the delta distribution and smooth any outliers (e.g. laptop shut overnight, working hours)
    | extend series_stats(list_TimeDeltainSeconds), outliers=series_outliers(list_TimeDeltainSeconds)
    // expand the deltas and the outliers
    | mvexpand list_TimeDeltainSeconds to typeof(double), outliers to typeof(double)
    // replace outliers with the average of the distribution
    | extend list_TimeDeltainSeconds_normalized=iff(outliers > 1.5 or outliers < -1.5, series_stats_list_TimeDeltainSeconds_avg , list_TimeDeltainSeconds)
    // summarize with the smoothed distribution
    | summarize BeaconCount=count(), makelist(list_TimeDeltainSeconds), list_TimeDeltainSeconds_normalized=makelist(list_TimeDeltainSeconds_normalized), makeset(set_DeviceAction) by StartTime, EndTime, SourceIP, DestinationIP, DestinationPort, sum_ReceivedBytes, sum_SentBytes
    // get stats on the smoothed distribution
    | extend series_stats(list_TimeDeltainSeconds_normalized)
    // match jitter tolerance on smoothed distrib
    | extend MaxJitter = (series_stats_list_TimeDeltainSeconds_normalized_avg*JitterTolerance)
    | where series_stats_list_TimeDeltainSeconds_normalized_stdev < MaxJitter
    // where the minimum beacon threshold is satisfied and there was some data transfer
    | where BeaconCount > TotalBeaconsThreshold and (sum_SentBytes > 0 or sum_ReceivedBytes > 0)
    // final projection
    | project StartTime, EndTime, SourceIP, DestinationIP, DestinationPort, BeaconCount, TimeDeltasInSeconds=list_list_TimeDeltainSeconds, Periodicity=series_stats_list_TimeDeltainSeconds_normalized_avg, ReceivedBytes=sum_ReceivedBytes, SentBytes=sum_SentBytes, Actions=set_set_DeviceAction
    // where periodicity is order of magnitude larger than time delta threshold (eliminates FPs whose periodicity is close to the values we ignored)
    | where Periodicity >= (10*TimeDeltaThresholdInSeconds)
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DestinationIP
version: 1.0.5
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Network" ]

Stages and Predicates

Parameters

let starttime = 1d;
let TimeDeltaThresholdInSeconds = 60;
let TotalBeaconsThreshold = 4;
let JitterTolerance = 0.2;

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceVendor == "Fortinet"

Stage 3: where

| where TimeGenerated > ago(starttime)

Stage 4: where

| where isnotempty(SourceIP) and isnotempty(DestinationIP) and SourceIP != "0.0.0.0"

Stage 5: where

| where DeviceAction !in ("close", "client-rst", "server-rst", "deny") and DestinationPort != 161

Stage 6: project

| project TimeGenerated , SourceIP, DestinationIP, DestinationPort, ReceivedBytes, SentBytes, DeviceAction

Stage 7: where

| where ipv4_is_private(DestinationIP) == false

Stage 8: sort

| sort by SourceIP asc, DestinationIP asc, DestinationPort asc, TimeGenerated asc

Stage 9: kusto:serialize

| serialize

Stage 10: extend

| extend nextTimeGenerated = next(TimeGenerated, 1), nextSourceIP = next(SourceIP, 1), nextDestIP = next(DestinationIP, 1), nextDestPort = next(DestinationPort, 1)

Stage 11: extend

| extend TimeDeltainSeconds = datetime_diff("second",nextTimeGenerated,TimeGenerated)

Stage 12: where

| where SourceIP == nextSourceIP and DestinationIP == nextDestIP and DestinationPort == nextDestPort

Stage 13: where

| where TimeDeltainSeconds > TimeDeltaThresholdInSeconds

Stage 14: summarize

| summarize count(), StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), sum(ReceivedBytes), sum(SentBytes), makelist(TimeDeltainSeconds), makeset(DeviceAction) by SourceIP, DestinationIP, DestinationPort

The stages below score time-series anomalies (make-series, series_decompose_anomalies).

Stage 15: extend

| extend series_stats(list_TimeDeltainSeconds), outliers=series_outliers(list_TimeDeltainSeconds)

Stage 16: mv-expand

| mvexpand list_TimeDeltainSeconds to typeof(double), outliers to typeof(double)

Stage 17: extend

| extend list_TimeDeltainSeconds_normalized=iff(outliers > 1.5 or outliers < -1.5, series_stats_list_TimeDeltainSeconds_avg , list_TimeDeltainSeconds)
list_TimeDeltainSeconds_normalized =
if(outliers > 1.5 or outliers < -1.5)series_stats_list_TimeDeltainSeconds_avg
elselist_TimeDeltainSeconds

Stage 18: summarize

| summarize BeaconCount=count(), makelist(list_TimeDeltainSeconds), list_TimeDeltainSeconds_normalized=makelist(list_TimeDeltainSeconds_normalized), makeset(set_DeviceAction) by StartTime, EndTime, SourceIP, DestinationIP, DestinationPort, sum_ReceivedBytes, sum_SentBytes
Threshold
gt 4

Stage 19: extend

| extend series_stats(list_TimeDeltainSeconds_normalized)

Stage 20: extend

| extend MaxJitter = (series_stats_list_TimeDeltainSeconds_normalized_avg*JitterTolerance)

Stage 21: where

| where series_stats_list_TimeDeltainSeconds_normalized_stdev < MaxJitter

Stage 22: where

| where BeaconCount > TotalBeaconsThreshold and (sum_SentBytes > 0 or sum_ReceivedBytes > 0)

Stage 23: project

| project StartTime, EndTime, SourceIP, DestinationIP, DestinationPort, BeaconCount, TimeDeltasInSeconds=list_list_TimeDeltainSeconds, Periodicity=series_stats_list_TimeDeltainSeconds_normalized_avg, ReceivedBytes=sum_ReceivedBytes, SentBytes=sum_SentBytes, Actions=set_set_DeviceAction

Stage 24: where

| where Periodicity >= (10*TimeDeltaThresholdInSeconds)

Exclusions

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

FieldKindExcluded values
DeviceActioninclient-rst, close, deny, server-rst
DestinationIPcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8

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
BeaconCountgt
  • 4 transforms: cased
DestinationIPeq
  • nextDestIP transforms: cased
DestinationIPis_not_null
  • (no value, null check)
DestinationPorteq
  • nextDestPort transforms: cased
DestinationPortne
  • 161 transforms: cased
DeviceVendoreq
  • Fortinet transforms: cased
Periodicityge
  • 600 transforms: cased
SourceIPeq
  • nextSourceIP transforms: cased
SourceIPis_not_null
  • (no value, null check)
SourceIPne
  • 0.0.0.0 transforms: cased
TimeDeltainSecondsgt
  • 60 transforms: cased
series_stats_list_TimeDeltainSeconds_normalized_stdevlt
  • MaxJitter transforms: cased
sum_ReceivedBytesgt
  • 0 transforms: cased
sum_SentBytesgt
  • 0 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
Actionsproject
BeaconCountproject
DestinationIPproject
DestinationPortproject
EndTimeproject
Periodicityproject
ReceivedBytesproject
SentBytesproject
SourceIPproject
StartTimeproject
TimeDeltasInSecondsproject