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>&nbsp;</o:p></span></p>\s*){30,}'
 87    )
 88    or regex.icontains(body.html.raw, '(<p>&nbsp;</p>\s*){7,}')
 89    or regex.icontains(body.html.raw, '(<p[^>]*>\s*&nbsp;<br>\s*</p>\s*){5,}')
 90    or regex.icontains(body.html.raw, '(<p[^>]*>&nbsp;</p>\s*){7,}')
 91    or strings.count(body.html.raw, "&nbsp;\u{200C}&nbsp;\u{200C}&nbsp") > 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*(?:&nbsp;|\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        // the display text is "go to documents" with the local part of recipient email in body
194        or (
195          strings.icontains(.display_text, "go to documents")
196          and any(recipients.to,
197                  strings.contains(body.current_thread.text, .email.local_part)
198          )
199        )
200  
201        // URL fragment containing recipient's address
202        or .href_url.fragment in map(recipients.to, .email.email)
203    )
204    // one hyperlinked image that's not a tracking pixel
205    or (
206      length(html.xpath(body.html,
207                        "//a//img[(number(@width) > 5 or not(@width)) and (number(@height) > 5 or not(@height))]"
208             ).nodes
209      ) == 1
210      and length(body.current_thread.text) < 500
211    )
212    or (
213      length(attachments) > 0
214      and any(attachments,
215              (
216                regex.icontains(beta.ocr(.).text,
217                                "activate",
218                                "re-auth",
219                                "verify",
220                                "acknowledg",
221                                "(keep|change).{0,20}(active|password|access)",
222                                '((verify|view|click|download|goto|keep|Vιew|release).{0,15}(attachment|current|download|fax|file|document|message|same)s?)',
223                                'use.same.pass',
224                                'validate.{0,15}account',
225                                'recover.{0,15}messages',
226                                '(retry|update).{0,10}payment',
227                                'check activity',
228                                '(listen|play).{0,10}(vm|voice)',
229                                'clarify.{0,20}(deposit|wallet|funds)',
230                                'enter.{0,15}teams',
231                                'Review and sign'
232                )
233              )
234              or (
235                any(file.explode(.),
236                    regex.icontains(.scan.ocr.raw,
237                                    "activate",
238                                    "re-auth",
239                                    "verify",
240                                    "acknowledg",
241                                    "(keep|change).{0,20}(active|password|access)",
242                                    '((verify|view|click|download|goto|keep|Vιew|release).{0,15}(attachment|current|download|fax|file|document|message|same)s?)',
243                                    'use.same.pass',
244                                    'validate.{0,15}account',
245                                    'recover.{0,15}messages',
246                                    '(retry|update).{0,10}payment',
247                                    'check activity',
248                                    '(listen|play).{0,10}(vm|voice)',
249                                    'clarify.{0,20}(deposit|wallet|funds)',
250                                    'enter.{0,15}teams',
251                                    'Review and sign'
252                    )
253                )
254              )
255      )
256    )
257  )
258  // the message is unsolicited and no false positives
259  and (
260    not profile.by_sender_email().solicited
261    or profile.by_sender_email().prevalence == "new"
262    or (
263      profile.by_sender_email().any_messages_malicious_or_spam
264      and not profile.by_sender_email().any_messages_benign
265    )
266    or (
267      profile.by_sender_email().any_messages_malicious_or_spam
268      and profile.by_sender_email().any_messages_benign
269      and (
270        not headers.auth_summary.dmarc.pass or not headers.auth_summary.spf.pass
271      )
272    )
273  )
274  
275  // negate replies/fowards containing legitimate docs
276  and not (length(headers.references) > 0 or headers.in_reply_to is not null)
277  
278  // negate highly trusted sender domains unless they fail DMARC authentication
279  and (
280    (
281      sender.email.domain.root_domain in $high_trust_sender_root_domains
282      and (
283        any(distinct(headers.hops, .authentication_results.dmarc is not null),
284            strings.ilike(.authentication_results.dmarc, "*fail")
285        )
286      )
287    )
288    or sender.email.domain.root_domain not in $high_trust_sender_root_domains
289  )  
290attack_types:
291  - "Credential Phishing"
292tactics_and_techniques:
293  - "Social engineering"
294detection_methods:
295  - "Content analysis"
296  - "Header analysis"
297  - "HTML analysis"
298  - "URL analysis"
299  - "Sender analysis"
300id: "9b68c2d8-951e-5e04-9fa3-2ca67d9226a6"
to-top