Detection rules › Sublime MQL
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.
Threat classification
Sublime's own taxonomy (not MITRE ATT&CK).
| Category | Values |
|---|---|
| Attack types | Credential Phishing |
| Tactics and techniques | Social engineering |
Event coverage
Rule body MQL
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")
or .name == "Generic Webmail"
)
and .confidence == "high"
)
// it's just an icon
or length(beta.ocr(.).text) < 20
or beta.parse_exif(.).image_height == beta.parse_exif(.).image_width
)
)
)
)
// body contains expire, expiration, loose, lose
and (
regex.icontains(body.current_thread.text,
'(expir(e(d|s)?|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|mailbox")
)
// 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>&nbsp;</o:p></span></p>\s*){30,}'
)
or regex.icontains(body.html.raw, '(?:<p>\s*&nbsp;\s*</p>\s*){7,}')
or regex.icontains(body.html.raw, '(?:<p>\s*&nbsp;\s*</p>\s*<br>\s*){7,}')
or regex.icontains(body.html.raw,
'(?:<p[^>]*>\s*&nbsp;\s*<br>\s*</p>\s*){5,}'
)
or regex.icontains(body.html.raw, '(?:<p[^>]*>&nbsp;</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
// or link URL contains an IPv4 address
or (
.href_url.domain.root_domain is null
and regex.icontains(.href_url.url, '(\d{1,3}.){3}\d{1,3}')
)
)
and .href_url.domain.root_domain not in $org_domains
)
// and no false positives and not solicited
and (
(
not profile.by_sender_email().any_messages_benign
and not profile.by_sender_email().solicited
)
or (
sender.email.domain.domain in $org_domains
and not headers.auth_summary.spf.pass
)
)
// not a reply
and (length(headers.references) == 0 or headers.in_reply_to is null)
// 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
)
Detection logic
Scope: inbound message.
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.
- inbound message
all of:
- length(filter(body.links, .href_url.domain.domain not in $org_domains)) > 0
- length(filter(body.links, .href_url.domain.domain not in $org_domains)) ≤ 10
any of:
- length(attachments) is 0
any of
filter(attachments)where:any of
file.explode(.)where all hold:- .scan.entropy.entropy > 7
- length(.scan.ocr.raw) < 20
any of:
- length(filter(attachments, .file_type == 'pdf')) > length(distinct(filter(attachments, .file_type == 'pdf'), .file_name))
- length(distinct(filter(attachments, .file_type == 'pdf'), .md5)) is 1
all of:
all of
attachmentswhere:- .file_type in $file_types_images
all of:
- length(attachments) > 0
- length(attachments) < 6
any of
attachmentswhere any holds:any of
ml.logo_detect(.).brandswhere all hold:any of:
- .name starts with 'Microsoft'
- .name is 'Generic Webmail'
- .confidence is 'high'
- length(beta.ocr(.).text) < 20
- beta.parse_exif(.).image_height is beta.parse_exif(.).image_width
all of:
- body.current_thread.text matches '(expir(e(d|s)?|ation|s)?|\\blo(o)?se\\b|(?:offices?|microsoft).365|re.{0,3}confirm)|due for update'
not:
- body.current_thread.text contains 'link expires in '
any of:
any of
[subject.subject, body.current_thread.text]where:- . matches 'account|access|your email|mailbox'
any of
recipients.towhere:any of
[subject.subject, body.current_thread.text]where any holds:- strings.icontains(strings.replace_confusables(.))
- strings.icontains(strings.replace_confusables(.))
any of
[strings.replace_confusables(subject.subject), strings.replace_confusables(body.current_thread.text)]where:. matches any of 2 patterns
\bpassword\b\bmulti.?factor\b
any of:
any of
ml.nlu_classifier(strings.replace_confusables(body.current_thread.text)).intentswhere all hold:- .name is 'cred_theft'
- .confidence is 'high'
at least 3 of:
- strings.replace_confusables(body.current_thread.text) contains 'password'
- strings.replace_confusables(body.current_thread.text) matches 'password\\s*(?:\\w+\\s+){0,4}\\s*reconfirm'
- strings.replace_confusables(body.current_thread.text) matches 'keep\\s*(?:\\w+\\s+){0,4}\\s*password'
- strings.replace_confusables(body.current_thread.text) contains 'password is due'
- strings.replace_confusables(body.current_thread.text) contains 'expiration'
- strings.replace_confusables(body.current_thread.text) contains 'expire'
- strings.replace_confusables(body.current_thread.text) contains 'expiring'
- strings.replace_confusables(body.current_thread.text) contains 'kindly'
- strings.replace_confusables(body.current_thread.text) contains 'renew'
- strings.replace_confusables(body.current_thread.text) contains 'review'
- strings.replace_confusables(body.current_thread.text) contains 'click below'
- strings.replace_confusables(body.current_thread.text) contains 'kicked out'
- strings.replace_confusables(body.current_thread.text) contains 'required now'
- strings.replace_confusables(body.current_thread.text) contains 'immediate action'
- strings.replace_confusables(body.current_thread.text) contains 'security update'
- strings.replace_confusables(body.current_thread.text) contains 'blocked'
- strings.replace_confusables(body.current_thread.text) contains 'locked'
- strings.replace_confusables(body.current_thread.text) contains 'interruption'
- strings.replace_confusables(body.current_thread.text) contains 'action is not taken'
any of:
all of:
- length(body.current_thread.text) > 200
- length(body.current_thread.text) < 2000
body.html.raw matches any of 7 patterns
(?:(?:<br\s*/?>\s*){20,}|\n{20,})(?:<p[^>]*>\s*<br\s*/?>\s*</p>\s*){30,}(?:<p class=".*?"><span style=".*?"><o:p>&nbsp;</o:p></span></p>\s*){30,}(?:<p>\s*&nbsp;\s*</p>\s*){7,}(?:<p>\s*&nbsp;\s*</p>\s*<br>\s*){7,}(?:<p[^>]*>\s*&nbsp;\s*<br>\s*</p>\s*){5,}(?:<p[^>]*>&nbsp;</p>\s*){7,}
any of
body.linkswhere all hold:any of:
- .href_url.domain.root_domain is not sender.email.domain.root_domain
all of:
- .href_url.domain.root_domain is missing
- .href_url.url matches '(\\d{1,3}.){3}\\d{1,3}'
- .href_url.domain.root_domain not in $org_domains
any of:
all of:
not:
- profile.by_sender_email().any_messages_benign
not:
- profile.by_sender_email().solicited
all of:
- sender.email.domain.domain in $org_domains
not:
- headers.auth_summary.spf.pass
any of:
- length(headers.references) is 0
- headers.in_reply_to is missing
any of:
all of:
- sender.email.domain.root_domain in $high_trust_sender_root_domains
any of
distinct(headers.hops)where:- .authentication_results.dmarc matches '*fail'
- sender.email.domain.root_domain not in $high_trust_sender_root_domains
Inspects: attachments[].file_type, body.current_thread.text, body.html.raw, body.links, body.links[].href_url.domain.domain, body.links[].href_url.domain.root_domain, body.links[].href_url.url, headers.auth_summary.spf.pass, headers.hops, headers.hops[].authentication_results.dmarc, headers.in_reply_to, headers.references, recipients.to, recipients.to[].email.email, recipients.to[].email.local_part, sender.email.domain.domain, sender.email.domain.root_domain, subject.subject, type.inbound. Sensors: beta.ocr, beta.parse_exif, file.explode, ml.logo_detect, ml.nlu_classifier, profile.by_sender_email, regex.icontains, strings.icontains, strings.ilike, strings.istarts_with, strings.replace_confusables. Reference lists: $file_types_images, $high_trust_sender_root_domains, $org_domains.
Indicators matched (42)
| Field | Match | Value |
|---|---|---|
attachments[].file_type | member | pdf |
attachments[].file_type | member | doc |
attachments[].file_type | member | docx |
attachments[].file_type | equals | pdf |
strings.istarts_with | prefix | Microsoft |
ml.logo_detect(attachments[]).brands[].name | equals | Generic Webmail |
ml.logo_detect(attachments[]).brands[].confidence | equals | high |
regex.icontains | regex | (expir(e(d|s)?|ation|s)?|\blo(o)?se\b|(?:offices?|microsoft).365|re.{0,3}confirm)|due for update |
strings.icontains | substring | link expires in |
regex.icontains | regex | account|access|your email|mailbox |
regex.icontains | regex | \bpassword\b |
regex.icontains | regex | \bmulti.?factor\b |
30 more
ml.nlu_classifier(strings.replace_confusables(body.current_thread.text)).intents[].name | equals | cred_theft |
ml.nlu_classifier(strings.replace_confusables(body.current_thread.text)).intents[].confidence | equals | high |
strings.icontains | substring | password |
regex.icontains | regex | password\s*(?:\w+\s+){0,4}\s*reconfirm |
regex.icontains | regex | keep\s*(?:\w+\s+){0,4}\s*password |
strings.icontains | substring | password is due |
strings.icontains | substring | expiration |
strings.icontains | substring | expire |
strings.icontains | substring | expiring |
strings.icontains | substring | kindly |
strings.icontains | substring | renew |
strings.icontains | substring | review |
strings.icontains | substring | click below |
strings.icontains | substring | kicked out |
strings.icontains | substring | required now |
strings.icontains | substring | immediate action |
strings.icontains | substring | security update |
strings.icontains | substring | blocked |
strings.icontains | substring | locked |
strings.icontains | substring | interruption |
strings.icontains | substring | action is not taken |
regex.icontains | regex | (?:(?:<br\s*/?>\s*){20,}|\n{20,}) |
regex.icontains | regex | (?:<p[^>]*>\s*<br\s*/?>\s*</p>\s*){30,} |
regex.icontains | regex | (?:<p class=".*?"><span style=".*?"><o:p>&nbsp;</o:p></span></p>\s*){30,} |
regex.icontains | regex | (?:<p>\s*&nbsp;\s*</p>\s*){7,} |
regex.icontains | regex | (?:<p>\s*&nbsp;\s*</p>\s*<br>\s*){7,} |
regex.icontains | regex | (?:<p[^>]*>\s*&nbsp;\s*<br>\s*</p>\s*){5,} |
regex.icontains | regex | (?:<p[^>]*>&nbsp;</p>\s*){7,} |
regex.icontains | regex | (\d{1,3}.){3}\d{1,3} |
strings.ilike | substring | *fail |