VIP impersonation: Fake thread with VIPs missing email metadata
Detects inbound messages that weaponize fabricated invoice or payment thread histories to impersonate or involve organizational VIPs. The rule identifies conversations where the two oldest visible threads discuss invoices, payments, or executive engagements, but contain incomplete sender or recipient email addresses — a hallmark of stitched-together or forged thread context. The targeted VIP appears in the fabricated thread history but is conspicuously absent from the current message's recipients, suggesting the VIP's name is being leveraged to establish false legitimacy while routing the live message away from their oversight. Matched messages span executive search retainer invoices, past-due account notices, wire transfer instructions, and advisory billing lures targeting named executives at known organizations.
Sublime rule (View on GitHub)
1name: "VIP impersonation: Fake thread with VIPs missing email metadata"
2description: "Detects inbound messages that weaponize fabricated invoice or payment thread histories to impersonate or involve organizational VIPs. The rule identifies conversations where the two oldest visible threads discuss invoices, payments, or executive engagements, but contain incomplete sender or recipient email addresses — a hallmark of stitched-together or forged thread context. The targeted VIP appears in the fabricated thread history but is conspicuously absent from the current message's recipients, suggesting the VIP's name is being leveraged to establish false legitimacy while routing the live message away from their oversight. Matched messages span executive search retainer invoices, past-due account notices, wire transfer instructions, and advisory billing lures targeting named executives at known organizations."
3type: "rule"
4severity: "high"
5source: |
6 type.inbound
7 // we need at least two threads
8 and length(body.previous_threads) > 1
9 and (
10 // NLU on previous threads
11 ( // oldest thread is invoice/payment related
12 (
13 any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 1].text,
14 subject=body.previous_threads[length(body.previous_threads
15 ) - 1].subject.base
16 ).tags,
17 .name in ("invoice", "payment") and .confidence != "low"
18 )
19 or any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads
20 ) - 1].text,
21 subject=body.previous_threads[length(body.previous_threads
22 ) - 1].subject.base
23 ).topics,
24 .name in ("Request to View Invoice", "Payment Information")
25 and .confidence != "low"
26 )
27 )
28 // second to oldest thread is invoice/payment related
29 and (
30 any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 2].text,
31 subject=body.previous_threads[length(body.previous_threads
32 ) - 2].subject.base
33 ).tags,
34 .name in ("invoice", "payment") and .confidence != "low"
35 )
36 or any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads
37 ) - 2].text,
38 subject=body.previous_threads[length(body.previous_threads
39 ) - 2].subject.base
40 ).topics,
41 .name in ("Request to View Invoice", "Payment Information")
42 and .confidence != "low"
43 )
44 )
45 )
46 // if we don't get NLU but there is a W9 or Inv attached, we can assume it's invoice related
47 or any(attachments, strings.istarts_with(.file_name, 'INV', "W-9", 'W9'))
48 )
49 // each of the two oldest threads have 1 or less recipients (sometimes the "to" header is missing)
50 and length(body.previous_threads[length(body.previous_threads) - 1].recipients.to
51 ) <= 1
52 and length(body.previous_threads[length(body.previous_threads) - 2].recipients.to
53 ) <= 1
54 // at least one party in the two oldest threads is missing an email
55 and (
56 body.previous_threads[length(body.previous_threads) - 1].sender.email.email == ""
57 or body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].email.email == ""
58 or body.previous_threads[length(body.previous_threads) - 2].sender.email.email == ""
59 or body.previous_threads[length(body.previous_threads) - 2].recipients.to[0].email.email == ""
60 )
61 // an org vip sent/received messages
62 and any($org_vips,
63 (
64 body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].display_name == .display_name
65 and body.previous_threads[length(body.previous_threads) - 2].sender.display_name == .display_name
66 )
67 or (
68 body.previous_threads[length(body.previous_threads) - 1].sender.display_name == .display_name
69 and body.previous_threads[length(body.previous_threads) - 2].recipients.to[0].display_name == .display_name
70 )
71 )
72 // the VIP is no longer in the current message
73 and not any(flatten([recipients.to, recipients.cc, recipients.bcc]),
74 any($org_vips,
75 .email != ""
76 and strings.icontains(..email.email, .email)
77 and (
78 (
79 body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].display_name == .display_name
80 and body.previous_threads[length(body.previous_threads) - 2].sender.display_name == .display_name
81 )
82 or (
83 body.previous_threads[length(body.previous_threads) - 1].sender.display_name == .display_name
84 and body.previous_threads[length(body.previous_threads) - 2].recipients.to[0].display_name == .display_name
85 )
86 )
87 )
88 )
89 // not from authorized senders on the org's domain
90 and not (
91 sender.email.domain.domain in $org_domains
92 and coalesce(headers.auth_summary.dmarc.pass, false)
93 )
94attack_types:
95 - "BEC/Fraud"
96tactics_and_techniques:
97 - "Impersonation: VIP"
98 - "Social engineering"
99 - "Spoofing"
100detection_methods:
101 - "Natural Language Understanding"
102 - "Header analysis"
103 - "Content analysis"
104 - "Sender analysis"
105id: "44a68195-1d0d-5a9f-b984-994784f73eee"