This hypothesis targets adversaries leveraging T1566 (Phishing) by using unverified external Microsoft Teams domains to distribute low-reputation URLs or establish high-volume communication channels before a threat verdict is assigned. Proactively hunting for these signals in Azure Sentinel allows the SOC to identify potential initial access vectors or lateral movement attempts that bypass standard reputation checks, enabling rapid containment before the unverified domains are fully exploited or whitelisted.
//This query surfaces external Microsoft Teams sender domains with NO threat verdict over the last 30 days that are
//still sending low-reputation URLs or unusual volume. Nothing has fired on these senders, so they never appear in a
//threat-based view. A domain skewing to 1:1 chats is an early social-engineering signal, and First/Last Seen let an
//analyst judge whether it is genuinely new to the environment.
//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)),
LowRepUrlMessages = countif(HasSuspUrl),
FirstSeen = min(Timestamp), LastSeen = max(Timestamp)
by SenderDomain
| where ThreatMessages == 0
| where LowRepUrlMessages > 0 or TeamsMessages >= 5
| order by LowRepUrlMessages desc, TeamsMessages desc
| take 20
| project ['Sender Domain']=SenderDomain, ['Teams Messages']=TeamsMessages, ['Distinct Senders']=Senders,
['1:1 Chat']=OneToOne, ['Group Chat']=GroupChat, ['Channel']=ChannelMsgs,
['Low-Rep URL Messages']=LowRepUrlMessages, ['First Seen']=FirstSeen, ['Last Seen']=LastSeen
id: bb9fd713-fc62-4380-930f-91490bbbeb78
name: Suspicious Microsoft Teams Sender Domains Without a Threat Verdict
description: |
This query surfaces external Microsoft Teams sender domains that carry no threat verdict yet but are sending low-reputation URLs or unusual volume.
description-detailed: |
This query surfaces external Microsoft Teams sender domains that have no threat verdict at all over the last 30 days, but which are either sending messages containing URLs on low-reputation top level domains or showing unusual message volume, using Advanced hunting in Microsoft Defender XDR. Results are split by conversation type and include first and last seen. These are the senders that have slipped past detection: by definition nothing has fired on them, so they will never appear in a threat-based view. A domain that skews toward one to one chats is an early social-engineering signal, since attackers open a private conversation before the lure is delivered. First and last seen are returned so an analyst can judge whether a domain is genuinely new to the environment. 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 surfaces external Microsoft Teams sender domains with NO threat verdict over the last 30 days that are
//still sending low-reputation URLs or unusual volume. Nothing has fired on these senders, so they never appear in a
//threat-based view. A domain skewing to 1:1 chats is an early social-engineering signal, and First/Last Seen let an
//analyst judge whether it is genuinely new to the environment.
//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(tody
Scenario: A new SaaS vendor (e.g., Salesforce or HubSpot) is onboarded and configured to send automated notifications via Microsoft Teams. The domain is new to the tenant, so no threat verdict exists yet, but the high volume of automated “task completed” or “alert” messages triggers the “unusual volume” condition.
ServicePrincipal or AppID registered in Azure AD, or maintain a whitelist of verified vendor domains (e.g., *.salesforce.com, *.hubspot.net).Scenario: An IT administrator performs a bulk import of external users or guest accounts for a project, triggering a surge of “Invitation sent” or “User added” notifications from the Microsoft identity platform domains (e.g., login.microsoftonline.com or teams.microsoft.com backend services) that lack a specific threat verdict for the specific subdomain used in the notification payload.
MessageSubject contains keywords like “Invitation,” “Added,” or “Provisioning.”Scenario: A development team uses Jenkins or GitHub Actions to post build status updates to a Teams channel. The webhook sends messages from a custom domain (e.g., ci-cd.example.com) that is not yet in the threat intelligence feed, and the burst of build failures or successes creates a low-reputation URL pattern (e.g., long dynamic build IDs) that looks suspicious.
/build/, `/