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