Detection rules › Kusto

Palo Alto - potential beaconing detected

Status
available
Severity
low
Time window
1h
Group by
FirewallName_s, Message, TimeDeltainSeconds
Source
github.com/Azure/Azure-Sentinel

'Identifies beaconing patterns from Palo Alto Network traffic logs based on recurrent timedelta patterns. The query leverages various KQL functions to calculate time deltas and then compares it with total events observed in a day to find percentage of beaconing. This outbound beaconing pattern to untrusted public networks should be investigated for any malware callbacks or data exfiltration attempts. Reference Blog: http://www.austintaylor.io/detect/beaconing/intrusion/detection/system/command/control/flare/elastic/stack/2017/06/10/detect-beaconing-with-flare-elasticsearch-and-intrusion-detection-systems/ https://techcommunity.microsoft.com/t5/microsoft-sentinel-blog/detect-network-beaconing-via-intra-request-time-delta-patterns/ba-p/779586'

MITRE ATT&CK coverage

Rule body kusto

id: f0be259a-34ac-4946-aa15-ca2b115d5feb
name: Palo Alto - potential beaconing detected
description: |
  'Identifies beaconing patterns from Palo Alto Network traffic logs based on recurrent timedelta patterns.
  The query leverages various KQL functions to calculate time deltas and then compares it with total events observed in a day to find percentage of beaconing.
  This outbound beaconing pattern to untrusted public networks should be investigated for any malware callbacks or data exfiltration attempts.
  Reference Blog:
  http://www.austintaylor.io/detect/beaconing/intrusion/detection/system/command/control/flare/elastic/stack/2017/06/10/detect-beaconing-with-flare-elasticsearch-and-intrusion-detection-systems/
  https://techcommunity.microsoft.com/t5/microsoft-sentinel-blog/detect-network-beaconing-via-intra-request-time-delta-patterns/ba-p/779586'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AzureCloudNGFWByPaloAltoNetworks
    dataTypes:
      - fluentbit_CL
queryFrequency: 1d
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
  - T1571
query: |
  let starttime = 2d;
  let endtime = 1d;
  let TimeDeltaThreshold = 25;
  let TotalEventsThreshold = 30;
  let MostFrequentTimeDeltaThreshold = 25;
  let PercentBeaconThreshold = 80;
  fluentbit_CL
  | where ident_s == "TRAFFIC"
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
  | where ipv4_is_private(tostring(parse_json(Message).dst_ip)) == false
  | project TimeGenerated, FirewallName_s, parse_json(Message).src_ip, parse_json(Message).sport, parse_json(Message).dst_ip, parse_json(Message).dport,Message
  | sort by tostring(parse_json(Message).src_ip) asc, TimeGenerated asc, tostring(parse_json(Message).dst_ip) asc, tostring(parse_json(Message).dport) asc
  | extend src_ip=tostring(parse_json(Message).src_ip)
  | serialize
  | extend nextTimeGenerated = next(TimeGenerated, 1), nextSourceIP = next(src_ip, 1)
  | extend TimeDeltainSeconds = datetime_diff('second', nextTimeGenerated, TimeGenerated)
  | where parse_json(Message).src_ip == nextSourceIP
  | where TimeDeltainSeconds > TimeDeltaThreshold
  | summarize count() by TimeDeltainSeconds, bin(TimeGenerated, 1h), FirewallName_s, tostring(parse_json(Message).src_ip), tostring(parse_json(Message).dst_ip), tostring(parse_json(Message).dport),Message
  | summarize (MostFrequentTimeDeltaCount, MostFrequentTimeDeltainSeconds) = arg_max(count_, TimeDeltainSeconds), TotalEvents=sum(count_)
  by bin(TimeGenerated, 1h), FirewallName_s, tostring(parse_json(Message).src_ip), tostring(parse_json(Message).dst_ip), tostring(parse_json(Message).dport),Message
  | where TotalEvents > TotalEventsThreshold and MostFrequentTimeDeltaCount > MostFrequentTimeDeltaThreshold
  | extend BeaconPercent = MostFrequentTimeDeltaCount/toreal(TotalEvents) * 100
  | where BeaconPercent > PercentBeaconThreshold
  | extend IPAddress = tostring(parse_json(Message).dst_ip)
  | extend HostName = tostring(split(FirewallName_s, ".")[0]), DomainIndex = toint(indexof(FirewallName_s, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(FirewallName_s, DomainIndex + 1), FirewallName_s)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: FirewallName_s
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 2d;
let endtime = 1d;
let TimeDeltaThreshold = 25;
let TotalEventsThreshold = 30;
let MostFrequentTimeDeltaThreshold = 25;
let PercentBeaconThreshold = 80;

Stage 1: source

fluentbit_CL

Stage 2: where

| where ident_s == "TRAFFIC"

Stage 3: where

| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))

