Detection rules › Sublime MQL

Sublime MQL rules: xero

Xero infrastructure abuse

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

Identifies messages that resemble credential theft, originating from Xero. Xero infrastrcture abuse has been observed recently to send phishing attacks.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesCredential Phishing
Tactics and techniquesEvasion, Social engineering

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • body
  • body.current_thread
  • body.links
  • headers
  • headers.hops
  • recipients
  • recipients.to
  • sender.email
  • subject
  • type

Rule Body

type.inbound
and sender.email.email == "messaging-service@post.xero.com"
and 
// there are external links (not org or xero domains)
length(filter(body.links,
              .href_url.domain.domain not in $org_domains
              and .href_url.domain.root_domain not in ("xero.com", )
       )
) > 0
and (
  any(ml.nlu_classifier(body.current_thread.text).intents,
      .name == "cred_theft" and .confidence == "high"
  )
  // subject match when cred_theft doesn't match
  // high confidence observed subject intros in the format of "Urgent Thing: ..."
  or regex.icontains(subject.subject,
                     '^(?:(?:Final|Last)?\s*Warning|(?:Final|Last|Legal|Critical|Content Violation)?\s*(?:Alert|Noti(?:ce|fication))|Appeal Required|Time.Sensitive|Critical.Alert|Important|Copyright Issue)\s*:\s*'
  )
  or any(ml.logo_detect(file.message_screenshot()).brands,
         .name in ("Facebook", "Meta", "Instagram")
         and .confidence in ("medium", "high")
  )
  // any of the links are for newly registered domains
  or any(filter(body.links,
                .href_url.domain.domain not in $org_domains
                and .href_url.domain.root_domain not in ("xero.com")
         ),
         network.whois(.href_url.domain).days_old < 30
  )
  or (
    any(ml.nlu_classifier(body.current_thread.text).topics,
        .name in ("B2B Cold Outreach", "Professional and Career Development")
        and .confidence != "low"
    )
  )
  // sender display name or subject contains confusables
  or (
    sender.display_name != strings.replace_confusables(sender.display_name)
    or subject.subject != strings.replace_confusables(subject.subject)
  )
  // IP pool appears to be tagged by Xero via Mailgun
  // https://help.mailgun.com/hc/en-us/articles/360052184214-IP-Pools
  or any(headers.hops,
         any(.fields,
             .name == "X-Mailgun-Sending-Ip-Pool-Name"
             and .value == "High Risk Pool"
         )
  )
)
and (
  ( // sender domain matches no body domains
    length(body.links) > 0
    and all(body.links,
            .href_url.domain.root_domain not in ("xero.com", )
            or .href_url.domain.root_domain is null
    )
  )
  // link contains email address
  or any(recipients.to,
         .email.domain.valid
         and any(body.links,
                 strings.icontains(.href_url.url, ..email.email)
                 or any(beta.scan_base64(.href_url.url,
                                         format="url",
                                         ignore_padding=true
                        ),
                        strings.icontains(., ...email.email)
                 )
                 or any(beta.scan_base64(.href_url.fragment,
                                         ignore_padding=true
                        ),
                        strings.icontains(., ...email.email)
                 )
                 // cloudflare turnstile or phishing warning page
                 or strings.icontains(ml.link_analysis(., mode="aggressive").final_dom.display_text,
                                      "cloudflare"
                 )
         )
  )
  or regex.icontains(subject.subject, "termination.*notice")
  or any(ml.nlu_classifier(body.current_thread.text).entities,
         .name in ("sender", "org")
         and regex.icontains(.text, 'Recruitment|staffing|\bhr\b')
  )
)

Detection Logic

Scope: inbound message.

