>
av.Amit VijayanCYBERSECURITY & RESEARCHLet\u2019s connect
Back to the library

Threat Hunting Playbooks

Hunting Token Theft via Microsoft Teams Message Exfiltration

Thumbnail for: Hunting Token Theft via Microsoft Teams Message Exfiltration

Why this matters now

Microsoft Teams is where decisions, credentials, and sensitive files move in plain conversation — and a stolen session token opens all of it without a password. Because the token was issued after the user already passed MFA, replaying it typically does not trigger a fresh MFA challenge. Infostealer malware routinely harvests chat-app session tokens alongside browser cookies, and a stolen token can be replayed from anywhere, by anyone who holds it. The theft happens on the endpoint; the payoff happens in the cloud. This playbook hunts both halves.

The pattern to look for is not a failed logon — it is a legitimate-looking session doing illegitimate-volume reading. A user who normally opens a few dozen chats a day suddenly pulls years of message history from an IP address they have never used. Nothing fails. Nothing alerts. The only anomaly is velocity, volume, and novelty.

How the theft works

1. Steal the token on the endpoint

Teams keeps session material in its local app-data folders so the user stays signed in between restarts. Anything running as the user — stealer malware, a malicious script, a compromised utility — can read those folders or copy the files and exfiltrate them. No privilege escalation is strictly required: the token lives at user privilege because the app itself runs at user privilege. The same stealer usually grabs browser cookie stores in the same run, so a Teams-token hit should always prompt a check for browser-token theft on the same host.

2. Replay the token from anywhere

With the token, the attacker calls the messaging APIs as the user: enumerate chats, channels, and teams, then read message history in bulk. There is no password attempt, no MFA prompt, and no new device enrollment to trip an alert. The first observable sign is often a burst of read activity from an IP, device, or geography the user has never used — a session that looks authenticated because, cryptographically, it is.

3. Harvest messages at machine speed

Human reading is slow and selective; token replay is a bulk download. The attacker pulls history in bulk — credentials pasted in chat, internal links, shared files, org-chart context — then moves on to the next account. Velocity is the tell: nobody reads ten thousand messages in an hour by hand.

The KQL hunts

Hunt 1 — Non-Teams processes touching Teams session storage

Legitimate Teams binaries and its updater are the only processes that should routinely write inside the Teams app-data folders. Anything else creating or modifying files there is either a stealer staging data or an installer you should be able to name:

// Hunt: files created/modified inside Teams app-data by non-Teams processes
DeviceFileEvents
| where Timestamp > ago(30d)
| where FolderPath has @"\Microsoft\Teams"
| where InitiatingProcessFileName !in~ ("Teams.exe", "ms-teams.exe",
    "TeamsUpdater.exe", "Update.exe", "setup.exe")
| where ActionType in~ ("FileCreated", "FileModified")
| project Timestamp, DeviceName, FolderPath, FileName, ActionType,
    InitiatingProcessFileName, InitiatingProcessCommandLine,
    InitiatingProcessAccountName, SHA1
| order by Timestamp desc

Baseline first: note which updater and installer names are normal in your fleet, then alert on everything else. A hit from powershell.exe, cmd.exe, or an unsigned binary in Downloads or Temp deserves immediate triage.

Hunt 2 — Command lines referencing Teams token storage

Stealers and copy-paste exfiltration scripts betray themselves in the command line: they name the storage folders they are raiding. Hunt for scripting and archiving tools pointed at Teams paths:

// Hunt: scripting/archiving tools pointed at Teams session storage paths
DeviceProcessEvents
| where Timestamp > ago(30d)
| where ProcessCommandLine has @"\Microsoft\Teams"
| where ProcessCommandLine has_any ("Local Storage", "Cookies",
    "IndexedDB", "Session Storage", "Network")
| where FileName in~ ("powershell.exe", "pwsh.exe", "cmd.exe",
    "xcopy.exe", "robocopy.exe", "7z.exe", "rar.exe", "curl.exe",
    "python.exe", "wscript.exe", "cscript.exe")
