In this section
DE3.5 Token Theft and Session Hijacking
Sections 3.2 and 3.3 built email-layer detections. Section 3.4 built identity-layer detections for password spray. This section stays in identity telemetry but targets a different attack stage, what happens after the attacker steals a session token through an AiTM phishing proxy. DE3-004 detects token replay, not the phishing that preceded it. The detection hypothesis shifts from "who is authenticating" to "does the session that follows authentication match the device and location that authenticated?"
Scenario
The CHAIN-HARVEST phishing email from Section 3.2 did its job. A finance team member clicked the link, landed on an AiTM proxy page that looked identical to the Microsoft login portal, entered credentials, completed MFA. The proxy relayed every interaction to the real Microsoft login server and captured the session cookie that Microsoft issued after successful authentication. The attacker now holds a valid session token that includes the MFA claim. They replay that token from a VPS in a different country, accessing Outlook and SharePoint as the compromised user. Microsoft Entra ID sees a valid token with a valid MFA assertion: no new authentication is required. The attacker is inside the tenant with no sign-in event in SigninLogs, because token replay doesn't trigger a new interactive sign-in. The activity appears only in AADNonInteractiveUserSignInLogs, from an IP and device that have never been associated with that user.
Why MFA doesn't prevent token theft
The fundamental misunderstanding about AiTM attacks is that they bypass MFA. They don't. The user completes MFA successfully: the attacker's proxy forwards every authentication challenge to the real identity provider and relays the user's response back. MFA works exactly as designed. The problem is what happens after MFA succeeds.
After successful authentication, the identity provider issues a session token (a cookie, a refresh token, or both) that represents the authenticated session. That token carries the MFA claim, it proves that MFA was completed at the time of issuance. Every subsequent request from the user's browser presents that token instead of re-authenticating. This is how SSO works: authenticate once, use the token for everything else.
The AiTM proxy intercepts that token at the moment of issuance. The attacker now has a token that the identity provider considers fully authenticated, including the MFA claim. When the attacker replays that token from their own infrastructure, the identity provider sees a valid token with a valid MFA assertion. No additional authentication is required. The attacker inherits the user's session.
This is not a failure of MFA. It is a limitation of token-based session management. The token proves that authentication happened, it does not prove that the current presenter of the token is the person who authenticated. Detecting token theft requires a different signal: comparing the infrastructure presenting the token (IP, device, UserAgent) against the infrastructure that originally authenticated.
Figure DE3.5a. Token replay doesn't create a new interactive sign-in. The attacker's activity appears only in AADNonInteractiveUserSignInLogs, from infrastructure that doesn't match the user's original authentication event in SigninLogs. DE3-004 detects the mismatch.
The two tables that tell different stories
Microsoft Entra ID records authentication events across two primary sign-in log tables. SigninLogs captures interactive sign-ins, events where a user actively authenticates (entering credentials, completing MFA, consenting to an app). AADNonInteractiveUserSignInLogs captures non-interactive sign-ins, events where a previously issued token is refreshed or replayed without user involvement.
In a normal session, both tables tell a consistent story. The user authenticates interactively from their corporate laptop at IP 82.34.x.x with device ID DESKTOP-NE-FIN03. Over the next 8 hours, token refreshes appear in the non-interactive logs from the same IP and device. The IP matches. The device matches. The UserAgent matches. This is expected behavior.
In a token theft scenario, the tables diverge. The interactive sign-in records the legitimate authentication from the user's device and IP. Minutes or hours later, non-interactive sign-ins appear from a completely different IP, with an empty device ID (because the attacker's browser isn't registered in Entra ID), and sometimes with a different UserAgent string. The user's legitimate session continues in parallel, they're still working from their device, generating their own non-interactive events. The attacker's replayed token generates a second stream of non-interactive events from different infrastructure.
Run this query to understand the baseline relationship between interactive and non-interactive sign-ins for a specific user:
// Compare interactive and non-interactive sign-in patterns
// Look for IP/device divergence that indicates token replay
let TargetUser = "j.wheeler@northgate-eng.com";
let LookbackWindow = 24h;
let InteractiveSessions =
SigninLogs
| where TimeGenerated > ago(LookbackWindow)
| where UserPrincipalName == TargetUser
| where ResultType == "0"
| extend DeviceId = tostring(DeviceDetail.deviceId)
| project
TimeGenerated, IPAddress, DeviceId,
UserAgent, AppDisplayName,
tostring(LocationDetails.city),
tostring(LocationDetails.countryOrRegion),
SignInType = "Interactive";
let NonInteractiveSessions =
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(LookbackWindow)
| where UserPrincipalName == TargetUser
| where ResultType == "0"
| extend DeviceId = tostring(DeviceDetail.deviceId)
| project
TimeGenerated, IPAddress, DeviceId,
UserAgent, AppDisplayName,
tostring(LocationDetails.city),
tostring(LocationDetails.countryOrRegion),
SignInType = "NonInteractive";
union InteractiveSessions, NonInteractiveSessions
| sort by TimeGenerated asc
In a clean environment, the IP addresses and device IDs in both result sets align. When you see a non-interactive sign-in from an IP that never appeared in the interactive sign-ins, particularly if the device ID is empty, you're looking at a potential token replay.
Building the cross-table detection rule
DE3-004 correlates interactive sign-ins with non-interactive sign-ins for the same user within a defined time window, flagging cases where the non-interactive session originates from infrastructure that doesn't match any of the user's recent interactive authentications.
// DE3-004: Detect token replay from mismatched infrastructure
// Hypothesis: non-interactive sign-ins from IPs/devices that
// never appear in the user's interactive auth history indicate
// a stolen token replayed from attacker infrastructure
let DetectionWindow = 1h;
let BaselineWindow = 14d;
// Step 1: Build per-user known IP baseline from interactive sign-ins
let UserKnownIPs = materialize(
SigninLogs
| where TimeGenerated > ago(BaselineWindow)
| where ResultType == "0"
| summarize
KnownIPs = make_set(IPAddress, 50),
KnownDevices = make_set(
tostring(DeviceDetail.deviceId), 20)
by UserPrincipalName
);
// Step 2: Find non-interactive sign-ins from unknown IPs
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(DetectionWindow)
| where ResultType == "0"
| extend DeviceId = tostring(DeviceDetail.deviceId)
| join kind=inner UserKnownIPs
on UserPrincipalName
// Step 3: Flag sessions from unknown infrastructure
| where KnownIPs !has IPAddress
| where DeviceId == "" or KnownDevices !has DeviceId
| summarize
EventCount = count(),
Apps = make_set(AppDisplayName, 10),
Resources = make_set(ResourceDisplayName, 10),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by UserPrincipalName, IPAddress, DeviceId,
tostring(LocationDetails.city),
tostring(LocationDetails.countryOrRegion)
| where EventCount >= 2
| project
UserPrincipalName, IPAddress,
City = LocationDetails_city,
Country = LocationDetails_countryOrRegion,
DeviceId, EventCount, Apps, Resources,
FirstSeen, LastSeen
Walk through the architecture. Step 1 builds a 14-day baseline of IPs and device IDs from the user's interactive sign-ins. Fourteen days captures work-from-home IPs, office IPs, mobile hotspot IPs, and typical travel patterns. The materialize function caches this baseline so the join doesn't recompute it for every row in the non-interactive logs.
Step 2 finds successful non-interactive sign-ins in the detection window and joins them against the user's known infrastructure. Step 3 applies two filters: the IP must not appear in the user's known IP set, AND the device ID must either be empty (the most common indicator, attackers' browsers aren't Entra ID registered) or unknown. The EventCount >= 2 threshold prevents single token refresh events from firing the rule: a single non-interactive event from a new IP could be a CDN edge change or a mobile network handoff. Two or more events from the same unknown IP within the detection window is a stronger signal.
The Apps and Resources fields in the output tell the analyst what the attacker is accessing. Token theft followed by immediate access to Exchange Online and SharePoint is the canonical CHAIN-HARVEST pattern: the attacker reads email for BEC reconnaissance and downloads documents for data exfiltration.
Anti-Pattern
Microsoft Entra ID Protection includes an "Anomalous Token" risk detection that fires when a token is used from an unfamiliar location. This detection is valuable but has two limitations for production SOC operations. First, it's an offline detection. Microsoft processes it asynchronously, and the alert can arrive hours after the replay. Second, it requires an Entra ID P2 license, which not every tenant has. DE3-004 provides a real-time detection using log data available to any tenant with Sentinel and the Microsoft Entra ID connector enabled.
Detecting UserAgent anomalies within authentication sessions
AiTM phishing kits like Sneaky2FA introduce a distinctive artifact: multiple different UserAgent strings within a single authentication correlation ID. When the phishing proxy relays the user's authentication to Microsoft, it rotates through a set of predefined UserAgent strings at each stage of the authentication flow. A legitimate browser session uses one consistent UserAgent throughout the entire sign-in process. An AiTM-proxied session uses two, three, or five different ones.
// Detect AiTM proxy kits via UserAgent rotation during auth
// Legitimate sessions: 1 UserAgent per CorrelationId
// AiTM-proxied sessions: 2+ UserAgents per CorrelationId
SigninLogs
| where TimeGenerated > ago(1h)
| where isnotempty(CorrelationId)
| where DeviceDetail.deviceId == ""
| summarize
UserAgents = make_set(UserAgent),
UACount = dcount(UserAgent),
ResultTypes = make_set(ResultType),
AuthSucceeded = countif(ResultType == "0")
by CorrelationId, UserPrincipalName, IPAddress
| where UACount > 2
| where AuthSucceeded > 0
| project
CorrelationId, UserPrincipalName,
IPAddress, UACount, UserAgents,
AuthSucceeded
The UACount > 2 threshold comes from observed AiTM kit behavior. Sneaky2FA rotates through five UserAgents during a single authentication flow. A legitimate user might produce two UserAgent entries if they switch between a mobile and desktop browser during an authentication flow that involves a redirect, but producing three or more within a single correlation ID is abnormal. The DeviceDetail.deviceId == "" pre-filter limits the query to unregistered devices, which eliminates most legitimate enterprise authentication.
This detection catches the phishing event itself: the moment the AiTM proxy is authenticating through the user. DE3-004's cross-table rule catches the subsequent replay. Together, they provide coverage at two points in the kill chain: the credential capture and the token use.
Building a device fingerprint baseline
The cross-table detection relies on knowing which IPs and devices are normal for each user. In environments with a mobile workforce, VPN pools, or frequent travel, the "known IP" approach generates false positives. A complementary signal is the device fingerprint, specifically, whether the device presenting the token is registered in Entra ID.
// Users who normally sign in from registered devices
// suddenly accessing resources from unregistered devices
let SensitiveApps = dynamic([
"Microsoft Exchange Online",
"Microsoft SharePoint Online",
"Microsoft Teams",
"Azure Portal"]);
// Step 1: Users who consistently use registered devices
let RegisteredDeviceUsers =
SigninLogs
| where TimeGenerated > ago(14d)
| where ResultType == "0"
| extend DeviceId = tostring(DeviceDetail.deviceId)
| summarize
TotalSignins = count(),
RegisteredSignins = countif(DeviceId != "")
by UserPrincipalName
| where TotalSignins > 5
| where RegisteredSignins * 1.0 / TotalSignins > 0.8
| project UserPrincipalName;
// Step 2: Unregistered device sessions for these users
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(1h)
| where ResultType == "0"
| where AppDisplayName in (SensitiveApps)
| extend DeviceId = tostring(DeviceDetail.deviceId)
| where DeviceId == ""
| where UserPrincipalName in (RegisteredDeviceUsers)
| project
TimeGenerated, UserPrincipalName,
IPAddress, AppDisplayName,
ResourceDisplayName, UserAgent,
tostring(LocationDetails.city),
tostring(LocationDetails.countryOrRegion)
This approach inverts the detection logic. Instead of asking "does this IP match the user's history," it asks "does this user normally authenticate from registered devices, and is this session coming from an unregistered one?" A user who authenticates from registered devices 80% or more of the time has a clear device hygiene pattern. When that user's token suddenly appears from an unregistered device accessing Exchange Online, the anomaly is significant regardless of the IP address.
The SensitiveApps filter focuses the detection on applications that attackers target after token theft, email for BEC reconnaissance, SharePoint for document exfiltration, Teams for internal communication monitoring, and Azure Portal for privilege escalation. Token refreshes against the Entra ID service itself or Microsoft Graph are noisier and lower value for initial detection.
Response: token revocation and session invalidation
When DE3-004 fires, the response must invalidate the stolen token before the attacker completes their objective. Microsoft provides two PowerShell cmdlets that work together:
# Revoke all refresh tokens for the compromised user
# This forces re-authentication on the next token refresh
$UserId = "j.wheeler@northgate-eng.com"
# Step 1: Revoke refresh tokens via Microsoft Graph
Invoke-MgGraphRequest `
-Method POST `
-Uri "https://graph.microsoft.com/v1.0/users/$UserId/revokeSignInSessions"
# Step 2: Force password reset (prevents re-compromise)
Update-MgUser -UserId $UserId `
-PasswordProfile @{
ForceChangePasswordNextSignIn = $true
ForceChangePasswordNextSignInWithMfa = $true
}
# Step 3: Check for persistence — new MFA methods or app consents
Get-MgUserAuthenticationMethod -UserId $UserId |
Select-Object Id, @{N="Type";E={$_.'@odata.type'}},
@{N="DisplayName";E={$_.DisplayName}}
# Step 4: Review app consent grants for OAuth persistence
Get-MgUserOAuth2PermissionGrant -UserId $UserId |
Where-Object { $_.ConsentType -eq "Principal" } |
Select-Object ClientId, Scope, StartDateTime
Step 1 revokes all refresh tokens. Access tokens remain valid until they expire (typically 60–90 minutes), but the attacker can't renew them. Step 2 forces a password reset with MFA, which prevents the attacker from re-authenticating with the stolen password. Steps 3 and 4 check for persistence, sophisticated attackers register a new MFA method or create an OAuth app consent grant within minutes of token theft, giving them a backdoor that survives token revocation and password reset.
The gap between revocation and access token expiry is the window of risk. If your organization has enabled Continuous Access Evaluation (CAE), this window shrinks to near-zero for supported applications. CAE-enabled apps re-validate the token in near real time when a revocation event occurs. Check your conditional access policies to confirm CAE is enabled.
The rule specification
Rule ID: DE3-004-Token-Replay-Cross-Table
Hypothesis: Non-interactive sign-in events from IPs and devices that have never appeared in a user's interactive authentication history represent stolen session tokens replayed from attacker infrastructure. The combination of unknown IP, empty device ID, and access to sensitive applications produces a high-confidence token replay signal.
ATT&CK: T1550.001. Use Alternate Authentication Material: Application Access Token. Also maps to T1557. Adversary-in-the-Middle (the preceding attack that enables the token theft).
Data source: AADNonInteractiveUserSignInLogs (primary), SigninLogs (baseline). Cross-table join on UserPrincipalName.
Frequency: Every 1 hour with 2-hour lookback. Token replay can begin minutes after the AiTM phishing, so hourly detection keeps the response window tight.
Severity: High. Token replay with access to Exchange Online or SharePoint is a confirmed compromise, not a suspicious indicator.
Entity mapping: UserPrincipalName → Account, IPAddress → IP, Apps → CloudApplication. The analyst sees the compromised user, the attacker's IP, and which applications the attacker accessed.
False positive sources: VPN pool rotation, corporate VPN users whose exit IP changes mid-session produce non-interactive events from IPs not in their interactive baseline. Mitigate by adding your VPN egress ranges to an exclusion watchlist. BYOD without Entra ID registration, personal devices that aren't registered produce empty device IDs legitimately. The RegisteredDeviceUsers variant of the rule filters these out by only alerting on users whose baseline shows consistent registered-device usage. Mobile network handoffs, cellular users switching between towers get new IPs. These typically produce single events, not sustained sessions: the EventCount >= 2 threshold handles most of these.