Identifies messages that resemble credential theft, originating from Xero. Xero infrastrcture abuse has been observed recently to send phishing attacks.

  1. inbound message
  2. sender.email.email is 'messaging-service@post.xero.com'
  3. length(filter(body.links, .href_url.domain.domain not in $org_domains and .href_url.domain.root_domain not in ("xero.com", ) ) ) > 0
  4. any of:
    • any(ml.nlu_classifier(body.current_thread.text).intents, .name == "cred_theft" and .confidence == "high" )
    • subject.subject matches '^(?:(?:Final|Last)?\\s*Warning|(?:Final|Last|Legal|Critical|Content Violation)?\\s*(?:Alert|Noti(?:ce|fication))|Appeal Required|Time.Sensitive|Critical.Alert|Important|Copyright Issue)\\s*:\\s*'
    • any(ml.logo_detect(file.message_screenshot()).brands, .name in ("Facebook", "Meta", "Instagram") and .confidence in ("medium", "high") )
    • any(filter(body.links, .href_url.domain.domain not in $org_domains and .href_url.domain.root_domain not in ("xero.com") ), network.whois(.href_url.domain).days_old < 30 )
    • ( any(ml.nlu_classifier(body.current_thread.text).topics, .name in ("B2B Cold Outreach", "Professional and Career Development") and .confidence != "low" ) )
    • any of:
      • sender.display_name is not strings.replace_confusables(sender.display_name)
      • subject.subject is not strings.replace_confusables(subject.subject)
    • any of headers.hops where:
      • any of .fields where all hold:
        • .name is 'X-Mailgun-Sending-Ip-Pool-Name'
        • .value is 'High Risk Pool'
  5. any of:
    • all of:
      • length(body.links) > 0
      • all of body.links where any holds:
        • .href_url.domain.root_domain not in ('xero.com')
        • .href_url.domain.root_domain is missing
    • any of recipients.to where all hold:
      • .email.domain.valid
      • any of body.links where any holds:
        • strings.icontains(.href_url.url, ..email.email)
        • any(beta.scan_base64(.href_url.url, format="url", ignore_padding=true ), strings.icontains(., ...email.email) )
        • any(beta.scan_base64(.href_url.fragment, ignore_padding=true ), strings.icontains(., ...email.email) )
        • strings.icontains(ml.link_analysis(., mode="aggressive").final_dom.display_text, "cloudflare" )
    • subject.subject matches 'termination.*notice'
    • any(ml.nlu_classifier(body.current_thread.text).entities, .name in ("sender", "org") and regex.icontains(.text, 'Recruitment|staffing|\bhr\b') )

Inspects: body.current_thread.text, body.links, body.links[].href_url.domain.domain, body.links[].href_url.domain.root_domain, body.links[].href_url.fragment, body.links[].href_url.url, headers.hops, headers.hops[].fields, headers.hops[].fields[].name, headers.hops[].fields[].value, recipients.to, recipients.to[].email.domain.valid, recipients.to[].email.email, sender.display_name, sender.email.email, subject.subject, type.inbound. Sensors: beta.scan_base64, file.message_screenshot, ml.link_analysis, ml.logo_detect, ml.nlu_classifier, network.whois, regex.icontains, strings.icontains, strings.replace_confusables. Reference lists: $org_domains.

Indicators Matched (18)

FieldMatchValue
sender.email.emailequalsmessaging-service@post.xero.com
ml.nlu_classifier(body.current_thread.text).intents[].nameequalscred_theft
ml.nlu_classifier(body.current_thread.text).intents[].confidenceequalshigh
regex.icontainsregex^(?:(?:Final|Last)?\s*Warning|(?:Final|Last|Legal|Critical|Content Violation)?\s*(?:Alert|Noti(?:ce|fication))|Appeal Required|Time.Sensitive|Critical.Alert|Important|Copyright Issue)\s*:\s*
ml.logo_detect(file.message_screenshot()).brands[].namememberFacebook
ml.logo_detect(file.message_screenshot()).brands[].namememberMeta
ml.logo_detect(file.message_screenshot()).brands[].namememberInstagram
ml.logo_detect(file.message_screenshot()).brands[].confidencemembermedium
ml.logo_detect(file.message_screenshot()).brands[].confidencememberhigh
ml.nlu_classifier(body.current_thread.text).topics[].namememberB2B Cold Outreach
ml.nlu_classifier(body.current_thread.text).topics[].namememberProfessional and Career Development
headers.hops[].fields[].nameequalsX-Mailgun-Sending-Ip-Pool-Name
6 more
headers.hops[].fields[].valueequalsHigh Risk Pool
strings.icontainssubstringcloudflare
regex.icontainsregextermination.*notice
ml.nlu_classifier(body.current_thread.text).entities[].namemembersender
ml.nlu_classifier(body.current_thread.text).entities[].namememberorg
regex.icontainsregexRecruitment|staffing|\bhr\b

Stages and Predicates

Stage 1: mql_rule

