Credential phishing: Fake password expiration from new and unsolicited sender
This rule looks for password expiration verbiage in the subject and body. Requiring between 1 - 9 links, a short body, and NLU in addition to statically specified term anchors. High trust senders are also negated.
Sublime rule (View on GitHub)
1name: "Credential phishing: Fake password expiration from new and unsolicited sender"
2description: "This rule looks for password expiration verbiage in the subject and body. Requiring between 1 - 9 links, a short body, and NLU in addition to statically specified term anchors. High trust senders are also negated."
3type: "rule"
4severity: "medium"
5source: |
6 type.inbound
7
8 // few links which are not in $org_domains
9 and 0 < length(filter(body.links, .href_url.domain.domain not in $org_domains)) <= 10
10
11 // no attachments or suspicious attachment
12 and (
13 length(attachments) == 0
14 or any(filter(attachments, .file_type in ("pdf", "doc", "docx")),
15 any(file.explode(.),
16 .scan.entropy.entropy > 7 and length(.scan.ocr.raw) < 20
17 )
18 )
19 // or there are duplicate pdfs in name
20 or (
21 length(filter(attachments, .file_type == "pdf")) > length(distinct(filter(attachments,
22 .file_type == "pdf"
23 ),
24 .file_name
25 )
26 )
27 or
28 // all PDFs are the same MD5
29 length(distinct(filter(attachments, .file_type == "pdf"), .md5)) == 1
30 // the attachments are all images and not too many attachments
31 or (
32 all(attachments, .file_type in $file_types_images)
33 and 0 < length(attachments) < 6
34 // any of those attachments are Microsoft branded
35 and any(attachments,
36 any(ml.logo_detect(.).brands,
37 (
38 strings.istarts_with(.name, "Microsoft")
39 or .name == "Generic Webmail"
40 )
41 and .confidence == "high"
42 )
43 // it's just an icon
44 or length(beta.ocr(.).text) < 20
45 or beta.parse_exif(.).image_height == beta.parse_exif(.).image_width
46 )
47 )
48 )
49 )
50
51 // body contains expire, expiration, loose, lose
52 and (
53 regex.icontains(body.current_thread.text,
54 '(expir(e(d|s)?|ation|s)?|\blo(o)?se\b|(?:offices?|microsoft).365|re.{0,3}confirm)|due for update'
55 )
56 and not strings.icontains(body.current_thread.text, 'link expires in ')
57 )
58 and (
59 // subject or body contains account or access
60 any([subject.subject, body.current_thread.text],
61 regex.icontains(., "account|access|your email|mailbox")
62 )
63 // suspicious use of recipients email address
64 or any(recipients.to,
65 any([subject.subject, body.current_thread.text],
66 strings.icontains(strings.replace_confusables(.),
67 ..email.local_part
68 )
69 or strings.icontains(strings.replace_confusables(.), ..email.email)
70 )
71 )
72 )
73
74 // subject or body must contains password
75 and any([
76 strings.replace_confusables(subject.subject),
77 strings.replace_confusables(body.current_thread.text)
78 ],
79 regex.icontains(., '\bpassword\b', '\bmulti.?factor\b')
80 )
81 and (
82 any(ml.nlu_classifier(strings.replace_confusables(body.current_thread.text)).intents,
83 .name == "cred_theft" and .confidence == "high"
84 )
85 or 3 of (
86 strings.icontains(strings.replace_confusables(body.current_thread.text),
87 'password'
88 ),
89 regex.icontains(strings.replace_confusables(body.current_thread.text),
90 'password\s*(?:\w+\s+){0,4}\s*reconfirm'
91 ),
92 regex.icontains(strings.replace_confusables(body.current_thread.text),
93 'keep\s*(?:\w+\s+){0,4}\s*password'
94 ),
95 strings.icontains(strings.replace_confusables(body.current_thread.text),
96 'password is due'
97 ),
98 strings.icontains(strings.replace_confusables(body.current_thread.text),
99 'expiration'
100 ),
101 strings.icontains(strings.replace_confusables(body.current_thread.text),
102 'expire'
103 ),
104 strings.icontains(strings.replace_confusables(body.current_thread.text),
105 'expiring'
106 ),
107 strings.icontains(strings.replace_confusables(body.current_thread.text),
108 'kindly'
109 ),
110 strings.icontains(strings.replace_confusables(body.current_thread.text),
111 'renew'
112 ),
113 strings.icontains(strings.replace_confusables(body.current_thread.text),
114 'review'
115 ),
116 strings.icontains(strings.replace_confusables(body.current_thread.text),
117 'click below'
118 ),
119 strings.icontains(strings.replace_confusables(body.current_thread.text),
120 'kicked out'
121 ),
122 strings.icontains(strings.replace_confusables(body.current_thread.text),
123 'required now'
124 ),
125 strings.icontains(strings.replace_confusables(body.current_thread.text),
126 'immediate action'
127 ),
128 strings.icontains(strings.replace_confusables(body.current_thread.text),
129 'security update'
130 ),
131 strings.icontains(strings.replace_confusables(body.current_thread.text),
132 'blocked'
133 ),
134 strings.icontains(strings.replace_confusables(body.current_thread.text),
135 'locked'
136 ),
137 strings.icontains(strings.replace_confusables(body.current_thread.text),
138 'interruption'
139 ),
140 strings.icontains(strings.replace_confusables(body.current_thread.text),
141 'action is not taken'
142 ),
143 )
144 )
145
146 // body length between 200 and 2000
147 and (
148 200 < length(body.current_thread.text) < 2000
149
150 // excessive whitespace
151 or (
152 regex.icontains(body.html.raw, '(?:(?:<br\s*/?>\s*){20,}|\n{20,})')
153 or regex.icontains(body.html.raw, '(?:<p[^>]*>\s*<br\s*/?>\s*</p>\s*){30,}')
154 or regex.icontains(body.html.raw,
155 '(?:<p class=".*?"><span style=".*?"><o:p> </o:p></span></p>\s*){30,}'
156 )
157 or regex.icontains(body.html.raw, '(?:<p>\s* \s*</p>\s*){7,}')
158 or regex.icontains(body.html.raw, '(?:<p>\s* \s*</p>\s*<br>\s*){7,}')
159 or regex.icontains(body.html.raw,
160 '(?:<p[^>]*>\s* \s*<br>\s*</p>\s*){5,}'
161 )
162 or regex.icontains(body.html.raw, '(?:<p[^>]*> </p>\s*){7,}')
163 )
164 )
165
166 // a body link does not match the sender domain
167 and any(body.links,
168 (
169 .href_url.domain.root_domain != sender.email.domain.root_domain
170 // or link URL contains an IPv4 address
171 or (
172 .href_url.domain.root_domain is null
173 and regex.icontains(.href_url.url, '(\d{1,3}.){3}\d{1,3}')
174 )
175 )
176 and .href_url.domain.root_domain not in $org_domains
177 )
178
179 // and no false positives and not solicited
180 and (
181 (
182 not profile.by_sender_email().any_messages_benign
183 and not profile.by_sender_email().solicited
184 )
185 or (
186 sender.email.domain.domain in $org_domains
187 and not headers.auth_summary.spf.pass
188 )
189 )
190
191 // not a reply
192 and (
193 length(headers.references) == 0
194 or not any(headers.hops, any(.fields, strings.ilike(.name, "In-Reply-To")))
195 )
196
197 // negate highly trusted sender domains unless they fail DMARC authentication
198 and (
199 (
200 sender.email.domain.root_domain in $high_trust_sender_root_domains
201 and (
202 any(distinct(headers.hops, .authentication_results.dmarc is not null),
203 strings.ilike(.authentication_results.dmarc, "*fail")
204 )
205 )
206 )
207 or sender.email.domain.root_domain not in $high_trust_sender_root_domains
208 )
209attack_types:
210 - "Credential Phishing"
211tactics_and_techniques:
212 - "Social engineering"
213detection_methods:
214 - "Content analysis"
215 - "Natural Language Understanding"
216 - "Sender analysis"
217id: "5d9c3a75-5f57-5d0c-a07f-0f300bbde076"