Credential phishing: Fake password expiration from new and unsolicited sender

This rule looks for password expiration verbiage in the subject and body. Requiring between 1 - 9 links, a short body, and NLU in addition to statically specified term anchors. High trust senders are also negated.

Sublime rule (View on GitHub)

  1name: "Credential phishing: Fake password expiration from new and unsolicited sender"
  2description: "This rule looks for password expiration verbiage in the subject and body. Requiring between 1 - 9 links, a short body, and NLU in addition to statically specified term anchors. High trust senders are also negated."
  3type: "rule"
  4severity: "medium"
  5source: |
  6  type.inbound
  7  
  8  // few links which are not in $org_domains
  9  and 0 < length(filter(body.links, .href_url.domain.domain not in $org_domains)) <= 10
 10  
 11  // no attachments or suspicious attachment
 12  and (
 13    length(attachments) == 0
 14    or any(filter(attachments, .file_type in ("pdf", "doc", "docx")),
 15           any(file.explode(.),
 16               .scan.entropy.entropy > 7 and length(.scan.ocr.raw) < 20
 17           )
 18    )
 19    // or there are duplicate pdfs in name 
 20    or (
 21      length(filter(attachments, .file_type == "pdf")) > length(distinct(filter(attachments,
 22                                                                                .file_type == "pdf"
 23                                                                         ),
 24                                                                         .file_name
 25                                                                )
 26      )
 27      or 
 28      // all PDFs are the same MD5
 29      length(distinct(filter(attachments, .file_type == "pdf"), .md5)) == 1
 30      // the attachments are all images and not too many attachments
 31      or (
 32        all(attachments, .file_type in $file_types_images)
 33        and 0 < length(attachments) < 6
 34        // any of those attachments are Microsoft branded
 35        and any(attachments,
 36                any(ml.logo_detect(.).brands,
 37                    (
 38                      strings.istarts_with(.name, "Microsoft")
 39                      or .name == "Generic Webmail"
 40                    )
 41                    and .confidence == "high"
 42                )
 43                // it's just an icon
 44                or length(beta.ocr(.).text) < 20
 45                or beta.parse_exif(.).image_height == beta.parse_exif(.).image_width
 46        )
 47      )
 48    )
 49  )
 50  
 51  // body contains expire, expiration, loose, lose 
 52  and (
 53    regex.icontains(body.current_thread.text,
 54                    '(expir(e(d|s)?|ation|s)?|\blo(o)?se\b|(?:offices?|microsoft).365|re.{0,3}confirm)|due for update'
 55    )
 56    and not strings.icontains(body.current_thread.text, 'link expires in ')
 57  )
 58  and (
 59    // subject or body contains account or access
 60    any([subject.subject, body.current_thread.text],
 61        regex.icontains(., "account|access|your email|mailbox")
 62    )
 63    // suspicious use of recipients email address
 64    or any(recipients.to,
 65           any([subject.subject, body.current_thread.text],
 66               strings.icontains(strings.replace_confusables(.),
 67                                 ..email.local_part
 68               )
 69               or strings.icontains(strings.replace_confusables(.), ..email.email)
 70           )
 71    )
 72  )
 73  
 74  // subject or body must contains password
 75  and any([
 76            strings.replace_confusables(subject.subject),
 77            strings.replace_confusables(body.current_thread.text)
 78          ],
 79          regex.icontains(., '\bpassword\b', '\bmulti.?factor\b')
 80  )
 81  and (
 82    any(ml.nlu_classifier(strings.replace_confusables(body.current_thread.text)).intents,
 83        .name == "cred_theft" and .confidence == "high"
 84    )
 85    or 3 of (
 86      strings.icontains(strings.replace_confusables(body.current_thread.text),
 87                        'password'
 88      ),
 89      regex.icontains(strings.replace_confusables(body.current_thread.text),
 90                      'password\s*(?:\w+\s+){0,4}\s*reconfirm'
 91      ),
 92      regex.icontains(strings.replace_confusables(body.current_thread.text),
 93                      'keep\s*(?:\w+\s+){0,4}\s*password'
 94      ),
 95      strings.icontains(strings.replace_confusables(body.current_thread.text),
 96                        'password is due'
 97      ),
 98      strings.icontains(strings.replace_confusables(body.current_thread.text),
 99                        'expiration'
100      ),
101      strings.icontains(strings.replace_confusables(body.current_thread.text),
102                        'expire'
103      ),
104      strings.icontains(strings.replace_confusables(body.current_thread.text),
105                        'expiring'
106      ),
107      strings.icontains(strings.replace_confusables(body.current_thread.text),
108                        'kindly'
109      ),
110      strings.icontains(strings.replace_confusables(body.current_thread.text),
111                        'renew'
112      ),
113      strings.icontains(strings.replace_confusables(body.current_thread.text),
114                        'review'
115      ),
116      strings.icontains(strings.replace_confusables(body.current_thread.text),
117                        'click below'
118      ),
119      strings.icontains(strings.replace_confusables(body.current_thread.text),
120                        'kicked out'
121      ),
122      strings.icontains(strings.replace_confusables(body.current_thread.text),
123                        'required now'
124      ),
125      strings.icontains(strings.replace_confusables(body.current_thread.text),
126                        'immediate action'
127      ),
128      strings.icontains(strings.replace_confusables(body.current_thread.text),
129                        'security update'
130      ),
131      strings.icontains(strings.replace_confusables(body.current_thread.text),
132                        'blocked'
133      ),
134      strings.icontains(strings.replace_confusables(body.current_thread.text),
135                        'locked'
136      ),
137      strings.icontains(strings.replace_confusables(body.current_thread.text),
138                        'interruption'
139      ),
140      strings.icontains(strings.replace_confusables(body.current_thread.text),
141                        'action is not taken'
142      ),
143    )
144  )
145  
146  // body length between 200 and 2000
147  and (
148    200 < length(body.current_thread.text) < 2000
149  
150    // excessive whitespace
151    or (
152      regex.icontains(body.html.raw, '(?:(?:<br\s*/?>\s*){20,}|\n{20,})')
153      or regex.icontains(body.html.raw, '(?:<p[^>]*>\s*<br\s*/?>\s*</p>\s*){30,}')
154      or regex.icontains(body.html.raw,
155                         '(?:<p class=".*?"><span style=".*?"><o:p>&nbsp;</o:p></span></p>\s*){30,}'
156      )
157      or regex.icontains(body.html.raw, '(?:<p>\s*&nbsp;\s*</p>\s*){7,}')
158      or regex.icontains(body.html.raw, '(?:<p>\s*&nbsp;\s*</p>\s*<br>\s*){7,}')
159      or regex.icontains(body.html.raw,
160                         '(?:<p[^>]*>\s*&nbsp;\s*<br>\s*</p>\s*){5,}'
161      )
162      or regex.icontains(body.html.raw, '(?:<p[^>]*>&nbsp;</p>\s*){7,}')
163    )
164  )
165  
166  // a body link does not match the sender domain
167  and any(body.links,
168          (
169            .href_url.domain.root_domain != sender.email.domain.root_domain
170            // or link URL contains an IPv4 address
171            or (
172              .href_url.domain.root_domain is null
173              and regex.icontains(.href_url.url, '(\d{1,3}.){3}\d{1,3}')
174            )
175          )
176          and .href_url.domain.root_domain not in $org_domains
177  )
178  
179  // and no false positives and not solicited
180  and (
181    (
182      not profile.by_sender_email().any_messages_benign
183      and not profile.by_sender_email().solicited
184    )
185    or (
186      sender.email.domain.domain in $org_domains
187      and not headers.auth_summary.spf.pass
188    )
189  )
190  
191  // not a reply
192  and (
193    length(headers.references) == 0
194    or not any(headers.hops, any(.fields, strings.ilike(.name, "In-Reply-To")))
195  )
196  
197  // negate highly trusted sender domains unless they fail DMARC authentication
198  and (
199    (
200      sender.email.domain.root_domain in $high_trust_sender_root_domains
201      and (
202        any(distinct(headers.hops, .authentication_results.dmarc is not null),
203            strings.ilike(.authentication_results.dmarc, "*fail")
204        )
205      )
206    )
207    or sender.email.domain.root_domain not in $high_trust_sender_root_domains
208  )  
209attack_types:
210  - "Credential Phishing"
211tactics_and_techniques:
212  - "Social engineering"
213detection_methods:
214  - "Content analysis"
215  - "Natural Language Understanding"
216  - "Sender analysis"
217id: "5d9c3a75-5f57-5d0c-a07f-0f300bbde076"
to-top