and
  or
    any(recipients.to)
      and
        any(body.links)
          or
            strings.icontains func_call "strings.icontains(.href_url.url, ..email.email)"
            strings.icontains func_call "strings.icontains(ml.link_analysis(., mode=\"aggressive\").final_dom.display_text,\n                                      \"cloudflare\"\n                 )"
             macro "any(beta.scan_base64(.href_url.fragment,\n                                         ignore_padding=true\n                        ),\n                        strings.icontains(., ...email.email)\n                 )"
             macro "any(beta.scan_base64(.href_url.url,\n                                         format=\"url\",\n                                         ignore_padding=true\n                        ),\n                        strings.icontains(., ...email.email)\n                 )"
        recipients.to.email.domain.valid eq "true"
    and
      body.links length_compare "0"
       macro "all(body.links,\n            .href_url.domain.root_domain not in (\"xero.com\", )\n            or .href_url.domain.root_domain is null\n    )"
    subject.subject regex_match "termination.*notice"
     macro "any(ml.nlu_classifier(body.current_thread.text).entities,\n         .name in (\"sender\", \"org\")\n         and regex.icontains(.text, 'Recruitment|staffing|\\bhr\\b')\n  )"
  or
    any(headers.hops)
      any(headers.hops.fields)
        and
          headers.hops.fields[].name eq "X-Mailgun-Sending-Ip-Pool-Name"
          headers.hops.fields[].value eq "High Risk Pool"
    sender.display_name macro "sender.display_name != strings.replace_confusables(sender.display_name)"
    subject.subject macro "subject.subject != strings.replace_confusables(subject.subject)"
    subject.subject regex_match "^(?:(?:Final|Last)?\\s*Warning|(?:Final|Last|Legal|Critical|Content Violation)?\\s*(?:Alert|Noti(?:ce|fication))|Appeal Required|Time.Sensitive|Critical.Alert|Important|Copyright Issue)\\s*:\\s*"
     macro "(\n    any(ml.nlu_classifier(body.current_thread.text).topics,\n        .name in (\"B2B Cold Outreach\", \"Professional and Career Development\")\n        and .confidence != \"low\"\n    )\n  )"
     macro "any(filter(body.links,\n                .href_url.domain.domain not in $org_domains\n                and .href_url.domain.root_domain not in (\"xero.com\")\n         ),\n         network.whois(.href_url.domain).days_old < 30\n  )"
     macro "any(ml.logo_detect(file.message_screenshot()).brands,\n         .name in (\"Facebook\", \"Meta\", \"Instagram\")\n         and .confidence in (\"medium\", \"high\")\n  )"
     macro "any(ml.nlu_classifier(body.current_thread.text).intents,\n      .name == \"cred_theft\" and .confidence == \"high\"\n  )"
  length func_call "length(filter(body.links,\n              .href_url.domain.domain not in $org_domains\n              and .href_url.domain.root_domain not in (\"xero.com\", )\n       )\n) > 0"
  sender.email.email eq "messaging-service@post.xero.com"
  type.inbound eq "true"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
sender.email.emaileq
  • messaging-service@post.xero.com
field:"sender.email.email" kind:eq value:"messaging-service@post.xero.com"
subject.subjectregex_match
  • ^(?:(?:Final|Last)?\s*Warning|(?:Final|Last|Legal|Critical|Content Violation)?\s*(?:Alert|Noti(?:ce|fication))|Appeal Required|Time.Sensitive|Critical.Alert|Important|Copyright Issue)\s*:\s* corpus 2 (mql 2)
  • termination.*notice corpus 9 (mql 9)
field:"subject.subject" kind:regex_match
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

Xero invoice abuse

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

Detects suspicious Xero invoice communications containing urgent payment requests where the sender's display name contains either confusable characters or impersonates internal services like HR or IT support.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud, Credential Phishing
Tactics and techniquesImpersonation: Brand, Impersonation: Employee, Social engineering

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • body
  • body.current_thread
  • body.links
  • sender.email
  • type

Rule Body

type.inbound
and sender.email.domain.root_domain == "xero.com"
and (
  // contains legitimate xero invoice links
  any(body.links,
      .href_url.domain.domain == "in.xero.com"
      or (
        .href_url.domain.root_domain == "mimecastprotect.com"
        and .href_url.query_params == "domain=in.xero.com"
      )
  )
  // or financial communications with invoice content and urgency
  and (
    any(beta.ml_topic(body.current_thread.text).topics,
        .name == "Financial Communications" and .confidence != "low"
    )
    and any(ml.nlu_classifier(body.current_thread.text).tags,
            .name == "invoice" and .confidence in ("medium", "high")
    )
    and any(ml.nlu_classifier(body.current_thread.text).entities,
            .name == "urgency"
    )
    and any(ml.nlu_classifier(body.current_thread.text).entities,
            .name == "request"
    )
  )
)
and (
  // display name contains confusables (brand impersonation)
  sender.display_name != strings.replace_confusables(sender.display_name)
  // or HR/recruitment/employment/internal service impersonation
  or regex.icontains(sender.display_name,
                     '\bhr\b|human resources|staffing|recruiting|recruitment|employment|payroll|it support|help ?desk|admin|administrator'
  )
)

