SOCLIFE
SOCLIFE

Evidence-led security analysis

Published knowledge

Search SOC//LIFE

HuntsHUNT-006

Hypothesis-led threat hunting

Hunt for Device Code Abuse Across Risky Identities

Starts from active Entra user risk, intersects it with device-code authentication, validates 50199-to-success and phishing-click patterns, and scopes post-compromise device and mailbox activity.

Question

Hunt goal

One or more medium/high risk identities may have completed attacker-controlled device-code authentication and then been used for token-backed cloud access.

Why this hunt

Microsoft documented a widespread AI-enabled device-code phishing campaign in April 2026 and observed device-code phishing again in July 2026 in CaptiveCrunch operations. Both reinforce the value of combining identity risk, authentication flow, short authentication sequences, and post-compromise cloud telemetry.

Data sources

Where to look

  • IdentityMicrosoft Entra ID Protection user-risk events with identity, event type, risk level/state, source IP, and sign-in correlation identifiers.
  • AuthenticationMicrosoft Entra and Defender XDR sign-in telemetry with authentication protocol, result/error, user risk, application, device, IP, session, and correlation context.
  • EmailDefender for Office 365 URL click telemetry with user, URL chain, message ID, action, and click timestamp.
  • SaaS AuditDefender XDR cloud application events for device registration, Exchange actions, and uncommon user activity after compromise.

Search path

Hunt steps

