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­-­S­i­g­n­&Return",
 55                          "d[0o]cument.signature",
 56                          "Electr[0o]nic.?Signature",
 57                          "Complete: ",
 58                          "Please (?:Review|Sign)",
 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>&nbsp;</o:p></span></p>\s*){30,}'
 82    )
 83    or regex.icontains(body.html.raw, '(<p>&nbsp;</p>\s*){7,}')
 84    or regex.icontains(body.html.raw, '(<p[^>]*>\s*&nbsp;<br>\s*</p>\s*){5,}')
 85    or regex.icontains(body.html.raw, '(<p[^>]*>&nbsp;</p>\s*){7,}')
 86    or strings.count(body.html.raw, '&nbsp;‌&nbsp;‌&nbsp') > 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    // common greetings via mailbox display name
113    or strings.icount(body.current_thread.text, mailbox.display_name) > 
114    // sum allows us to add more logic as needed
115    sum([
116          strings.icount(body.current_thread.text,
117                         strings.concat('was sent to ', mailbox.display_name)
118          ),
119          strings.icount(body.current_thread.text,
120                         strings.concat('intended for ', mailbox.display_name)
121          )
122        ]
123    )
124    // Abnormally high count of mailto links in raw html
125    or regex.count(body.html.raw,
126                   'mailto:[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'
127    ) > 50
128  
129    // High count of empty elements (padding) 
130    or regex.count(body.html.raw,
131                   '<(?:p|div|span|td)[^>]*>\s*(?:&nbsp;|\s)*\s*</(?:p|div|span|td)>'
132    ) > 30
133  
134    // HR impersonation
135    or strings.ilike(sender.display_name, "HR", "H?R", "*Human Resources*")
136  )
137  and (
138    any(body.links,
139  
140        // suspicious content within link display_text
141        regex.icontains(strings.replace_confusables(.display_text),
142                        "activate",
143                        "re-auth",
144                        "verify",
145                        "acknowledg",
146                        "(keep|change).{0,20}(active|password|access)",
147                        '((verify|view|click|download|goto|keep|Vιew|release).{0,15}(attachment|current|download|fax|file|document|message|same)s?)',
148                        'use.same.pass',
149                        'validate.{0,15}account',
150                        'recover.{0,15}messages',
151                        '(retry|update).{0,10}payment',
152                        'check activity',
153                        '(listen|play).{0,10}(vm|voice)',
154                        'clarify.{0,20}(deposit|wallet|funds)',
155                        'enter.{0,15}teams',
156                        'Review and sign',
157                        'REVIEW.*DOCUMENT'
158        )
159        // check that the display_text is all lowercase
160        or (
161          regex.contains(.display_text,
162                         "\\bVIEW",
163                         "DOWNLOAD",
164                         "CHECK",
165                         "KEEP.(SAME|MY)",
166                         "VERIFY",
167                         "ACCESS\\b",
168                         "SIGN\\b",
169                         "ENABLE\\b",
170                         "RETAIN",
171                         "PLAY",
172                         "LISTEN",
173          )
174          and regex.match(.display_text, "^[^a-z]*[A-Z][^a-z]*$")
175        )
176  
177        // the display text is _exactly_
178        or .display_text in~ ("Open")
179  
180        // URL fragment containing recipient's address
181        or .href_url.fragment in map(recipients.to, .email.email)
182    )
183    // one hyperlinked image that's not a tracking pixel
184    or (
185      length(html.xpath(body.html,
186                        "//a//img[(number(@width) > 5 or not(@width)) and (number(@height) > 5 or not(@height))]"
187             ).nodes
188      ) == 1
189      and length(body.current_thread.text) < 500
190    )
191    or (
192      length(attachments) > 0
193      and any(attachments,
194              (
195                regex.icontains(beta.ocr(.).text,
196                                "activate",
197                                "re-auth",
198                                "verify",
199                                "acknowledg",
200                                "(keep|change).{0,20}(active|password|access)",
201                                '((verify|view|click|download|goto|keep|Vιew|release).{0,15}(attachment|current|download|fax|file|document|message|same)s?)',
202                                'use.same.pass',
203                                'validate.{0,15}account',
204                                'recover.{0,15}messages',
205                                '(retry|update).{0,10}payment',
206                                'check activity',
207                                '(listen|play).{0,10}(vm|voice)',
208                                'clarify.{0,20}(deposit|wallet|funds)',
209                                'enter.{0,15}teams',
210                                'Review and sign'
211                )
212              )
213              or (
214                any(file.explode(.),
215                    regex.icontains(.scan.ocr.raw,
216                                    "activate",
217                                    "re-auth",
218                                    "verify",
219                                    "acknowledg",
220                                    "(keep|change).{0,20}(active|password|access)",
221                                    '((verify|view|click|download|goto|keep|Vιew|release).{0,15}(attachment|current|download|fax|file|document|message|same)s?)',
222                                    'use.same.pass',
223                                    'validate.{0,15}account',
224                                    'recover.{0,15}messages',
225                                    '(retry|update).{0,10}payment',
226                                    'check activity',
227                                    '(listen|play).{0,10}(vm|voice)',
228                                    'clarify.{0,20}(deposit|wallet|funds)',
229                                    'enter.{0,15}teams',
230                                    'Review and sign'
231                    )
232                )
233              )
234      )
235    )
236  )
237  and (
238    not profile.by_sender_email().solicited
239    or profile.by_sender_email().prevalence == "new"
240    or (
241      profile.by_sender_email().any_messages_malicious_or_spam
242      and not profile.by_sender_email().any_messages_benign
243    )
244  )
245  and not profile.by_sender_email().any_messages_benign
246  
247  // negate replies/fowards containing legitimate docs
248  and not (
249    length(headers.references) > 0
250    or any(headers.hops, any(.fields, strings.ilike(.name, "In-Reply-To")))
251  )
252  
253  // negate highly trusted sender domains unless they fail DMARC authentication
254  and (
255    (
256      sender.email.domain.root_domain in $high_trust_sender_root_domains
257      and (
258        any(distinct(headers.hops, .authentication_results.dmarc is not null),
259            strings.ilike(.authentication_results.dmarc, "*fail")
260        )
261      )
262    )
263    or sender.email.domain.root_domain not in $high_trust_sender_root_domains
264  )  
265attack_types:
266  - "Credential Phishing"
267tactics_and_techniques:
268  - "Social engineering"
269detection_methods:
270  - "Content analysis"
271  - "Header analysis"
272  - "HTML analysis"
273  - "URL analysis"
274  - "Sender analysis"
275id: "9b68c2d8-951e-5e04-9fa3-2ca67d9226a6"
to-top