Detection rules › Kusto
Detect Possible Teams BEC Attack by High Teams Recipients
An external sender suddenly increasing the amount of internal users they are sending messages to, can indicate that external user being compromised and used for BEC Attacks. In these kind of attacks compromised accounts are used to send phishing links or attachments to users in business relationships.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1566 Phishing |
References
Rule body yaml
// Possible BEC detection by high teams recipients
let increase_percentage = 200;
let base = (
MessageEvents
| where TimeGenerated > ago(14d)
// Focus on chat messsages
| where ThreadType == "chat"
// Only return external users sending messages
| join kind=leftanti (
IdentityInfo
| where TimeGenerated > ago(14d)
| distinct AccountObjectId
) on $left.SenderObjectId == $right.AccountObjectId
// Make a set of all the chats they are posting to, for every day
| summarize ChatSet = make_set(ThreadId) by SenderEmailAddress, bin(TimeGenerated, 1d)
// Count the amount of chats they posted to for each day
| extend ChatAmmount = array_length(ChatSet)
);
// Get the average of send chats per external user per day
let averageBySender = (
base
| summarize AverageChatsBySender = avg(ChatAmmount) by SenderEmailAddress
);
// Check if the sender dubbled their chats to internal users compared to their baseline
base
| where TimeGenerated > ago(1d)
| join kind=inner averageBySender on SenderEmailAddress
| where ChatAmmount > AverageChatsBySender * (increase_percentage / 100)
Stages and Predicates
Parameters
let increase_percentage = 200;
Let binding: averageBySender
let averageBySender = (
base
| summarize AverageChatsBySender = avg(ChatAmmount) by SenderEmailAddress
);
Derived from base.
The stages below define let base (the rule's main pipeline source).
Stage 1: source
MessageEvents
Stage 2: where
| where TimeGenerated > ago(14d)
Stage 3: where
| where ThreadType == "chat"
Stage 4: join (negated)
| join kind=leftanti (
IdentityInfo
| where TimeGenerated > ago(14d)
| distinct AccountObjectId
) on $left.SenderObjectId == $right.AccountObjectId
Stage 5: summarize
| summarize ChatSet = make_set(ThreadId) by SenderEmailAddress, bin(TimeGenerated, 1d)
Stage 6: extend
| extend ChatAmmount = array_length(ChatSet)
The stages below run on base (the outer pipeline).
Stage 7: where
base
| where TimeGenerated > ago(1d)
Stage 8: join
| join kind=inner averageBySender on SenderEmailAddress
Stage 9: where
| where ChatAmmount > AverageChatsBySender * (increase_percentage / 100)
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 |
|---|---|---|
ThreadType | 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 |
|---|---|
ChatSet | summarize |
SenderEmailAddress | summarize |
ChatAmmount | extend |