| project Timestamp, DeviceName, AccountName, FileName,
    ProcessCommandLine, InitiatingProcessFileName,
    InitiatingProcessAccountName, ProcessId
| order by Timestamp desc

Filter out your own backup, migration, or eDiscovery tooling by name before alerting — those are the legitimate lookalikes for this pattern.

Hunt 3 — Teams read-volume surge from a new location (the SaaS side)

The endpoint hunts catch the theft; this one catches the replay. Baseline each account's daily Teams activity, then flag a large surge — especially one arriving from a country or IP the account has never used:

// Hunt: per-account Teams activity surge vs 14-day baseline
let BaselineDays = 14d;
let LookbackDays = 1d;
let SurgeRatio = 5.0;
CloudAppEvents
| where TimeGenerated > ago(BaselineDays + LookbackDays)
| where Application has "Teams"
| where isnotempty(AccountObjectId)
| summarize DailyActions = count()
    by AccountObjectId, AccountDisplayName, Day = startofday(TimeGenerated)
| extend IsBaseline = Day < startofday(ago(LookbackDays))
| summarize BaselineMedian = percentile_if(DailyActions, 50, IsBaseline),
    BaselineMax = max_if(DailyActions, IsBaseline),
    RecentActions = max_if(DailyActions, not(IsBaseline))
    by AccountObjectId, AccountDisplayName
| extend SurgeVsMedian = toreal(RecentActions) / max(toreal(BaselineMedian), 1.0)
| where SurgeVsMedian >= SurgeRatio
| project AccountDisplayName, AccountObjectId, RecentActions,
    BaselineMedian, BaselineMax, SurgeVsMedian
| order by SurgeVsMedian desc
// Hunt: bulk Teams reads from a country the account has not used in 14 days
let KnownCountries = CloudAppEvents
| where TimeGenerated between (ago(15d) .. ago(1d))
| where Application has "Teams"
| where isnotempty(AccountObjectId)
| summarize KnownCountries = make_set(CountryCode) by AccountObjectId;
CloudAppEvents
| where TimeGenerated > ago(1d)
| where Application has "Teams"
| where isnotempty(AccountObjectId)
| summarize RecentActions = count(), IPs = make_set(IPAddress),
    Countries = make_set(CountryCode) by AccountObjectId, AccountDisplayName
| where RecentActions > 1000
| join kind=inner KnownCountries on AccountObjectId
| where set_difference(Countries, KnownCountries) != dynamic([])
| project AccountDisplayName, AccountObjectId, RecentActions, IPs, Countries
| order by RecentActions desc

Tune the 1000-action threshold to your heaviest legitimate Teams users before turning this into a detection. The signal that matters is the combination — bulk reads plus a new location — not either half alone.

Splunk equivalents

The same hunts, expressed for Splunk with Sysmon and the Microsoft 365 audit feed. Adapt the M365 sourcetype to the add-on in your environment:

SPL 1 — Files created in Teams app-data by non-Teams processes (Sysmon EventCode 11)

index=windows sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=11
| search TargetFilename="*Microsoft\\Teams*"
| search NOT Image IN ("*\\Teams.exe", "*\\ms-teams.exe", "*\\TeamsUpdater.exe", "*\\Update.exe")
| table _time, Computer, Image, TargetFilename, User

SPL 2 — Command lines referencing Teams storage (Sysmon EventCode 1)

index=windows sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1
| search CommandLine="*Microsoft\\Teams*" (CommandLine="*Local Storage*" OR CommandLine="*Cookies*" OR CommandLine="*IndexedDB*")
| search Image IN ("*\\powershell.exe", "*\\pwsh.exe", "*\\cmd.exe", "*\\7z.exe", "*\\curl.exe", "*\\python.exe")
| table _time, Computer, User, Image, CommandLine, ParentImage

