Credential phishing: Suspicious e-sign agreement document notification
Detects phishing attempts disguised as e-signature requests, characterized by common document sharing phrases, unusual HTML padding, and suspicious link text.
Sublime rule (View on GitHub)
1name: "Credential phishing: Suspicious e-sign agreement document notification"
2description: "Detects phishing attempts disguised as e-signature requests, characterized by common document sharing phrases, unusual HTML padding, and suspicious link text."
3type: "rule"
4severity: "medium"
5source: |
6 type.inbound
7 and any([subject.subject, sender.display_name],
8 regex.icontains(strings.replace_confusables(.),
9 "D[0o]cuLink",
10 "Agreement",
11 "Access.&.Appr[0o]ved",
12 "Agreement.{0,5}Review",
13 "Attend.and.Review",
14 "action.re?quired",
15 "AuthentiSign",
16 "Completed.File",
17 "D[0o]chsared",
18 "D[0o]cshared",
19 "D[0o]csPoint",
20 "D[0o]cument.Shared",
21 "D[0o]cuCentre",
22 "D[0o]cuCenter",
23 "D[0o]cCenter",
24 "D[0o]csOnline",
25 "D[0o]cSend",
26 "D[0o]cu?Send",
27 "d[0o]csign",
28 "D[0o]cu-eSin",
29 "D[0o]cu-management",
30 "\\beSign",
31 "e\\.sign",
32 "esign.[0o]nline",
33 "[SsZz][lL][GgSs][Nn].*D[0o]c",
34 "e-d[0o]c",
35 "e-signature",
36 "e-Verify Doc",
37 "eSignature",
38 "eSign&Return",
39 "eSign[0o]nline",
40 "Fileshare",
41 "Review.and.C[0o]mplete",
42 "Review.&.Sign",
43 "Sign[0o]nline",
44 "Signature.Request",
45 "Shared.C[0o]mpleted",
46 "Sign.and.Seal",
47 "viaSign",
48 "D[0o]cuSign",
49 "D[0o]csID",
50 "Complete.{0,10}D[0o]cuSign",
51 "Enroll & Sign",
52 "Review and Sign",
53 "Sign(?:Report|Now)",
54 "SignD[0o]c",
55 "D[0o]cxxx",
56 "d[0o]cufile",
57 'E\x{00AD}-\x{00AD}S\x{00AD}i\x{00AD}g\x{00AD}n\x{00AD}&Return',
58 "d[0o]cument.signature",
59 "Electr[0o]nic.?Signature",
60 "Complete: ",
61 "Please (?:Review|Sign)",
62 "^REVIEW$",
63 "requests your signature",
64 "signature on.*contract",
65 "Independent Contract",
66 "Contract.*signature",
67 "add your signature",
68 "signature needed",
69 "attn_task",
70 "DocReq\\b"
71 )
72 or (
73 regex.icontains(strings.replace_confusables(.), "action.re?quired")
74 and not (
75 sender.email.domain.root_domain == "sharepointonline.com"
76 and headers.auth_summary.dmarc.pass
77 and strings.icontains(subject.subject, "asked to edit")
78 )
79 )
80 )
81 and (
82 // unusual repeated patterns in HTML
83 regex.icontains(body.html.raw, '((<br\s*/?>\s*){20,}|\n{20,})')
84 or regex.icontains(body.html.raw, '(<p[^>]*>\s*<br\s*/?>\s*</p>\s*){30,}')
85 or regex.icontains(body.html.raw,
86 '(<p class=".*?"><span style=".*?"><o:p> </o:p></span></p>\s*){30,}'
87 )
88 or regex.icontains(body.html.raw, '(<p> </p>\s*){7,}')
89 or regex.icontains(body.html.raw, '(<p[^>]*>\s* <br>\s*</p>\s*){5,}')
90 or regex.icontains(body.html.raw, '(<p[^>]*> </p>\s*){7,}')
91 or strings.count(body.html.raw, " \u{200C} \u{200C} ") > 50
92 or regex.count(body.html.raw,
93 '<span\s*class\s*=\s*"[^\"]+"\s*>\s*[a-z]\s*<\/span><span\s*class\s*=\s*"[^\"]+"\s*>\s*[a-z]+\s*<\/span>'
94 ) > 50
95 // lookalike docusign
96 or regex.icontains(body.html.raw, '>Docus[1l]gn<')
97 or strings.icontains(body.current_thread.text, 'completed by all parties')
98 or (
99 regex.icontains(body.html.inner_text, 'Document')
100 and length(body.html.inner_text) < 500
101 )
102 // common greetings via email.local_part
103 or any(recipients.to,
104 // use count to ensure the email address is not part of a disclaimer
105 strings.icount(body.current_thread.text, .email.local_part) >
106 // sum allows us to add more logic as needed
107 sum([
108 strings.icount(body.current_thread.text,
109 strings.concat('was sent to ', .email.email)
110 ),
111 strings.icount(body.current_thread.text,
112 strings.concat('intended for ', .email.email)
113 )
114 ]
115 )
116 )
117 // common greetings via mailbox display name
118 or strings.icount(body.current_thread.text, mailbox.display_name) >
119 // sum allows us to add more logic as needed
120 sum([
121 strings.icount(body.current_thread.text,
122 strings.concat('was sent to ', mailbox.display_name)
123 ),
124 strings.icount(body.current_thread.text,
125 strings.concat('intended for ', mailbox.display_name)
126 )
127 ]
128 )
129 // Abnormally high count of mailto links in raw html
130 or regex.count(body.html.raw,
131 'mailto:[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'
132 ) > 50
133
134 // High count of empty elements (padding)
135 or regex.count(body.html.raw,
136 '<(?:p|div|span|td)[^>]*>\s*(?: |\s)*\s*</(?:p|div|span|td)>'
137 ) > 30
138
139 // HR impersonation
140 or strings.ilike(sender.display_name, "HR", "H?R", "*Human Resources*")
141
142 // Sender display name contains a phone number
143 or regex.icontains(sender.display_name,
144 '\+?([ilo0-9]{1}.)?\(?[ilo0-9]{3}?\)?.[ilo0-9]{3}.?[ilo0-9]{4}'
145 )
146 )
147 and (
148 any(body.links,
149
150 // suspicious content within link display_text
151 regex.icontains(strings.replace_confusables(.display_text),
152 "activate",
153 "re-auth",
154 "verify",
155 "acknowledg",
156 "(keep|change).{0,20}(active|password|access)",
157 '((verify|view|click|download|goto|keep|Vιew|release).{0,15}(attachment|current|download|fax|file|document|message|same)s?)',
158 'use.same.pass',
159 'validate.{0,15}account',
160 'recover.{0,15}messages',
161 '(retry|update).{0,10}payment',
162 'check activity',
163 '(listen|play).{0,10}(vm|voice)',
164 'clarify.{0,20}(deposit|wallet|funds)',
165 'enter.{0,15}teams',
166 'Review and sign',
167 'REVIEW.*DOCUMENT',
168 'Open Document',
169 'Sign Now',
170 'complete tasks?'
171 )
172 // check that the display_text is all lowercase
173 or (
174 regex.contains(.display_text,
175 "\\bVIEW",
176 "DOWNLOAD",
177 "CHECK",
178 "KEEP.(SAME|MY)",
179 "VERIFY",
180 "ACCESS\\b",
181 "SIGN\\b",
182 "ENABLE\\b",
183 "RETAIN",
184 "PLAY",
185 "LISTEN",
186 )
187 and regex.match(.display_text, "^[^a-z]*[A-Z][^a-z]*$")
188 )
189
190 // the display text is _exactly_
191 or .display_text in~ ("Open")
192
193 // URL fragment containing recipient's address
194 or .href_url.fragment in map(recipients.to, .email.email)
195 )
196 // one hyperlinked image that's not a tracking pixel
197 or (
198 length(html.xpath(body.html,
199 "//a//img[(number(@width) > 5 or not(@width)) and (number(@height) > 5 or not(@height))]"
200 ).nodes
201 ) == 1
202 and length(body.current_thread.text) < 500
203 )
204 or (
205 length(attachments) > 0
206 and any(attachments,
207 (
208 regex.icontains(beta.ocr(.).text,
209 "activate",
210 "re-auth",
211 "verify",
212 "acknowledg",
213 "(keep|change).{0,20}(active|password|access)",
214 '((verify|view|click|download|goto|keep|Vιew|release).{0,15}(attachment|current|download|fax|file|document|message|same)s?)',
215 'use.same.pass',
216 'validate.{0,15}account',
217 'recover.{0,15}messages',
218 '(retry|update).{0,10}payment',
219 'check activity',
220 '(listen|play).{0,10}(vm|voice)',
221 'clarify.{0,20}(deposit|wallet|funds)',
222 'enter.{0,15}teams',
223 'Review and sign'
224 )
225 )
226 or (
227 any(file.explode(.),
228 regex.icontains(.scan.ocr.raw,
229 "activate",
230 "re-auth",
231 "verify",
232 "acknowledg",
233 "(keep|change).{0,20}(active|password|access)",
234 '((verify|view|click|download|goto|keep|Vιew|release).{0,15}(attachment|current|download|fax|file|document|message|same)s?)',
235 'use.same.pass',
236 'validate.{0,15}account',
237 'recover.{0,15}messages',
238 '(retry|update).{0,10}payment',
239 'check activity',
240 '(listen|play).{0,10}(vm|voice)',
241 'clarify.{0,20}(deposit|wallet|funds)',
242 'enter.{0,15}teams',
243 'Review and sign'
244 )
245 )
246 )
247 )
248 )
249 )
250 // the message is unsolicited and no false positives
251 and (
252 not profile.by_sender_email().solicited
253 or profile.by_sender_email().prevalence == "new"
254 or (
255 profile.by_sender_email().any_messages_malicious_or_spam
256 and not profile.by_sender_email().any_messages_benign
257 )
258 or (
259 profile.by_sender_email().any_messages_malicious_or_spam
260 and profile.by_sender_email().any_messages_benign
261 and (
262 not headers.auth_summary.dmarc.pass or not headers.auth_summary.spf.pass
263 )
264 )
265 )
266
267 // negate replies/fowards containing legitimate docs
268 and not (length(headers.references) > 0 or headers.in_reply_to is not null)
269
270 // negate highly trusted sender domains unless they fail DMARC authentication
271 and (
272 (
273 sender.email.domain.root_domain in $high_trust_sender_root_domains
274 and (
275 any(distinct(headers.hops, .authentication_results.dmarc is not null),
276 strings.ilike(.authentication_results.dmarc, "*fail")
277 )
278 )
279 )
280 or sender.email.domain.root_domain not in $high_trust_sender_root_domains
281 )
282attack_types:
283 - "Credential Phishing"
284tactics_and_techniques:
285 - "Social engineering"
286detection_methods:
287 - "Content analysis"
288 - "Header analysis"
289 - "HTML analysis"
290 - "URL analysis"
291 - "Sender analysis"
292id: "9b68c2d8-951e-5e04-9fa3-2ca67d9226a6"