Stage 4: where

| where ipv4_is_private(tostring(parse_json(Message).dst_ip)) == false

Stage 5: project

| project TimeGenerated, FirewallName_s, parse_json(Message).src_ip, parse_json(Message).sport, parse_json(Message).dst_ip, parse_json(Message).dport,Message

Stage 6: sort

| sort by tostring(parse_json(Message).src_ip) asc, TimeGenerated asc, tostring(parse_json(Message).dst_ip) asc, tostring(parse_json(Message).dport) asc

Stage 7: extend

| extend src_ip=tostring(parse_json(Message).src_ip)

Stage 8: kusto:serialize

| serialize

Stage 9: extend

| extend nextTimeGenerated = next(TimeGenerated, 1), nextSourceIP = next(src_ip, 1)

Stage 10: extend

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

Stage 11: where

| where parse_json(Message).src_ip == nextSourceIP

Stage 12: where

| where TimeDeltainSeconds > TimeDeltaThreshold

Stage 13: summarize

| summarize count() by TimeDeltainSeconds, bin(TimeGenerated, 1h), FirewallName_s, tostring(parse_json(Message).src_ip), tostring(parse_json(Message).dst_ip), tostring(parse_json(Message).dport),Message

Stage 14: summarize

| summarize (MostFrequentTimeDeltaCount, MostFrequentTimeDeltainSeconds) = arg_max(count_, TimeDeltainSeconds), TotalEvents=sum(count_)
by bin(TimeGenerated, 1h), FirewallName_s, tostring(parse_json(Message).src_ip), tostring(parse_json(Message).dst_ip), tostring(parse_json(Message).dport),Message
Threshold
gt 30

Stage 15: where

| where TotalEvents > TotalEventsThreshold and MostFrequentTimeDeltaCount > MostFrequentTimeDeltaThreshold

Stage 16: extend

| extend BeaconPercent = MostFrequentTimeDeltaCount/toreal(TotalEvents) * 100

Stage 17: where

| where BeaconPercent > PercentBeaconThreshold

Stage 18: extend (3 consecutive steps)

| extend IPAddress = tostring(parse_json(Message).dst_ip)
| extend HostName = tostring(split(FirewallName_s, ".")[0]), DomainIndex = toint(indexof(FirewallName_s, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(FirewallName_s, DomainIndex + 1), FirewallName_s)

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
BeaconPercentgt
  • 80 transforms: cased
MostFrequentTimeDeltaCountgt
  • 25 transforms: cased
TimeDeltainSecondsgt
  • 25 transforms: cased
TotalEventsgt
  • 30 transforms: cased
ident_seq
  • TRAFFIC transforms: cased
src_ipeq
  • nextSourceIP 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
FirewallName_ssummarize
Messagesummarize
MostFrequentTimeDeltaCountsummarize
TotalEventssummarize
BeaconPercentextend
IPAddressextend
DomainIndexextend
HostNameextend
HostNameDomainextend