Each search broadens the view from patient zero to related users, devices, infrastructure, and follow-on activity.
  1. Step 1First search

    Rank active medium/high risk users

    Finding

    The initial search ranks medium/high at-risk identities and preserves the risk event types, IPs, and correlation context.

    Identity risk defines the population; it does not by itself prove compromise.

    View query
    Q-01First search

    Rank active medium/high risk users

    What this checks

    Build the identity-first hunt population before looking at device-code flow.

    KQL
    AADUserRiskEvents
    | where TimeGenerated >= ago(7d)
    | where RiskLevel in~ ("medium","high")
    | where RiskState in~ ("atRisk","confirmedCompromised")
    | extend AccountUpn=tolower(UserPrincipalName)
    | summarize
        FirstRisk=min(TimeGenerated),
        LastRisk=max(TimeGenerated),
        RiskEvents=make_set(RiskEventType,50),
        RiskLevels=make_set(RiskLevel,10),
        RiskStates=make_set(RiskState,10),
        SourceIPs=make_set(IpAddress,50),
        Correlations=make_set(CorrelationId,50)
        by AccountUpn, UserId
    | order by LastRisk desc
    SPL
    index=<entra_risk_index> sourcetype=<entra_user_risk_sourcetype>
    earliest=-7d
    | eval
        user=lower(coalesce(user,UserPrincipalName,user_principal_name)),
        risk_type=coalesce(risk_type,RiskEventType),
        risk_level=lower(coalesce(risk_level,RiskLevel)),
        risk_state=lower(coalesce(risk_state,RiskState)),
        src=coalesce(src,IpAddress),
        correlation_id=coalesce(correlation_id,CorrelationId)
    | where risk_level IN ("medium","high") AND risk_state IN ("atrisk","confirmedcompromised")
    | stats
        min(_time) as first_risk
        max(_time) as last_risk
        values(risk_type) as risk_events
        values(risk_level) as risk_levels
        values(risk_state) as risk_states
        values(src) as source_ips
        values(correlation_id) as correlations
        by user
    | convert ctime(first_risk) ctime(last_risk)
    | sort - last_risk
    What to look for

    A prioritized set of risky identities with risk types, IPs, timing, and correlation identifiers.

    Technical details
    Tested signal

    Medium/high active or confirmed-compromised user-risk events.

    Assumptions
    • Entra ID Protection user-risk telemetry is available.
    Data requirements and relevant fields
    identity

    Microsoft Entra ID Protection user-risk events with identity, event type, risk level/state, source IP, and sign-in correlation identifiers.

    • TimeGenerated
    • ActivityDateTime
    • DetectedDateTime
    • UserPrincipalName
    • UserId
    • IpAddress
    • CorrelationId
    • RequestId
    • RiskEventType
    • RiskLevel
    • RiskState
    • RiskDetail
    • DetectionTimingType
    • Activity
    KQL schema

    Validate table availability, Entra ID Protection licensing, connector retention, and local field population.

    SPL schema

    Replace index/sourcetype placeholders and map user-risk and sign-in concepts to the local Entra export.

    Limitations
    • Risk can be benign or delayed; it is a starting population, not a verdict.

    KQL uses Microsoft Entra / Defender XDR identity telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

  2. Step 2Pivot

    Find successful device-code use by risky identities

    Finding

    Risky users with first-seen or unusual device-code use become the highest-priority authentication candidates.

    Expected CLI/shared-device scenarios must be separated from ordinary user identities.

    View query
    Q-02Pivot

    Find successful device-code use by risky identities

    What this checks

    Intersect the risky-user population with successful device-code authentication and show whether the user has prior legitimate history.

    KQL
    let current_window = 7d;
    let baseline_window = 30d;
    
    let active_risk =
        AADUserRiskEvents
        | where TimeGenerated >= ago(current_window)
        | where RiskLevel in~ ("medium", "high")
        | where RiskState in~ ("atRisk", "confirmedCompromised")
        | extend AccountUpn=tolower(UserPrincipalName)
        | summarize
            RiskTime=max(TimeGenerated),
            RiskEvents=make_set(RiskEventType, 20),
            RiskLevels=make_set(RiskLevel, 10),
            RiskStates=make_set(RiskState, 10),
            RiskIPs=make_set(IpAddress, 20),
            RiskCorrelations=make_set(CorrelationId, 20)
            by AccountUpn;
    
    let historical_device_code =
        SigninLogs
        | where TimeGenerated between (ago(baseline_window) .. ago(current_window))
        | where AuthenticationProtocol =~ "deviceCode"
        | where tostring(ResultType) == "0"
        | extend AccountUpn=tolower(UserPrincipalName)
        | summarize PriorDeviceCode=count() by AccountUpn;
    
    SigninLogs
    | where TimeGenerated >= ago(current_window)
    | where AuthenticationProtocol =~ "deviceCode"
    | where tostring(ResultType) == "0"
    | extend AccountUpn=tolower(UserPrincipalName)
    | join kind=inner active_risk on AccountUpn
    | where TimeGenerated between (RiskTime - 30m .. RiskTime + 30m)
    | join kind=leftouter historical_device_code on AccountUpn
    | extend PriorDeviceCode=coalesce(PriorDeviceCode,0)
    | where PriorDeviceCode == 0
    | project
        TimeGenerated,
        RiskTime,
        AccountUpn,
        IPAddress,
        AuthenticationProtocol,
        AuthenticationRequirement,
        RiskLevelDuringSignIn,
        RiskLevelAggregated,
        RiskState,
        AppDisplayName,
        AppId,
        ResourceDisplayName,
        ConditionalAccessStatus,
        DeviceDetail,
        CorrelationId,
        SessionId,
        RiskEvents,
        RiskLevels,
        RiskStates,
        RiskIPs,
        PriorDeviceCode
    | order by TimeGenerated desc
    SPL
    (
        index=<entra_risk_index> sourcetype=<entra_user_risk_sourcetype> earliest=-7d
    )
    OR
    (
        index=<entra_signin_index> sourcetype=<entra_signin_sourcetype> earliest=-30d
    )
    | eval
        user=lower(coalesce(user, UserPrincipalName, user_principal_name)),
        event_type=case(
            isnotnull(RiskEventType),"risk",
            lower(coalesce(AuthenticationProtocol,auth_protocol))="devicecode","device_code_signin",
            true(),"other"
        ),
        risk_level=lower(coalesce(risk_level,RiskLevel)),
        risk_state=lower(coalesce(risk_state,RiskState)),
        result=coalesce(result,ResultType),
        src=coalesce(src,IPAddress,IpAddress),
        app_name=coalesce(app_name,AppDisplayName),
        session_id=coalesce(session_id,SessionId),
        correlation_id=coalesce(correlation_id,CorrelationId),
        is_current=if(_time>=relative_time(now(),"-7d"),1,0)
    | eventstats
        count(eval(event_type="device_code_signin" AND tostring(result)="0" AND is_current=0)) as prior_device_code
        by user
    | sort 0 user _time
    | streamstats current=f
        last(eval(if(event_type="risk" AND risk_level IN ("medium","high") AND risk_state IN ("atrisk","confirmedcompromised"),_time,null()))) as risk_time
        last(eval(if(event_type="risk",risk_level,null()))) as last_risk_level
        last(eval(if(event_type="risk",risk_state,null()))) as last_risk_state
        by user
    | where
        event_type="device_code_signin"
        AND tostring(result)="0"
        AND is_current=1
        AND prior_device_code=0
        AND isnotnull(risk_time)
        AND abs(_time-risk_time)<=1800
    | table _time risk_time user src app_name session_id correlation_id last_risk_level last_risk_state prior_device_code
    | sort - _time
    What to look for

    Risky users with device-code authentication, prioritized by lack of historical device-code usage and sign-in risk.

    Technical details
    Tested signal

    Risky identity plus successful device-code authentication.

    Assumptions
    • SigninLogs and user-risk telemetry share a consistent UPN.
    Data requirements and relevant fields
    identity

    Microsoft Entra ID Protection user-risk events with identity, event type, risk level/state, source IP, and sign-in correlation identifiers.

    • TimeGenerated
    • ActivityDateTime
    • DetectedDateTime
    • UserPrincipalName
    • UserId
    • IpAddress
    • CorrelationId
    • RequestId
    • RiskEventType
    • RiskLevel
    • RiskState
    • RiskDetail
    • DetectionTimingType
    • Activity
    authentication

    Microsoft Entra sign-in logs with authentication protocol, risk state, result, application, device, IP, and correlation/session context.

    • TimeGenerated
    • UserPrincipalName
    • UserId
    • IPAddress
    • AuthenticationProtocol
    • AuthenticationRequirement
    • ResultType
    • ResultDescription
    • RiskLevelDuringSignIn
    • RiskLevelAggregated
    • RiskState
    • CorrelationId
    • SessionId
    • AppDisplayName
    • AppId
    • ResourceDisplayName
    • ConditionalAccessStatus
    • DeviceDetail
    • AutonomousSystemNumber
    KQL schema

    Validate table availability, Entra ID Protection licensing, connector retention, and local field population.

    SPL schema

    Replace index/sourcetype placeholders and map user-risk and sign-in concepts to the local Entra export.

    Limitations
    • Normal CLI/shared-device workflows can still appear.

    KQL uses Microsoft Entra / Defender XDR identity telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

  3. Step 3Pivot

    Find 50199-to-success authentication pairs

    Finding

    The 50199-to-success sequence provides a campaign-relevant authentication pivot without depending on known infrastructure.

    The sequence needs device-code and risk context because 50199 is not inherently malicious.

    View query
    Q-03Pivot

    Find 50199-to-success authentication pairs

    What this checks

    Search the environment for Microsoft's documented user-interrupt-to-success pattern without requiring known infrastructure.

    KQL
    EntraIdSigninEvents
    | where Timestamp >= ago(7d)
    | where ErrorCode in (0,50199)
    | summarize
        FirstSeen=min(Timestamp),
        LastSeen=max(Timestamp),
        ErrorCodes=make_set(ErrorCode),
        IPAddresses=make_set(IPAddress,20),
        RiskLevels=make_set(RiskLevelDuringSignin,10),
        Applications=make_set(Application,20)
        by AccountUpn, CorrelationId, SessionId
    | where ErrorCodes has_all (0,50199)
    | extend Duration=LastSeen-FirstSeen
    | where Duration <= 5m
    | order by FirstSeen desc
    SPL
    index=<entra_signin_index> sourcetype=<defender_entra_signin_sourcetype>
    earliest=-7d
    | eval
        user=lower(coalesce(user,AccountUpn)),
        error_code=coalesce(error_code,ErrorCode),
        correlation_id=coalesce(correlation_id,CorrelationId),
        session_id=coalesce(session_id,SessionId),
        src=coalesce(src,IPAddress)
    | where error_code IN (0,50199)
    | stats
        min(_time) as first_seen
        max(_time) as last_seen
        values(error_code) as error_codes
        values(src) as source_ips
        values(RiskLevelDuringSignin) as risk_levels
        by user correlation_id session_id
    | eval duration=last_seen-first_seen
    | where mvfind(error_codes,"50199")>=0 AND mvfind(error_codes,"0")>=0 AND duration<=300
    | convert ctime(first_seen) ctime(last_seen)
    | sort - first_seen
    What to look for

    Users with the 50199-to-success sequence who can be intersected with user risk and device-code activity.

    Technical details
    Tested signal

    50199 and success for the same user/session/correlation context within five minutes.

    Assumptions
    • Defender XDR Entra sign-in events are available.
    Data requirements and relevant fields
    authentication

    Microsoft Defender XDR Entra sign-in events used for the documented 50199-to-success device-code investigation pattern.

    • Timestamp
    • AccountUpn
    • AccountObjectId
    • IPAddress
    • ErrorCode
    • RiskLevelDuringSignin
    • CorrelationId
    • SessionId
    • Application
    • ResourceDisplayName
    • Call
    • ReportId
    KQL schema

    Validate table availability, Entra ID Protection licensing, connector retention, and local field population.

    SPL schema

    Replace index/sourcetype placeholders and map user-risk and sign-in concepts to the local Entra export.

    Limitations
    • The sequence alone is not malicious.

    KQL uses Microsoft Entra / Defender XDR identity telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

  4. Step 4Pivot

    Correlate phishing clicks with risky authentication

    Finding

    Click-to-risky-sign-in correlation can tie the identity compromise back to a phishing delivery path.

    Preserve the original message and URL chain before making a containment decision.

    View query
    Q-04Pivot

    Correlate phishing clicks with risky authentication

    What this checks

    Hunt the Microsoft-published pattern of a URL click followed by a successful risky sign-in for the same identity.

    KQL
    let clicks =
        UrlClickEvents
        | where Timestamp >= ago(7d)
        | extend AccountUpn=tolower(AccountUpn)
            | project ClickTime=Timestamp, ActionType, UrlChain, NetworkMessageId, Url, AccountUpn;
    EntraIdSigninEvents
    | where Timestamp >= ago(7d)
    | where ErrorCode == 0
    | where RiskLevelDuringSignin in (10, 50, 100)
    | extend AccountUpn=tolower(AccountUpn)
    | join kind=inner clicks on AccountUpn
    | where (Timestamp - ClickTime) between (-2m .. 7m)
    | project Timestamp, ClickTime, AccountUpn, RiskLevelDuringSignin, SessionId, CorrelationId, IPAddress, Url, UrlChain, NetworkMessageId, ActionType
    | order by Timestamp asc
    SPL
    (
        index=<url_click_index> sourcetype=<defender_url_click_sourcetype> earliest=-7d
    )
    OR
    (
        index=<entra_signin_index> sourcetype=<defender_entra_signin_sourcetype> earliest=-7d
    )
    | eval
        user=lower(coalesce(user,AccountUpn)),
        event_type=case(
            isnotnull(NetworkMessageId),"url_click",
            isnotnull(ErrorCode),"signin",
            true(),"other"
        ),
        error_code=coalesce(error_code,ErrorCode),
        signin_risk=coalesce(signin_risk,RiskLevelDuringSignin),
        session_id=coalesce(session_id,SessionId),
        correlation_id=coalesce(correlation_id,CorrelationId),
        src=coalesce(src,IPAddress)
    | sort 0 user _time
    | streamstats current=f
        last(eval(if(event_type="url_click",_time,null()))) as click_time
        last(eval(if(event_type="url_click",Url,null()))) as clicked_url
        last(eval(if(event_type="url_click",NetworkMessageId,null()))) as message_id
        by user
    | where event_type="signin" AND error_code=0 AND signin_risk IN (10,50,100)
        AND isnotnull(click_time) AND (_time-click_time)>=-120 AND (_time-click_time)<=420
    | table _time click_time user signin_risk src session_id correlation_id clicked_url message_id
    | sort 0 _time
    What to look for

    Risky sign-ins tightly following email URL clicks, suitable for device-code and session review.

    Technical details
    Tested signal

    URL click and risky successful sign-in within a short interval.

    Assumptions
    • Defender for Office 365 URL click and Entra sign-in telemetry are available.
    Data requirements and relevant fields
    email

    Defender for Office 365 URL click telemetry with user, URL chain, message ID, action, and click timestamp.

    • Timestamp
    • AccountUpn
    • ActionType
    • Url
    • UrlChain
    • NetworkMessageId
    • Workload
    authentication

    Microsoft Defender XDR Entra sign-in events used for the documented 50199-to-success device-code investigation pattern.

    • Timestamp
    • AccountUpn
    • AccountObjectId
    • IPAddress
    • ErrorCode
    • RiskLevelDuringSignin
    • CorrelationId
    • SessionId
    • Application
    • ResourceDisplayName
    • Call
    • ReportId
    KQL schema

    Validate table availability, Entra ID Protection licensing, connector retention, and local field population.

    SPL schema

    Replace index/sourcetype placeholders and map user-risk and sign-in concepts to the local Entra export.

    Limitations
    • A legitimate click and unrelated risky sign-in can coincide.

    KQL uses Microsoft Entra / Defender XDR identity telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

  5. Step 5Pivot

    Hunt post-compromise device registration

    Finding

    Unexpected device registration can expose persistence established after token acquisition.

    Validate against MDM and helpdesk onboarding before treating the registration as attacker persistence.

    View query
    Q-05Pivot

    Hunt post-compromise device registration

    What this checks

    Identify device-registration activity involving risky identities after suspicious authentication.

    KQL
    CloudAppEvents
    | where Timestamp >= ago(7d)
    | where AccountDisplayName == "Device Registration Service"
    | extend
        ApplicationId_=tostring(ActivityObjects[0].ApplicationId),
        ServiceName_=tostring(ActivityObjects[0].Name),
        UserPrincipalName=tolower(tostring(RawEventData.ObjectId))
    | project Timestamp, UserPrincipalName, ServiceName_, ApplicationId_, IPAddress, ActivityObjects, RawEventData
    | order by Timestamp desc
    SPL
    index=<cloud_app_index> sourcetype=<cloud_app_events_sourcetype>
    earliest=-7d
    | where AccountDisplayName="Device Registration Service"
    | eval
        user=lower(coalesce(user,UserPrincipalName,ObjectId)),
        src=coalesce(src,IPAddress),
        app_id=coalesce(app_id,ApplicationId)
    | fields _time user src app_id AccountDisplayName ActivityObjects RawEventData
    | sort - _time
    What to look for

    New device objects or registrations associated with recently risky users.

    Technical details
    Tested signal

    Device Registration Service activity by or for a recently risky identity.

    Assumptions
    • CloudAppEvents retains device registration activity.
    Data requirements and relevant fields
    saas audit

    Defender XDR cloud application events for device registration, Exchange actions, and uncommon user activity after compromise.

    • Timestamp
    • AccountObjectId
    • AccountDisplayName
    • ApplicationId
    • ActionType
    • IPAddress
    • ActivityObjects
    • RawEventData
    • UncommonForUser
    KQL schema

    Validate table availability, Entra ID Protection licensing, connector retention, and local field population.

    SPL schema

    Replace index/sourcetype placeholders and map user-risk and sign-in concepts to the local Entra export.

    Limitations
    • Legitimate device onboarding can create the same event and needs MDM/helpdesk context.

    KQL uses Microsoft Entra / Defender XDR identity telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

  6. Step 6Pivot

    Hunt mailbox rules and unusual mail access after risky sign-in

    Finding

    Mailbox-rule changes or unusual mail access show whether the compromise progressed into data access, evasion, or BEC preparation.

    Different victims can have different post-compromise branches; this is a scoping step, not a required stage.

    View query
    Q-06Pivot

    Hunt mailbox rules and unusual mail access after risky sign-in

    What this checks

    Search for Microsoft-observed Exchange post-compromise activity after risky identity authentication.

    KQL
    CloudAppEvents
    | where Timestamp >= ago(7d)
    | where ActionType in (
        "New-InboxRule",
        "Set-InboxRule",
        "Enable-InboxRule",
        "UpdateInboxRules",
        "MailItemsAccessed"
    )
    | where isnotempty(IPAddress)
    | project Timestamp, AccountObjectId, AccountDisplayName, ApplicationId, ActionType, IPAddress, UncommonForUser, ActivityObjects, RawEventData
    | order by Timestamp desc
    SPL
    index=<cloud_app_index> sourcetype=<cloud_app_events_sourcetype>
    earliest=-7d
    | eval
        user=lower(coalesce(user,UserPrincipalName,AccountDisplayName)),
        action=coalesce(action,ActionType),
        src=coalesce(src,IPAddress),
        app_id=coalesce(app_id,ApplicationId)
    | where action IN ("New-InboxRule","Set-InboxRule","Enable-InboxRule","UpdateInboxRules","MailItemsAccessed")
    | fields _time user src action app_id UncommonForUser ActivityObjects RawEventData
    | sort - _time
    What to look for

    Mail access or mailbox-rule activity that aligns with a risky user/device-code session.

    Technical details
    Tested signal

    Inbox-rule manipulation or unusual mail access by recently risky users.

    Assumptions
    • CloudAppEvents includes Exchange activity and uncommon-user context where supported.
    Data requirements and relevant fields
    saas audit

    Defender XDR cloud application events for device registration, Exchange actions, and uncommon user activity after compromise.

    • Timestamp
    • AccountObjectId
    • AccountDisplayName
    • ApplicationId
    • ActionType
    • IPAddress
    • ActivityObjects
    • RawEventData
    • UncommonForUser
    KQL schema

    Validate table availability, Entra ID Protection licensing, connector retention, and local field population.

    SPL schema

    Replace index/sourcetype placeholders and map user-risk and sign-in concepts to the local Entra export.

    Limitations
    • Not every device-code compromise progresses to Exchange abuse.

    KQL uses Microsoft Entra / Defender XDR identity telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

