Detection rules › Sublime MQL

Credential phishing: Fake password expiration from new and unsolicited sender

Severity
medium
Type
rule
Source
github.com/sublime-security/sublime-rules

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

CategoryValues
Attack typesCredential Phishing
Tactics and techniquesSocial 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.

  1. inbound message
  2. 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
  3. 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 attachments where:
          • .file_type in $file_types_images
        • all of:
          • length(attachments) > 0
          • length(attachments) < 6
        • any of attachments where any holds:
          • any of ml.logo_detect(.).brands where 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
  4. 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 '
  5. any of:
    • any of [subject.subject, body.current_thread.text] where:
      • . matches 'account|access|your email|mailbox'
    • any of recipients.to where:
      • any of [subject.subject, body.current_thread.text] where any holds:
        • strings.icontains(strings.replace_confusables(.))
        • strings.icontains(strings.replace_confusables(.))
  6. 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
  7. any of:
    • any of ml.nlu_classifier(strings.replace_confusables(body.current_thread.text)).intents where 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'
  8. 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,}
  9. any of body.links where 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
  10. 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
  11. any of:
    • length(headers.references) is 0
    • headers.in_reply_to is missing
  12. 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)

FieldMatchValue
attachments[].file_typememberpdf
attachments[].file_typememberdoc
attachments[].file_typememberdocx
attachments[].file_typeequalspdf
strings.istarts_withprefixMicrosoft
ml.logo_detect(attachments[]).brands[].nameequalsGeneric Webmail
ml.logo_detect(attachments[]).brands[].confidenceequalshigh
regex.icontainsregex(expir(e(d|s)?|ation|s)?|\blo(o)?se\b|(?:offices?|microsoft).365|re.{0,3}confirm)|due for update
strings.icontainssubstringlink expires in
regex.icontainsregexaccount|access|your email|mailbox
regex.icontainsregex\bpassword\b
regex.icontainsregex\bmulti.?factor\b
30 more
ml.nlu_classifier(strings.replace_confusables(body.current_thread.text)).intents[].nameequalscred_theft
ml.nlu_classifier(strings.replace_confusables(body.current_thread.text)).intents[].confidenceequalshigh
strings.icontainssubstringpassword
regex.icontainsregexpassword\s*(?:\w+\s+){0,4}\s*reconfirm
regex.icontainsregexkeep\s*(?:\w+\s+){0,4}\s*password
strings.icontainssubstringpassword is due
strings.icontainssubstringexpiration
strings.icontainssubstringexpire
strings.icontainssubstringexpiring
strings.icontainssubstringkindly
strings.icontainssubstringrenew
strings.icontainssubstringreview
strings.icontainssubstringclick below
strings.icontainssubstringkicked out
strings.icontainssubstringrequired now
strings.icontainssubstringimmediate action
strings.icontainssubstringsecurity update
strings.icontainssubstringblocked
strings.icontainssubstringlocked
strings.icontainssubstringinterruption
strings.icontainssubstringaction is not taken
regex.icontainsregex(?:(?:<br\s*/?>\s*){20,}|\n{20,})
regex.icontainsregex(?:<p[^>]*>\s*<br\s*/?>\s*</p>\s*){30,}
regex.icontainsregex(?:<p class=".*?"><span style=".*?"><o:p>&nbsp;</o:p></span></p>\s*){30,}
regex.icontainsregex(?:<p>\s*&nbsp;\s*</p>\s*){7,}
regex.icontainsregex(?:<p>\s*&nbsp;\s*</p>\s*<br>\s*){7,}
regex.icontainsregex(?:<p[^>]*>\s*&nbsp;\s*<br>\s*</p>\s*){5,}
regex.icontainsregex(?:<p[^>]*>&nbsp;</p>\s*){7,}
regex.icontainsregex(\d{1,3}.){3}\d{1,3}
strings.ilikesubstring*fail