SPL 3 — Teams activity surge from a new client IP (M365 audit feed)

sourcetype="ms:o365:management" Workload="MicrosoftTeams"
| bin _time span=1d
| stats count AS DailyActions, dc(ClientIP) AS UniqueIPs, values(ClientIP) AS IPs by UserId, _time
| eventstats avg(DailyActions) AS BaselineAvg by UserId
| where DailyActions > (BaselineAvg * 5) AND DailyActions > 1000
| table _time, UserId, DailyActions, BaselineAvg, UniqueIPs, IPs

Cross-check any SPL 3 hit against Entra sign-in logs: token replay often appears as activity with no matching fresh interactive sign-in from that IP — the session was minted elsewhere and carried in.

Triage: what to do when a hunt hits

1. Endpoint hit. Treat it as active token theft until proven otherwise. Pull the process hash and check whether the same hash appears on other hosts. Check whether the same process also touched browser cookie stores — stealers rarely take only one token type. Capture the process's network connections: the exfiltration destination is your best pivot for scope.

2. SaaS-side hit. Check Entra sign-in logs for the account: is there a fresh interactive sign-in from the suspect IP, or only API activity? Activity without a matching sign-in is the token-replay fingerprint. Check whether the same IP or device touched other accounts — one stolen token often becomes a sweep across the victim's collaborators.

3. Scope the messages. Audit logs show operations, not message content, so you cannot list exactly what the attacker read from logs alone. Assume every chat and channel the account could access was readable, and prioritize rotating any secret that ever appeared in those conversations — credentials, reset links, internal URLs, and file shares.

Remediation and hardening

  • Revoke the session, not just the password. A password reset alone does not kill a live stolen token. Revoke the user's refresh tokens in Entra and force re-authentication, then disable the account during investigation if the replay is confirmed.
  • Turn on Continuous Access Evaluation (CAE). CAE lets Entra revoke access near-real-time when risk signals fire, instead of waiting for the token to expire on its own schedule.
  • Use Conditional Access token protection and device compliance. Token protection binds tokens to the device so a token copied elsewhere is far less useful; requiring compliant devices for Teams access raises the same bar. MFA alone does not stop token replay — it only stops the attacker obtaining a token through interactive sign-in.
  • Shorten token lifetimes where the business allows. Shorter-lived tokens shrink the window a stolen token stays useful, at the cost of more frequent re-authentication.
  • Harden the endpoint. Block unsigned binaries from running out of user-writable folders, alert on access to chat-app storage folders by non-chat processes, and remove local admin where possible so a stealer cannot easily install persistence alongside the theft.
  • Stop secrets in chat. Enforce DLP policies for credentials and tokens in Teams messages, set retention that matches the risk, and treat any secret that appeared in an accessed chat as compromised and rotate it.
  • Watch the account's collaborators. A harvested mailbox of chats maps the victim's trust network. Brief the people and teams whose conversations were exposed, and hunt their accounts with the same queries.

Mapping to MITRE ATT&CK

For detection-engineering bookkeeping, the token-theft chain maps to: T1555 (Credentials from Password Stores — session material in local app storage), T1550.001 (Use Alternate Authentication Material: Application Access Token — token replay), T1078 (Valid Accounts — operating as the victim), T1083 (File and Directory Discovery — locating token stores), T1020 (Automated Exfiltration — bulk message harvest), and T1041 (Exfiltration Over C2 Channel — stealer upload of the token).

The same behavioral logic applies to vendor-account abuse: both attacks succeed without breaking authentication at all — one replays a stolen token, the other abuses a legitimate third-party account. In both cases the detection is behavioral: volume, velocity, and novelty, not a failed logon.

Keep exploring.

All articles

Threat Hunting Playbooks

Hunting Rogue OAuth Consent Grants in Entra ID

One click on a legitimate-looking consent prompt can hand an attacker's app a refresh token to a mailbox that survives password resets. Hunt rogue OAuth grants in Entra ID.

12 min read