This hypothesis targets adversaries leveraging external Microsoft Teams domains to deliver phishing payloads or malicious links, aligning with MITRE technique T1566, by identifying high-volume senders with low-reputation URLs. Proactively hunting for these patterns in Azure Sentinel allows the SOC to isolate compromised or malicious external conversations before they propagate, reducing the risk of credential theft or malware execution within the organization.
//This query ranks external Microsoft Teams sender domains by threat volume over the last 30 days, split by
//conversation type, with threat-type breakdown, threat rate and a count of messages carrying low-reputation URLs.
//Senders skewing to 1:1 chats are notable because there are no colleagues in the conversation to challenge the request.
//Messages are de-duplicated to the latest record per message; only inbound external threads are counted.
let suspTlds = dynamic(["top","xyz","click","link","shop","online","site","live","icu","cyou","sbs","rest","quest","cfd","bond","buzz","fun","space","monster","work","tk","ml","ga","cf","gq","dev","app","zip","mov","info","win","loan","men","stream","country"]);
let suspUrlMsgs = MessageUrlInfo
| where Timestamp > ago(30d)
| where isnotempty(UrlDomain)
| extend Tld = tostring(split(UrlDomain, ".")[-1])
| where Tld in (suspTlds)
| distinct TeamsMessageId;
MessageEvents
| where Timestamp > ago(30d)
| where isnotempty(TeamsMessageId)
//Only the columns used below are carried through the de-duplication, to keep the shuffle cost down.
| summarize arg_max(Timestamp, IsExternalThread, IsOwnedThread, SenderEmailAddress, GroupId, RecipientDetails, ThreatTypes) by TeamsMessageId
| where IsExternalThread == 1 and IsOwnedThread == 0
| extend SenderDomain = tolower(tostring(split(SenderEmailAddress, "@")[1]))
| where isnotempty(SenderDomain)
| extend ConvType = case(isnotempty(GroupId), "Channel",
array_length(todynamic(RecipientDetails)) > 1, "Group chat",
"1:1 chat")
| extend HasSuspUrl = TeamsMessageId in (suspUrlMsgs)
| summarize TeamsMessages = count(), Senders = dcount(SenderEmailAddress),
OneToOne = countif(ConvType == "1:1 chat"),
GroupChat = countif(ConvType == "Group chat"),
ChannelMsgs = countif(ConvType == "Channel"),
ThreatMessages = countif(isnotempty(ThreatTypes)),
Malware = countif(ThreatTypes has "Malware"),
Phish = countif(ThreatTypes has "Phish"),
Spam = countif(ThreatTypes has "Spam"),
LowRepUrlMessages = countif(HasSuspUrl),
FirstSeen = min(Timestamp), LastSeen = max(Timestamp)
by SenderDomain
| where ThreatMessages > 0
| extend ThreatRatePct = round(100.0 * ThreatMessages / TeamsMessages, 1)
| top 20 by ThreatMessages desc
| project ['Sender Domain']=SenderDomain, ['Teams Messages']=TeamsMessages, ['Distinct Senders']=Senders,
['1:1 Chat']=OneToOne, ['Group Chat']=GroupChat, ['Channel']=ChannelMsgs,
['Threat Messages']=ThreatMessages, Malware, Phish, Spam,
['Threat Rate %']=ThreatRatePct, ['Low-Rep URL Messages']=LowRepUrlMessages,
['First Seen']=FirstSeen, ['Last Seen']=LastSeen
id: 91dffff3-b1f9-48bd-b0de-a8940b32e265
name: External Microsoft Teams Sender Domain Risk
description: |
This query ranks external Microsoft Teams sender domains by threat volume, split by conversation type, with threat rate and low-reputation URL counts.
description-detailed: |
This query ranks external Microsoft Teams sender domains by the number of messages carrying a threat over the last 30 days, using Advanced hunting in Microsoft Defender XDR. For each domain it returns the split across conversation types (one to one chat, group chat, channel), the breakdown by threat type, the threat rate, a count of messages carrying URLs on low-reputation top level domains, and first and last seen. Conversation shape is the signal that repays attention: legitimate partner domains tend to spread across channels and group chats, whereas social-engineering senders skew heavily toward one to one chats where there are no colleagues present to challenge the request. A domain that is new in the period, skews to one to one, or shows a high threat rate on low volume is the anomaly worth reviewing. Messages are de-duplicated to the latest record per message, and only inbound external threads are counted.
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- MessageEvents
- MessageUrlInfo
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
//This query ranks external Microsoft Teams sender domains by threat volume over the last 30 days, split by
//conversation type, with threat-type breakdown, threat rate and a count of messages carrying low-reputation URLs.
//Senders skewing to 1:1 chats are notable because there are no colleagues in the conversation to challenge the request.
//Messages are de-duplicated to the latest record per message; only inbound external threads are counted.
let suspTlds = dynamic(["top","xyz","click","link","shop","online","site","live","icu","cyou","sbs","rest","quest","cfd","bond","buzz","fun","space","monster","work","tk","ml","ga","cf","gq","dev","app","zip","mov","info","win","loan","men","stream","country"]);
let suspUrlMsgs = MessageUrlInfo
| where Timestamp > ago(30d)
| where isnotempty(UrlDomain)
| extend Tld = tostring(split(UrlDomain, ".")[-1])
| where Tld in (suspTlds)
| distinct TeamsMessageId;
MessageEvents
| where Timestamp > ago(30d)
| where isnotempty(TeamsMessageId)
//Only the columns used below are carried through the de-duplication, to keep the shuffle cost down.
| summarize arg_max(Timestamp, IsExternalThread, IsOwnedThread, SenderEmailAddress, GroupId, RecipientDetails, ThreatTypes) by TeamsMessageId
| where IsExternalThread == 1 and IsOwnedThread == 0
| extend SenderDomain = tolower(tostring(split(SenderEmailAddress, "@")[1]))
| where isnotempty(SenderDomain)
| extend ConvType = case(isnotempty(GroupId), "Channel",
array_length(todynamic(RecipientDetails)) > 1, "Group chat",
Scenario: A third-party SaaS vendor (e.g., Salesforce, ServiceNow, or Jira) sends automated notifications or alerts to a shared Teams channel via their official API integration. These messages often contain deep links to the vendor’s portal, which may be flagged as “low-reputation” by URL filtering engines if the specific subdomain is new or if the vendor uses a generic CDN domain for assets.
*.salesforce.com, *.servicenow.com, *.atlassian.net) or exclude conversations where the sender is a known bot identity (e.g., Microsoft Teams bot, Jira bot) and the message type is systemMessage or notification.Scenario: An external partner or client joins a cross-organizational Teams meeting and shares a link to a project management tool (e.g., Asana, Monday.com, or Trello) or a Figma design file. These links are legitimate business artifacts but may have a lower URL reputation score due to high click-through rates from various users or recent domain changes.
*.asana.com, *.monday.com, *.trello.com, *.figma.com) when the conversation type is groupChat or meetingChat and the sender is not a known phishing domain.Scenario: A marketing or sales team uses an external email-to-Teams integration (e.g., Zapier, Power Automate, or a custom webhook) to post incoming customer inquiries or leads into a Teams channel. The external sender domain might be a generic webhook provider (e.g., *.zapier.com, *.powerautomate.com) or a custom domain that hasn’t been