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