Detection Logic

Scope: inbound message.

Detects suspicious Xero invoice communications containing urgent payment requests where the sender's display name contains either confusable characters or impersonates internal services like HR or IT support.

  1. inbound message
  2. sender.email.domain.root_domain is 'xero.com'
  3. all of:
    • any of body.links where any holds:
      • .href_url.domain.domain is 'in.xero.com'
      • all of:
        • .href_url.domain.root_domain is 'mimecastprotect.com'
        • .href_url.query_params is 'domain=in.xero.com'
    • all of:
      • any(beta.ml_topic(body.current_thread.text).topics, .name == "Financial Communications" and .confidence != "low" )
      • any(ml.nlu_classifier(body.current_thread.text).tags, .name == "invoice" and .confidence in ("medium", "high") )
      • any(ml.nlu_classifier(body.current_thread.text).entities, .name == "urgency" )
      • any(ml.nlu_classifier(body.current_thread.text).entities, .name == "request" )
  4. any of:
    • sender.display_name is not strings.replace_confusables(sender.display_name)
    • sender.display_name matches '\\bhr\\b|human resources|staffing|recruiting|recruitment|employment|payroll|it support|help ?desk|admin|administrator'

Inspects: body.current_thread.text, body.links, body.links[].href_url.domain.domain, body.links[].href_url.domain.root_domain, body.links[].href_url.query_params, sender.display_name, sender.email.domain.root_domain, type.inbound. Sensors: beta.ml_topic, ml.nlu_classifier, regex.icontains, strings.replace_confusables.

Indicators Matched (11)

FieldMatchValue
sender.email.domain.root_domainequalsxero.com
body.links[].href_url.domain.domainequalsin.xero.com
body.links[].href_url.domain.root_domainequalsmimecastprotect.com
body.links[].href_url.query_paramsequalsdomain=in.xero.com
beta.ml_topic(body.current_thread.text).topics[].nameequalsFinancial Communications
ml.nlu_classifier(body.current_thread.text).tags[].nameequalsinvoice
ml.nlu_classifier(body.current_thread.text).tags[].confidencemembermedium
ml.nlu_classifier(body.current_thread.text).tags[].confidencememberhigh
ml.nlu_classifier(body.current_thread.text).entities[].nameequalsurgency
ml.nlu_classifier(body.current_thread.text).entities[].nameequalsrequest
regex.icontainsregex\bhr\b|human resources|staffing|recruiting|recruitment|employment|payroll|it support|help ?desk|admin|administrator

Stages and Predicates

Stage 1: mql_rule

and
  any(body.links)
    or
      and
        body.links.href_url.domain.root_domain eq "mimecastprotect.com"
        body.links.href_url.query_params eq "domain=in.xero.com"
      body.links.href_url.domain.domain eq "in.xero.com"
  or
    sender.display_name macro "sender.display_name != strings.replace_confusables(sender.display_name)"
    sender.display_name regex_match "\\bhr\\b|human resources|staffing|recruiting|recruitment|employment|payroll|it support|help ?desk|admin|administrator"
  sender.email.domain.root_domain eq "xero.com"
  type.inbound eq "true"
   macro "any(beta.ml_topic(body.current_thread.text).topics,\n        .name == \"Financial Communications\" and .confidence != \"low\"\n    )"
   macro "any(ml.nlu_classifier(body.current_thread.text).entities,\n            .name == \"request\"\n    )"
   macro "any(ml.nlu_classifier(body.current_thread.text).entities,\n            .name == \"urgency\"\n    )"
   macro "any(ml.nlu_classifier(body.current_thread.text).tags,\n            .name == \"invoice\" and .confidence in (\"medium\", \"high\")\n    )"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
sender.display_nameregex_match
    • \bhr\b
    • human resources
    • staffing
    • recruiting
    • recruitment
    • employment
    • payroll
    • it support
    • help ?desk
    • admin
    • administrator
field:"sender.display_name" kind:regex_match
sender.email.domain.root_domaineq
  • xero.com
field:"sender.email.domain.root_domain" kind:eq value:"xero.com"
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

Back to all Sublime MQL rules