• Sublime Core Feed
Medium Severity

Credential Phishing: Fake Password Expiration from New and Unsolicited sender

Labels

Credential Phishing
Social engineering
Content analysis
Natural Language Understanding
Sender analysis

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
GitHub
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>&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
        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
)
MQL Rule Console
DocsLearning Labs

Playground

Test against your own EMLs or sample data.

Share

Post about this on your socials.

Get Started. Today.

Managed or self-managed. No MX changes.

Get Started