Detection rules › Kusto

Detect Possible Teams BEC Attack by High Teams Recipients

Time window
1d
Group by
AccountObjectId, SenderEmailAddress, SenderObjectId
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

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

TacticTechniques
Initial AccessT1566 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.

FieldKindValues
ThreadTypeeq
  • chat 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
ChatSetsummarize
SenderEmailAddresssummarize
ChatAmmountextend