Brand impersonation: Microsoft Teams invitation

Detects messages impersonating a Microsoft Teams invites by matching known invite text patterns while containing join links that do not resolve to Microsoft domains. Additional verification includes checking for absent phone dial-in options and missing standard Teams help text or HTML meeting components.

Sublime rule (View on GitHub)

  1name: "Brand impersonation: Microsoft Teams invitation"
  2description: "Detects messages impersonating a Microsoft Teams invites by matching known invite text patterns while containing join links that do not resolve to Microsoft domains. Additional verification includes checking for absent phone dial-in options and missing standard Teams help text or HTML meeting components."
  3type: "rule"
  4severity: "high"
  5source: |
  6  type.inbound
  7  and (
  8    (
  9      strings.icontains(body.current_thread.text, 'Microsoft Teams')
 10      and strings.icontains(body.current_thread.text,
 11                            'join the meeting',
 12                            'confirm your attendance',
 13                            'confirm attendance'
 14      )
 15      and strings.contains(body.current_thread.text, 'Meeting ID:')
 16      and strings.contains(body.current_thread.text, 'Passcode:')
 17    )
 18    or (
 19      strings.icontains(body.current_thread.text, "teams")
 20      // strings that give us confidence it's teams
 21      and 2 of (
 22        strings.icontains(body.current_thread.text, "internal"),
 23        strings.icontains(body.current_thread.text, "message"),
 24        strings.icontains(body.current_thread.text, "meeting"),
 25        strings.icontains(body.current_thread.text, "Download Teams")
 26      )
 27    )
 28    // either the subject or sender.display name containt Microsoft Teams and Meeting
 29    or (
 30      any([subject.base, sender.display_name],
 31          strings.icontains(., 'Microsoft Teams')
 32          and strings.icontains(., 'meeting')
 33      )
 34    )
 35    or length(filter(ml.nlu_classifier(body.current_thread.text).entities,
 36                     .name == "org"
 37                     and strings.icontains(.text, "microsoft teams")
 38              )
 39    ) >= 2
 40  )
 41  and (
 42    (
 43      // not a reply
 44      length(headers.references) == 0
 45      and headers.in_reply_to is null
 46      // few links
 47      and length(distinct(body.links, .href_url.url)) < 10
 48      // short body
 49      and (
 50        length(body.current_thread.text) < 600
 51        // account for NLU disclaimer in length
 52        or any(map(filter(ml.nlu_classifier(body.current_thread.text).entities,
 53                          .name == "disclaimer"
 54                   ),
 55                   .text
 56               ),
 57               (length(body.current_thread.text) - length(.)) < 600
 58        )
 59        // NLU only tags the first english disclaimer, so pick up repeat disclaimers or non-english disclaimers as well
 60        or any(regex.iextract(body.current_thread.text,
 61                              '^(?P<disclaimer>.{100,}?)(?:confidentiality notice|nota di riservatezza)'
 62               ),
 63               length(.named_groups["disclaimer"]) < 600
 64        )
 65      )
 66    ) // or sender domain is new
 67    or network.whois(sender.email.domain).days_old < 365
 68  )
 69  // no unsubscribe links
 70  // common in newsletters which link to a webinar style event
 71  and not any(body.links, strings.icontains(.display_text, "unsub"))
 72  
 73  // one of the links contains is a CTA that doesn't link to MS
 74  and any(body.current_thread.links,
 75          (
 76            .display_text =~ "join the meeting"
 77            or strings.icontains(.display_text,
 78                                 "join the meeting",
 79                                 "play recording",
 80                                 "open in teams"
 81            )
 82            // is a mismatched domain via .display_url
 83            or (
 84              .display_url.domain.root_domain in (
 85                "microsoft.com",
 86                "microsoft.us",
 87                "microsoft.cn",
 88                "live.com"
 89              )
 90              and .mismatched
 91            )
 92          )
 93          and .href_url.domain.root_domain not in (
 94            "microsoft.com",
 95            "microsoft.us",
 96            "microsoft.cn",
 97            "live.com"
 98          )
 99          and not (
100            .href_url.domain.root_domain == "mimecastprotect.com"
101            and (
102              strings.parse_domain(.href_url.query_params_decoded["domain"][0]).root_domain in (
103                "microsoft.com",
104                "microsoft.us",
105                "microsoft.cn",
106                "live.com"
107              )
108              or strings.parse_domain(.href_url.query_params_decoded["domain"][0]).root_domain in $bulk_mailer_url_root_domains
109            )
110          )
111          // rewriters often abstract the link
112          and .href_url.domain.root_domain not in $bulk_mailer_url_root_domains
113  )
114  // missing the dial by phone element
115  and not strings.icontains(body.current_thread.text, 'Dial in by phone')
116  
117  // any of these suspicious elements from the body
118  and (
119    // malicious samples leveraged recipient domain branding here
120    not strings.icontains(body.current_thread.text, 'Microsoft Teams Need help?')
121    // malicious samples contained unique html elements not present in legit ones
122    or strings.icontains(body.html.raw, '<div class="meeting-title">')
123    or strings.icontains(body.html.raw, '<div class="meeting-time">')
124    or strings.icontains(body.html.raw, '<div class="meeting-location">')
125    or strings.icontains(body.html.raw, '<span class="conflict-badge">')
126    or strings.icontains(body.html.raw, 'class="join-button"')
127  )
128  
129  // negate highly trusted sender domains unless they fail DMARC authentication
130  and not (
131    sender.email.domain.root_domain in $high_trust_sender_root_domains
132    and coalesce(headers.auth_summary.dmarc.pass, false)
133  )  
134attack_types:
135  - "Credential Phishing"
136tactics_and_techniques:
137  - "Impersonation: Brand"
138  - "Social engineering"
139detection_methods:
140  - "Content analysis"
141  - "Header analysis"
142  - "HTML analysis"
143  - "URL analysis"
144id: "46410ad8-3465-505f-a78e-f77704910a91"
to-top