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                    strings.istarts_with(.name, "Microsoft")
 38                    and .confidence == "high"
 39                )
 40        )
 41      )
 42    )
 43  )
 44  
 45  // body contains expire, expiration, loose, lose 
 46  and (
 47    regex.icontains(body.current_thread.text,
 48                    '(expir(e)?(ation|s)|\blo(o)?se\b|(?:offices?|microsoft).365|re.{0,3}confirm)|due for update'
 49    )
 50    and not strings.icontains(body.current_thread.text, 'link expires in ')
 51  )
 52  and (
 53    // subject or body contains account or access
 54    any([subject.subject, body.current_thread.text],
 55        regex.icontains(., "account|access|your email")
 56    )
 57    // suspicious use of recipients email address
 58    or any(recipients.to,
 59           any([subject.subject, body.current_thread.text],
 60               strings.icontains(strings.replace_confusables(.),
 61                                 ..email.local_part
 62               )
 63               or strings.icontains(strings.replace_confusables(.), ..email.email)
 64           )
 65    )
 66  )
 67  
 68  // subject or body must contains password
 69  and any([
 70            strings.replace_confusables(subject.subject),
 71            strings.replace_confusables(body.current_thread.text)
 72          ],
 73          regex.icontains(., '\bpassword\b', '\bmulti.?factor\b')
 74  )
 75  and (
 76    any(ml.nlu_classifier(strings.replace_confusables(body.current_thread.text)).intents,
 77        .name == "cred_theft" and .confidence == "high"
 78    )
 79    or 3 of (
 80        strings.icontains(strings.replace_confusables(body.current_thread.text), 'password'),
 81        regex.icontains(strings.replace_confusables(body.current_thread.text), 'password\s*(?:\w+\s+){0,4}\s*reconfirm'),
 82        regex.icontains(strings.replace_confusables(body.current_thread.text), 'keep\s*(?:\w+\s+){0,4}\s*password'),
 83        strings.icontains(strings.replace_confusables(body.current_thread.text), 'password is due'),
 84        strings.icontains(strings.replace_confusables(body.current_thread.text), 'expiration'),
 85        strings.icontains(strings.replace_confusables(body.current_thread.text), 'expire'),
 86        strings.icontains(strings.replace_confusables(body.current_thread.text), 'expiring'),
 87        strings.icontains(strings.replace_confusables(body.current_thread.text), 'kindly'),
 88        strings.icontains(strings.replace_confusables(body.current_thread.text), 'renew'),
 89        strings.icontains(strings.replace_confusables(body.current_thread.text), 'review'),
 90        strings.icontains(strings.replace_confusables(body.current_thread.text), 'click below'),
 91        strings.icontains(strings.replace_confusables(body.current_thread.text), 'kicked out'),
 92        strings.icontains(strings.replace_confusables(body.current_thread.text), 'required now'),
 93        strings.icontains(strings.replace_confusables(body.current_thread.text), 'immediate action'),
 94        strings.icontains(strings.replace_confusables(body.current_thread.text), 'security update'),
 95        strings.icontains(strings.replace_confusables(body.current_thread.text), 'blocked'),
 96        strings.icontains(strings.replace_confusables(body.current_thread.text), 'locked'),
 97        strings.icontains(strings.replace_confusables(body.current_thread.text), 'interruption'),
 98        strings.icontains(strings.replace_confusables(body.current_thread.text), 'action is not taken'),
 99  
100    )
101  )
102  
103  // body length between 200 and 2000
104  and (
105    200 < length(body.current_thread.text) < 2000
106  
107    // excessive whitespace
108    or (
109      regex.icontains(body.html.raw, '(?:(?:<br\s*/?>\s*){20,}|\n{20,})')
110      or regex.icontains(body.html.raw, '(?:<p[^>]*>\s*<br\s*/?>\s*</p>\s*){30,}')
111      or regex.icontains(body.html.raw,
112                         '(?:<p class=".*?"><span style=".*?"><o:p>&nbsp;</o:p></span></p>\s*){30,}'
113      )
114      or regex.icontains(body.html.raw, '(?:<p>\s*&nbsp;\s*</p>\s*){7,}')
115      or regex.icontains(body.html.raw, '(?:<p>\s*&nbsp;\s*</p>\s*<br>\s*){7,}')
116      or regex.icontains(body.html.raw,
117                         '(?:<p[^>]*>\s*&nbsp;\s*<br>\s*</p>\s*){5,}'
118      )
119      or regex.icontains(body.html.raw, '(?:<p[^>]*>&nbsp;</p>\s*){7,}')
120    )
121  )
122  
123  // a body link does not match the sender domain
124  and any(body.links,
125          .href_url.domain.root_domain != sender.email.domain.root_domain
126          and .href_url.domain.root_domain not in $org_domains
127  )
128  
129  // and no false positives and not solicited
130  and (
131    not profile.by_sender().any_false_positives
132    and not profile.by_sender().solicited
133  )
134  
135  // not a reply
136  and (
137    length(headers.references) == 0
138    or not any(headers.hops, any(.fields, strings.ilike(.name, "In-Reply-To")))
139  )
140  
141  // negate highly trusted sender domains unless they fail DMARC authentication
142  and (
143    (
144      sender.email.domain.root_domain in $high_trust_sender_root_domains
145      and (
146        any(distinct(headers.hops, .authentication_results.dmarc is not null),
147            strings.ilike(.authentication_results.dmarc, "*fail")
148        )
149      )
150    )
151    or sender.email.domain.root_domain not in $high_trust_sender_root_domains
152  )  
153attack_types:
154  - "Credential Phishing"
155tactics_and_techniques:
156  - "Social engineering"
157detection_methods:
158  - "Content analysis"
159  - "Natural Language Understanding"
160  - "Sender analysis"
161id: "5d9c3a75-5f57-5d0c-a07f-0f300bbde076"
to-top