Medium Severity
Credential Phishing: Fake Password Expiration from New and Unsolicited sender
Description
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.
References
No references.
Sublime Security
Created May 8th, 2024 • Last updated Feb 11th, 2025
Feed Source
Sublime Core Feed
Source
type.inbound
// few links which are not in $org_domains
and 0 < length(filter(body.links, .href_url.domain.domain not in $org_domains)) <= 10
// no attachments or suspicious attachment
and (
length(attachments) == 0
or any(filter(attachments, .file_type in ("pdf", "doc", "docx")),
any(file.explode(.),
.scan.entropy.entropy > 7 and length(.scan.ocr.raw) < 20
)
)
// or there are duplicate pdfs in name
or (
length(filter(attachments, .file_type == "pdf")) > length(distinct(filter(attachments,
.file_type == "pdf"
),
.file_name
)
)
or
// all PDFs are the same MD5
length(distinct(filter(attachments, .file_type == "pdf"), .md5)) == 1
// the attachments are all images and not too many attachments
or (
all(attachments, .file_type in $file_types_images)
and 0 < length(attachments) < 6
// any of those attachments are Microsoft branded
and any(attachments,
any(ml.logo_detect(.).brands,
strings.istarts_with(.name, "Microsoft")
and .confidence == "high"
)
)
)
)
)
// body contains expire, expiration, loose, lose
and (
regex.icontains(body.current_thread.text,
'(expir(e)?(ation|s)|\blo(o)?se\b|(?:offices?|microsoft).365|re.{0,3}confirm)|due for update'
)
and not strings.icontains(body.current_thread.text, 'link expires in ')
)
and (
// subject or body contains account or access
any([subject.subject, body.current_thread.text],
regex.icontains(., "account|access|your email")
)
// suspicious use of recipients email address
or any(recipients.to,
any([subject.subject, body.current_thread.text],
strings.icontains(strings.replace_confusables(.),
..email.local_part
)
or strings.icontains(strings.replace_confusables(.), ..email.email)
)
)
)
// subject or body must contains password
and any([
strings.replace_confusables(subject.subject),
strings.replace_confusables(body.current_thread.text)
],
regex.icontains(., '\bpassword\b', '\bmulti.?factor\b')
)
and (
any(ml.nlu_classifier(strings.replace_confusables(body.current_thread.text)).intents,
.name == "cred_theft" and .confidence == "high"
)
or 3 of (
strings.icontains(strings.replace_confusables(body.current_thread.text), 'password'),
regex.icontains(strings.replace_confusables(body.current_thread.text), 'password\s*(?:\w+\s+){0,4}\s*reconfirm'),
regex.icontains(strings.replace_confusables(body.current_thread.text), 'keep\s*(?:\w+\s+){0,4}\s*password'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'password is due'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'expiration'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'expire'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'expiring'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'kindly'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'renew'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'review'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'click below'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'kicked out'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'required now'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'immediate action'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'security update'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'blocked'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'locked'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'interruption'),
strings.icontains(strings.replace_confusables(body.current_thread.text), 'action is not taken'),
)
)
// body length between 200 and 2000
and (
200 < length(body.current_thread.text) < 2000
// excessive whitespace
or (
regex.icontains(body.html.raw, '(?:(?:<br\s*/?>\s*){20,}|\n{20,})')
or regex.icontains(body.html.raw, '(?:<p[^>]*>\s*<br\s*/?>\s*</p>\s*){30,}')
or regex.icontains(body.html.raw,
'(?:<p class=".*?"><span style=".*?"><o:p> </o:p></span></p>\s*){30,}'
)
or regex.icontains(body.html.raw, '(?:<p>\s* \s*</p>\s*){7,}')
or regex.icontains(body.html.raw, '(?:<p>\s* \s*</p>\s*<br>\s*){7,}')
or regex.icontains(body.html.raw,
'(?:<p[^>]*>\s* \s*<br>\s*</p>\s*){5,}'
)
or regex.icontains(body.html.raw, '(?:<p[^>]*> </p>\s*){7,}')
)
)
// a body link does not match the sender domain
and any(body.links,
.href_url.domain.root_domain != sender.email.domain.root_domain
and .href_url.domain.root_domain not in $org_domains
)
// and no false positives and not solicited
and (
not profile.by_sender().any_false_positives
and not profile.by_sender().solicited
)
// not a reply
and (
length(headers.references) == 0
or not any(headers.hops, any(.fields, strings.ilike(.name, "In-Reply-To")))
)
// negate highly trusted sender domains unless they fail DMARC authentication
and (
(
sender.email.domain.root_domain in $high_trust_sender_root_domains
and (
any(distinct(headers.hops, .authentication_results.dmarc is not null),
strings.ilike(.authentication_results.dmarc, "*fail")
)
)
)
or sender.email.domain.root_domain not in $high_trust_sender_root_domains
)
Playground
Test against your own EMLs or sample data.