Detection rules › Kusto
Palo Alto - potential beaconing detected
'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
| Tactic | Techniques |
|---|---|
| Command & Control | T1071 Application Layer Protocol, T1571 Non-Standard Port |
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
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.
| Field | Kind | Values |
|---|---|---|
BeaconPercent | gt |
|
MostFrequentTimeDeltaCount | gt |
|
TimeDeltainSeconds | gt |
|
TotalEvents | gt |
|
ident_s | eq |
|
src_ip | eq |
|
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.
| Field | Source |
|---|---|
FirewallName_s | summarize |
Message | summarize |
MostFrequentTimeDeltaCount | summarize |
TotalEvents | summarize |
BeaconPercent | extend |
IPAddress | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |