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), 'password'),
87 regex.icontains(strings.replace_confusables(body.current_thread.text), 'password\s*(?:\w+\s+){0,4}\s*reconfirm'),
88 regex.icontains(strings.replace_confusables(body.current_thread.text), 'keep\s*(?:\w+\s+){0,4}\s*password'),
89 strings.icontains(strings.replace_confusables(body.current_thread.text), 'password is due'),
90 strings.icontains(strings.replace_confusables(body.current_thread.text), 'expiration'),
91 strings.icontains(strings.replace_confusables(body.current_thread.text), 'expire'),
92 strings.icontains(strings.replace_confusables(body.current_thread.text), 'expiring'),
93 strings.icontains(strings.replace_confusables(body.current_thread.text), 'kindly'),
94 strings.icontains(strings.replace_confusables(body.current_thread.text), 'renew'),
95 strings.icontains(strings.replace_confusables(body.current_thread.text), 'review'),
96 strings.icontains(strings.replace_confusables(body.current_thread.text), 'click below'),
97 strings.icontains(strings.replace_confusables(body.current_thread.text), 'kicked out'),
98 strings.icontains(strings.replace_confusables(body.current_thread.text), 'required now'),
99 strings.icontains(strings.replace_confusables(body.current_thread.text), 'immediate action'),
100 strings.icontains(strings.replace_confusables(body.current_thread.text), 'security update'),
101 strings.icontains(strings.replace_confusables(body.current_thread.text), 'blocked'),
102 strings.icontains(strings.replace_confusables(body.current_thread.text), 'locked'),
103 strings.icontains(strings.replace_confusables(body.current_thread.text), 'interruption'),
104 strings.icontains(strings.replace_confusables(body.current_thread.text), 'action is not taken'),
105
106 )
107 )
108
109 // body length between 200 and 2000
110 and (
111 200 < length(body.current_thread.text) < 2000
112
113 // excessive whitespace
114 or (
115 regex.icontains(body.html.raw, '(?:(?:<br\s*/?>\s*){20,}|\n{20,})')
116 or regex.icontains(body.html.raw, '(?:<p[^>]*>\s*<br\s*/?>\s*</p>\s*){30,}')
117 or regex.icontains(body.html.raw,
118 '(?:<p class=".*?"><span style=".*?"><o:p> </o:p></span></p>\s*){30,}'
119 )
120 or regex.icontains(body.html.raw, '(?:<p>\s* \s*</p>\s*){7,}')
121 or regex.icontains(body.html.raw, '(?:<p>\s* \s*</p>\s*<br>\s*){7,}')
122 or regex.icontains(body.html.raw,
123 '(?:<p[^>]*>\s* \s*<br>\s*</p>\s*){5,}'
124 )
125 or regex.icontains(body.html.raw, '(?:<p[^>]*> </p>\s*){7,}')
126 )
127 )
128
129 // a body link does not match the sender domain
130 and any(body.links,
131 .href_url.domain.root_domain != sender.email.domain.root_domain
132 and .href_url.domain.root_domain not in $org_domains
133 )
134
135 // and no false positives and not solicited
136 and (
137 (
138 not profile.by_sender_email().any_messages_benign
139 and not profile.by_sender_email().solicited
140 )
141 or (
142 sender.email.domain.domain in $org_domains
143 and not headers.auth_summary.spf.pass
144 )
145 )
146
147 // not a reply
148 and (
149 length(headers.references) == 0
150 or not any(headers.hops, any(.fields, strings.ilike(.name, "In-Reply-To")))
151 )
152
153 // negate highly trusted sender domains unless they fail DMARC authentication
154 and (
155 (
156 sender.email.domain.root_domain in $high_trust_sender_root_domains
157 and (
158 any(distinct(headers.hops, .authentication_results.dmarc is not null),
159 strings.ilike(.authentication_results.dmarc, "*fail")
160 )
161 )
162 )
163 or sender.email.domain.root_domain not in $high_trust_sender_root_domains
164 )
165attack_types:
166 - "Credential Phishing"
167tactics_and_techniques:
168 - "Social engineering"
169detection_methods:
170 - "Content analysis"
171 - "Natural Language Understanding"
172 - "Sender analysis"
173id: "5d9c3a75-5f57-5d0c-a07f-0f300bbde076"