Blast radius

Wider-compromise pivots

  • Measure device-code flow by app, resource, department, and user role to identify where it is genuinely required.
  • Review Conditional Access report-only results for device-code authentication before enforcing a block.
  • Search risky identities for authentication from anonymous or threat-intelligence-linked IPs.
  • Use SessionId and other linkable identifiers to track the same session into Microsoft 365 workload logs.
  • Review newly registered devices and PRT-capable persistence for confirmed victims.
  • Search Graph reconnaissance and mailbox activity after the suspicious session.
  • Audit whether high-value users are protected with phishing-resistant authentication and risk-based access controls.

Evidence threshold

What would increase confidence

  • User is medium/high risk and remains at risk or confirmed compromised.
  • Successful device-code authentication is new for the identity.
  • Device-code flow is not required by the user's role or app/resource.
  • 50199 is followed by success in the same session/correlation context.
  • A phishing URL click precedes the risky sign-in.
  • Source IP/device/app context is unusual for the user.
  • A new device is registered after the suspicious authentication.
  • Mailbox rules, unusual mail access, or Graph activity follow.
  • No approved helpdesk, CLI, IoT, or shared-device workflow explains the sequence.

Conclusion

Result and next action

The hunt operationalizes User Risk as an investigation entry point and shows how to distinguish a generic risky user from a current device-code phishing compromise using authentication flow, timing, click, and cloud follow-on evidence.

  • Disable confirmed compromised identities temporarily where active use is suspected.
  • Revoke refresh tokens and sign-in sessions.
  • Remove untrusted device registrations and malicious mailbox persistence.
  • Scope mail, Graph, SharePoint, and other cloud access after compromise.
  • Block device-code flow where there is no business need.
  • For required use, constrain the flow through Conditional Access and narrow app/user/resource scope.
  • Deploy phishing-resistant authentication for high-value users and use risk-based Conditional Access.

The hunt starts from Microsoft Entra User Risk and asks a narrower question: which risky identities also show authentication behavior consistent with current device-code phishing?

It expands in stages: risky users, successful device-code use, 50199-to-success sequences, phishing-click correlation, device registration, and mailbox activity.

This keeps User Risk useful without turning it into a generic alert stream. A high-risk user becomes operationally meaningful when the risk can be connected to a suspicious authentication flow and post-authentication behavior.

Context

ATT&CK and limits

Behavior mapping

MITRE ATT&CK

This mapping describes the valid-account behavior examined by the Hunt. It does not prove token theft, attribution, or technique-wide coverage.

Review boundary

Sources and limits

The conclusion stays bounded to the stated scope and available logs.