Disclaimer: All research and opinions expressed here are my own and are independent of any employer or organisation.

Token Protection Switch with Different IP

hunt-2025-038 v1.0 Entra created Paul Newton

Hypothesis

Threat Actors will use token theft as a means of initial access, privilege escalation and persistence. Combined with phishing as a means for initial access.

Token Protection Switch with Different IP

This analytic looks at authentications in both SigninLogs and AADNonInteractiveUserSignInLogs, where the initial authentication is bound to a device, and the 2nd is unbound, and comes from a different IP address. This is also restricted to the "Microsoft Azure CLI" application, and should hopefully catch context shifts for authentications to the app. It also looks at a time period of 3h for the authentications, where the two different authentications occur within 10 minutes of each other. These thresholds can easily be adjusted.

KQL
let TimeWindow = 10m;
SigninLogs
| where TimeGenerated > ago(3h)
| where AppDisplayName == "Microsoft Azure CLI"
| where ResultType == 0
| extend 
    DeviceDetailParsed = parse_json(DeviceDetail),
    LocationDetailParsed = parse_json(LocationDetails),
    TokenProtection = parse_json(TokenProtectionStatusDetails)
| extend 
    DeviceName = tostring(DeviceDetailParsed.displayName),
    Location = tostring(LocationDetailParsed.countryOrRegion),
    SessionStatus = tostring(TokenProtection.signInSessionStatus)
| where IncomingTokenType == "primaryRefreshToken" and SessionStatus == "bound"
| project 
    InteractiveTime = TimeGenerated,
    UserPrincipalName,
    DeviceName,
    VictimIP = IPAddress,
    VictimLocation = Location,
    VictimASN = AutonomousSystemNumber
| join kind=inner (
    AADNonInteractiveUserSignInLogs
    | where TimeGenerated > ago(3h)
    | where AppDisplayName == "Microsoft Azure CLI"
    | extend 
        DeviceDetailParsed = parse_json(DeviceDetail),
        LocationDetailParsed = parse_json(LocationDetails),
        TokenProtection = parse_json(TokenProtectionStatusDetails)
    | extend 
        DeviceName = tostring(DeviceDetailParsed.displayName),
        Location = tostring(LocationDetailParsed.countryOrRegion),
        SessionStatus = tostring(TokenProtection.signInSessionStatus)
    | where SessionStatus == "unbound"
    | project 
        NonInteractiveTime = TimeGenerated,
        UserPrincipalName,
        DeviceName,
        AttackerIP = IPAddress,
        AttackerLocation = Location,
        AttackerASN = AutonomousSystemNumber,
        ResourceDisplayName
) on UserPrincipalName, DeviceName
| where NonInteractiveTime between (InteractiveTime .. (InteractiveTime + TimeWindow))
| where VictimIP != AttackerIP
| project 
    InteractiveTime,
    NonInteractiveTime,
    TimeDiff = NonInteractiveTime - InteractiveTime,
    UserPrincipalName,
    DeviceName,
    VictimIP,
    AttackerIP,
    VictimLocation,
    AttackerLocation,
    VictimASN,
    AttackerASN,
    ResourceDisplayName

Triage steps

  1. 1 Review the time difference between the bound and unbound authentications - very short intervals (under 5 minutes) are highly suspicious
  2. 2 Verify if the user was legitimately travelling or using a VPN between the two authentications
  3. 3 Check the IP reputation and ASN details for the attacker IP - look for VPS providers, proxies, or known malicious infrastructure
  4. 4 Compare the victim and attacker geolocations - impossible travel scenarios are strong indicators of compromise
  5. 5 Review the user's recent sign-in history for other suspicious activity or additional context switches
  6. 6 Verify if the user performed any copy/paste actions or visited suspicious URLs prior to the token protection switch
  7. 7 Check if multiple users show the same pattern, which could indicate a broader phishing campaign
  8. 8 If confirmed compromise, immediately revoke all active sessions and tokens for the affected user
  9. 9 Reset user credentials and review what resources were accessed using the unbound token

True positive example

InteractiveTime
17/12/2025, 15:58:06.000
NonInteractiveTime
17/12/2025, 15:58:56.000
TimeDiff
00:00:50
UserPrincipalName
[email protected]
DeviceName
HUNTERWORK
VictimIP
86.142.241.139
AttackerIP
159.28.115.7
VictimLocation
GB
AttackerLocation
DK
VictimASN
2856
AttackerASN
208172
ResourceDisplayName
Azure Resource Manager

ConsentFix phishing attack detected - User authenticated from GB (86.142.241.139) with a bound PRT token, followed 50 seconds later by an unbound token authentication from DK (159.28.115.7). The rapid context switch from a compliant device to an unbound session, combined with the geographic discrepancy (GB to Denmark) and short time window, strongly indicates a stolen authorization code from a ConsentFix-style phishing attack. The attacker exchanged the stolen auth code for access tokens and immediately used them to access Azure Resource Manager.

Back to all hunts