Query Details
id: d9e5f7a8-b3c4-6d0e-1f2a-3b4c5d6e7f8a
name: Interactive to Non-Interactive Token Theft Pivot
version: 1.0.0
kind: Scheduled
description: |
Detects when a user had a successful interactive sign-in (especially without MFA or with
CA not applied) and then the same session was used for non-interactive token refreshes
from a DIFFERENT IP address and country within a 2-hour window. This is the classic
pattern of an attacker stealing a token immediately after initial authentication.
MITRE ATT&CK: T1528 (Steal Application Access Token), T1539 (Steal Web Session Cookie)
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
- SigninLogs
queryFrequency: 1h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
- LateralMovement
- DefenseEvasion
relevantTechniques:
- T1528
- T1539
- T1078
query: |
// ── IP Exclusion Lists ─────────────────────────────────────────────────────
// Add corporate VPN egress IPs or other known-safe IPs to KnownSafeIPs.
// Add CIDR ranges (VPN subnets, on-prem NAT, Azure VPN Gateway) to KnownSafeNetworks.
// Watchlist alternative:
// let KnownSafeIPs = (_GetWatchlist('VPNExcludedIPs') | project SearchKey);
let KnownSafeIPs = dynamic([
// "203.0.113.50", // Example VPN egress IP 1
// "198.51.100.12", // Example VPN egress IP 2
// "20.x.x.x" // Example Azure VPN Gateway public IP
]);
// CIDR ranges – ipv4_lookup filters both interactive and NI IPs
// Azure datacenter IP ranges: https://aka.ms/azureipranges (add to a Watchlist for large sets)
let KnownSafeNetworks = datatable(network: string) [
// "10.0.0.0/8", // RFC1918 – private / on-prem
// "172.16.0.0/12", // RFC1918 – private / on-prem
// "192.168.0.0/16", // RFC1918 – private / on-prem
// "YOUR_VPN_CIDR/24", // Replace with corporate VPN egress subnet
"0.0.0.0/32" // Placeholder – keeps datatable valid when list is empty
];
// ──────────────────────────────────────────────────────────────────────────
let InteractiveSessions =
SigninLogs
| where TimeGenerated > ago(3h)
| where ResultType == 0
| where AuthenticationRequirement == "singleFactorAuthentication"
or ConditionalAccessStatus == "notApplied"
| where not(IPAddress in (KnownSafeIPs))
| evaluate ipv4_lookup(KnownSafeNetworks, IPAddress, network, return_unmatched = true)
| where isempty(network)
| project-away network
| project
UserPrincipalName,
InteractiveTime = TimeGenerated,
InteractiveIP = IPAddress,
InteractiveCountry = Location,
AppDisplayName,
CorrelationId;
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(3h)
| where ResultType == 0
| where not(IPAddress in (KnownSafeIPs))
| evaluate ipv4_lookup(KnownSafeNetworks, IPAddress, network, return_unmatched = true)
| where isempty(network)
| project-away network
| join kind=inner InteractiveSessions on UserPrincipalName
| where IPAddress != InteractiveIP
and Location != InteractiveCountry
and (TimeGenerated - InteractiveTime) between (1m .. 2h)
| project
NISignInTime = TimeGenerated,
InteractiveTime,
UserPrincipalName,
AppDisplayName,
NIIPAddress = IPAddress,
NICountry = Location,
InteractiveIP,
InteractiveCountry,
CorrelationId,
UniqueTokenIdentifier,
TimeSinceInteractive = (TimeGenerated - InteractiveTime)
| order by UserPrincipalName asc, InteractiveTime asc
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: NIIPAddress
customDetails:
InteractiveIP: InteractiveIP
InteractiveCountry: InteractiveCountry
NonInteractiveCountry: NICountry
AppDisplayName: AppDisplayName
alertDetailsOverride:
alertDisplayNameFormat: "Token Pivot - {{UserPrincipalName}} NI token from {{NIIPAddress}} after interactive auth"
alertDescriptionFormat: "User {{UserPrincipalName}} used a non-interactive token from {{NIIPAddress}} within 2 hours of interactive auth from {{InteractiveIP}}. Possible token theft."
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AnyAlert
groupByEntities:
- Account
groupByAlertDetails: []
groupByCustomDetails: []
This query is designed to detect potential token theft incidents in a network. Here's a simplified explanation:
Purpose: The query identifies situations where a user logs in interactively (like entering a password) and then, within two hours, a non-interactive token (used for automated access) is used from a different IP address and country. This pattern suggests that an attacker might have stolen the user's token right after they logged in.
Data Sources: It uses logs from Azure Active Directory, specifically focusing on interactive and non-interactive sign-in logs.
Exclusions: Known safe IP addresses and networks (like corporate VPNs) are excluded from triggering alerts to reduce false positives.
Process:
Alerting: If such a pattern is detected, it triggers an alert indicating possible token theft. The alert includes details like the user's name, the IP addresses involved, and the time difference between the interactive and non-interactive logins.
Severity and Response: The severity of this alert is marked as high, and it is configured to create an incident for further investigation.
MITRE ATT&CK Techniques: The query is aligned with specific MITRE ATT&CK techniques related to credential access and lateral movement, indicating its relevance to known attack patterns.
Overall, this query helps security teams identify and respond to potential security breaches involving token theft, which could allow attackers unauthorized access to systems.

David Alonso
Released: May 29, 2026
Tables
Keywords
Operators