Adversaries may attempt to steal device codes through phishing to gain unauthorized access to user accounts. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify and mitigate potential credential compromise and lateral movement.
KQL Query
let suspiciousUserClicks = materialize(UrlClickEvents
| where ActionType in ("ClickAllowed", "UrlScanInProgress", "UrlErrorPage") or IsClickedThrough != "0"
| where UrlChain has_any ("microsoft.com/devicelogin", "login.microsoftonline.com/common/oauth2/deviceauth")
| extend AccountUpn = tolower(AccountUpn)
| project ClickTime = Timestamp, ActionType, UrlChain, NetworkMessageId, Url, AccountUpn);
//Check for Risky Sign-In in the short time window
let interestedUsersUpn = suspiciousUserClicks
| where isnotempty(AccountUpn)
| distinct AccountUpn;
let suspiciousSignIns = materialize(AADSignInEventsBeta
| where ErrorCode == 0
| where AccountUpn in~ (interestedUsersUpn)
| where RiskLevelDuringSignIn in (10, 50, 100)
| extend AccountUpn = tolower(AccountUpn)
| join kind=inner suspiciousUserClicks on AccountUpn
| where (Timestamp - ClickTime) between (-2min .. 7min)
| project Timestamp, ReportId, ClickTime, AccountUpn, RiskLevelDuringSignIn, SessionId, IPAddress, Url
);
//Validate errorCode 50199 followed by success in 5 minute time interval for the interested user, which suggests a pause to input the code from the phishing email
let interestedSessionUsers = suspiciousSignIns
| where isnotempty(AccountUpn)
| distinct AccountUpn;
let shortIntervalSignInAttemptUsers = materialize(AADSignInEventsBeta
| where AccountUpn in~ (interestedSessionUsers)
| where ErrorCode in (0, 50199)
| summarize ErrorCodes = make_set(ErrorCode) by AccountUpn, CorrelationId, SessionId
| where ErrorCodes has_all (0, 50199)
| distinct AccountUpn);
suspiciousSignIns
| where AccountUpn in (shortIntervalSignInAttemptUsers)
id: ad76e484-f159-4d23-99ee-e734f0b8b60b
name: Possible device code phishing attempts
description: |
This query helps hunting for possible device code Phishing attempts
description-detailed: |
This query helps hunting for possible device code Phishing attempts. More information: https://www.microsoft.com/en-us/security/blog/2025/02/13/storm-2372-conducts-device-code-phishing-campaign/
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- UrlClickEvents
- AADSignInEventsBeta
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
let suspiciousUserClicks = materialize(UrlClickEvents
| where ActionType in ("ClickAllowed", "UrlScanInProgress", "UrlErrorPage") or IsClickedThrough != "0"
| where UrlChain has_any ("microsoft.com/devicelogin", "login.microsoftonline.com/common/oauth2/deviceauth")
| extend AccountUpn = tolower(AccountUpn)
| project ClickTime = Timestamp, ActionType, UrlChain, NetworkMessageId, Url, AccountUpn);
//Check for Risky Sign-In in the short time window
let interestedUsersUpn = suspiciousUserClicks
| where isnotempty(AccountUpn)
| distinct AccountUpn;
let suspiciousSignIns = materialize(AADSignInEventsBeta
| where ErrorCode == 0
| where AccountUpn in~ (interestedUsersUpn)
| where RiskLevelDuringSignIn in (10, 50, 100)
| extend AccountUpn = tolower(AccountUpn)
| join kind=inner suspiciousUserClicks on AccountUpn
| where (Timestamp - ClickTime) between (-2min .. 7min)
| project Timestamp, ReportId, ClickTime, AccountUpn, RiskLevelDuringSignIn, SessionId, IPAddress, Url
);
//Validate errorCode 50199 followed by success in 5 minute time interval for the interested user, which suggests a pause to input the code from the phishing email
let interestedSessionUsers = suspiciousSignIns
| where isnotempty(AccountUpn)
| distinct AccountUpn;
let shortIntervalSignInAttemptUsers = materialize(AADSignInEventsBeta
| where AccountUpn in~ (interestedSessionUsers)
| where ErrorCode in (0, 50199)
| summarize ErrorCodes = make_set(ErrorCode) by AccountUpn, CorrelationId, SessionId
| where ErrorCodes has_all (0, 50199)
| distinct AccountUpn);
suspiciousSignIns
| where AccountUpn in (shortIntervalSignInAttemptUsers)
version: 1.0.0
| Sentinel Table | Notes |
|---|---|
UrlClickEvents | Ensure this data connector is enabled |
Scenario: Scheduled Job for Device Code Renewal
Description: A legitimate scheduled job runs to renew device codes for user authentication, which may trigger the rule due to the presence of device codes in logs.
Filter/Exclusion: Exclude events where the source is a known system service or job scheduler (e.g., task scheduler or cron job for Azure AD device code renewal). Use a filter like:
(source_process_name = "task scheduler.exe" OR source_process_name = "cron") AND (event_id = 4663) AND (device_code IN ("renew", "refresh"))
Scenario: Admin Task to Reset Device Codes
Description: An administrator manually resets device codes for users during troubleshooting or password reset processes, which may be flagged as phishing attempts.
Filter/Exclusion: Exclude events where the user is a known admin or the action is part of a password reset workflow. Use a filter like:
(user_principal_name = "admin@domain.com" OR user_principal_name LIKE "%admin%") AND (event_id = 4663) AND (device_code IN ("reset", "revoke"))
Scenario: Integration with Identity Provider (IDP) for SSO
Description: A legitimate integration between the enterprise’s identity provider (e.g., Okta, Azure AD) and internal systems may involve device code flows for SSO, which could be misinterpreted as phishing.
Filter/Exclusion: Exclude events where the source is the IDP or a trusted SSO service. Use a filter like:
(source_ip IN ("10.0.0.0/8", "172.16.0.0/12", "192.168