EXPLORE
← Back to Explore
sublimemediumRule

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.

MITRE ATT&CK

initial-access

Detection Query

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 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
)

Data Sources

Email MessagesEmail HeadersEmail Attachments

Platforms

email
Raw Content
name: "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."
type: "rule"
severity: "medium"
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")
                      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 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
  )
attack_types:
  - "Credential Phishing"
tactics_and_techniques:
  - "Social engineering"
detection_methods:
  - "Content analysis"
  - "Natural Language Understanding"
  - "Sender analysis"
id: "5d9c3a75-5f57-5d0c-a07f-0f300bbde076"