4.8 Token Replay Investigation

60 minutes · Module 4 · Free

Token Replay Investigation

Token replay is the post-compromise technique used in AiTM phishing, session hijacking, and token theft attacks. The attacker obtains a valid session or refresh token and replays it to access the victim’s resources without needing their credentials or MFA.

This subsection teaches the specific KQL patterns for detecting and investigating token replay. You will use these patterns extensively in Module 13 (AiTM Investigation) and Module 16 (Token Replay and Session Hijacking).

Where token replay appears

Token replay events appear in AADNonInteractiveUserSignInLogs — not in SigninLogs. The attacker presents a stolen token directly to the token endpoint, which generates a non-interactive sign-in event. There is no login page, no credential prompt, no MFA challenge.

The detection signature

Token replay = successful non-interactive sign-ins from an IP that has no corresponding interactive sign-in from the same user and location. The legitimate user signed in interactively from London. The attacker's token replay appears as a non-interactive sign-in from Lagos. No interactive sign-in from Lagos exists for that user — the token was not earned, it was stolen.

Detection query: token replay from anomalous IP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let lookback = 7d;
let targetUser = "j.morrison@northgateeng.com";
let knownIPs =
    SigninLogs
    | where TimeGenerated > ago(30d)
    | where UserPrincipalName =~ targetUser
    | where ResultType == 0
    | distinct IPAddress;
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(lookback)
| where UserPrincipalName =~ targetUser
| where ResultType == 0
| where IPAddress !in (knownIPs)
| extend Country = tostring(LocationDetails.countryOrRegion)
| project TimeGenerated, IPAddress, Country, AppDisplayName, ResourceDisplayName
| sort by TimeGenerated desc

This query builds a baseline of the user’s known IPs from interactive sign-ins (30-day window), then finds non-interactive sign-ins from IPs not in that baseline. Results are strong indicators of token replay.

Detection query: environment-wide token replay hunting

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let knownUserIPs =
    SigninLogs
    | where TimeGenerated > ago(30d)
    | where ResultType == 0
    | distinct UserPrincipalName, IPAddress;
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(1d)
| where ResultType == 0
| join kind=anti knownUserIPs on UserPrincipalName, IPAddress
| extend Country = tostring(LocationDetails.countryOrRegion)
| summarize
    EventCount = count(),
    Apps = make_set(AppDisplayName),
    Countries = make_set(Country)
    by UserPrincipalName, IPAddress
| where EventCount > 5
| sort by EventCount desc

This hunts across all users: find non-interactive sign-ins from IPs that have never been associated with that user interactively. The EventCount > 5 filter reduces noise from single token refreshes that may be benign.

What to do when you find token replay

  1. Revoke all sessions — this invalidates the stolen token immediately
  2. Force password reset — in case the attacker also has the credentials
  3. Check for post-compromise activity — inbox rules (CloudAppEvents), email access (MailItemsAccessed via Purview), lateral phishing (EmailEvents)
  4. Check device compliance — would a “require compliant device” policy have blocked this?
  5. Document the timeline — when was the token stolen, when was it first used, what was accessed

Try it yourself

Run the per-user token replay detection query in the demo environment (or your tenant) for any user. Compare the IPs in SigninLogs to those in AADNonInteractiveUserSignInLogs. Are there any non-interactive IPs that do not appear in the interactive logs?
In a clean environment, most non-interactive IPs will match interactive IPs — the same device refreshing tokens from the same network. Any mismatch is worth investigating. In the demo environment, you may find mismatches from service-to-service authentication (Microsoft backend infrastructure refreshing tokens on behalf of the user) — these typically resolve to Microsoft-owned IP ranges and are benign.

Check your understanding

1. Why does token replay appear in AADNonInteractiveUserSignInLogs instead of SigninLogs?

The attacker chose to use non-interactive mode
Token replay is blocked from interactive sign-ins
The attacker presents a stolen token directly to the token endpoint without going through a login page — there is no user interaction, so the event is non-interactive by definition

2. Your token replay detection finds non-interactive sign-ins from an IP in a country where the user has never signed in interactively. What is your first containment action?

Revoke all sessions for the affected user — this immediately invalidates the stolen token
Block the IP address in the firewall
Reset the user's password