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

hunt-2025-045 v1.0 by Paul Newton

Device Code Auth Followed by FOCI Multi-Resource Burst

Platform

Entra ID Azure

Data Sources

SigninLogs AADNonInteractiveUserSignInLogs MicrosoftGraphActivityLogs

MITRE ATT&CK

Tactics

Credential Access Collection Lateral Movement

Threat Actors

Unknown

Tags

#foci #device-code #entra #identity #oauth #token-theft #post-exploitation #correlation

Hunt Hypothesis

A device code phish succeeds when the victim completes authentication via the attacker's device code URL. Within minutes, the attacker silently exchanges the captured refresh token against multiple Microsoft resource endpoints using FOCI — generating a burst of non-interactive sign-ins from the same client_id. Correlating an interactive device code authentication in SigninLogs with a subsequent FOCI burst in AADNonInteractiveUserSignInLogs for the same user, within a short time window, closes the loop between the phish and the post-exploitation pivot. This two-event chain is a high-confidence signal for a completed device code token theft and immediate post-exploitation.

Device Code Auth Followed by FOCI Burst Within 20 Minutes

Analytic #1

Correlates a successful interactive device code authentication in SigninLogs with a subsequent burst of non-interactive FOCI token exchanges in AADNonInteractiveUserSignInLogs for the same user within 20 minutes. The burst must involve a single FOCI-capable client_id hitting three or more distinct resource endpoints in a 10-minute window. Joining with MicrosoftGraphActivityLogs surfaces the actual API calls made after the pivot, providing immediate context for the blast radius.

Detection Queries

KQL
let FOCIAppIds = dynamic([
    "04b07795-8542-4bc9-aaaa-59d79c0a3df9",  // Azure CLI
    "1950a258-227b-4e31-a9cf-717495945fc2",  // Azure PowerShell
    "29d9ed98-a469-4536-ade2-f981bc1d605e",  // Microsoft Authentication Broker
    "d3590ed6-52b3-4102-aeff-aad2292ab01c",  // Microsoft Office
    "04b07795-8ddb-461a-bbee-02f9e1bf7b46"   // Azure CLI (alt)
]);
let CIDRASN = externaldata(CIDR:string, CIDRASN:int, CIDRASNName:string)
    ['https://firewalliplists.gypthecat.com/lists/kusto/kusto-cidr-asn.csv.zip']
    with (ignoreFirstRecord=true);
let DeviceCodeAuths =
    SigninLogs
    | where TimeGenerated > ago(2h)
    | where AuthenticationProtocol == "deviceCode"
    | where ResultType == 0
    | project UserPrincipalName, DeviceCodeTime = TimeGenerated,
              DeviceCodeIP = IPAddress, DeviceCodeAuthProtocol = AuthenticationProtocol;
let GraphLogs =
    MicrosoftGraphActivityLogs
    | where TimeGenerated > ago(2h)
    | evaluate ipv4_lookup(CIDRASN, IPAddress, CIDR, return_unmatched=true)
    | extend GraphASN = CIDRASN, GraphASNName = CIDRASNName
    | project SignInActivityId, GraphIPAddress = IPAddress, GraphASN, GraphASNName,
              ResponseStatusCode, UserAgent, RequestMethod, RequestUri;
let AADLogs =
    AADNonInteractiveUserSignInLogs
    | where TimeGenerated > ago(2h)
    | where ResultType == 0
    | where AppId in (FOCIAppIds)
    | evaluate ipv4_lookup(CIDRASN, IPAddress, CIDR, return_unmatched=true)
    | extend ASN = CIDRASN, ASNName = CIDRASNName;
AADLogs
| join kind=leftouter (GraphLogs) on $left.CorrelationId == $right.SignInActivityId
| summarize
    Resources     = make_set(ResourceDisplayName),
    ResourceCount = dcount(ResourceDisplayName),
    FirstSeen     = min(TimeGenerated),
    LastSeen      = max(TimeGenerated),
    IPs           = make_set(IPAddress),
    ASNs          = make_set(ASNName),
    UserAgents    = make_set(UserAgent),
    RequestUris   = make_set(RequestUri),
    SignInCount   = count(),
    AppIdCount    = dcount(AppId)
    by UserPrincipalName, AppId, AppDisplayName, bin(TimeGenerated, 10m)
| where ResourceCount >= 3
| where AppIdCount == 1
| extend SpanMinutes = datetime_diff('minute', LastSeen, FirstSeen)
| join kind=inner (DeviceCodeAuths) on UserPrincipalName
| where FirstSeen between (DeviceCodeTime .. (DeviceCodeTime + 20m))
| project DeviceCodeTime, DeviceCodeIP, DeviceCodeAuthProtocol,
          FirstSeen, LastSeen, SpanMinutes,
          UserPrincipalName, AppDisplayName, AppId,
          AppIdCount, ResourceCount, Resources,
          IPs, ASNs, UserAgents, RequestUris, SignInCount
| order by FirstSeen desc

Triage Steps

  1. Confirm the device code sign-in (DeviceCodeIP) does not match the user's normal sign-in geography or ASN — a legitimate self-initiated device code auth would originate from the user's device
  2. Check the time delta between DeviceCodeTime and FirstSeen — sub-5-minute gaps indicate automated post-exploitation tooling
  3. Review Resources — full coverage of Exchange + Teams + Azure Management + Key Vault is the highest-confidence pivot pattern
  4. Inspect UserAgents from GraphLogs — attacker tooling will show Python requests, curl, or custom UA strings, not browser or Office client UAs
  5. Review RequestUris for immediate post-exploitation activity — mail search queries, file enumeration, or Teams message reads within the same window
  6. Check whether the DeviceCodeIP and the FOCI burst IPs differ — the phish and the pivot typically originate from the same C2 or operator machine
  7. If confirmed, revoke all refresh tokens via Entra immediately and audit all Graph API activity for the affected user over the prior 24 hours

True Positive Example

Log Entry:
{
  "log_entry": {
    "DeviceCodeTime": "2026-05-01T14:00:00Z",
    "DeviceCodeIP": "45.142.212.100",
    "DeviceCodeAuthProtocol": "deviceCode",
    "FirstSeen": "2026-05-01T14:02:00Z",
    "LastSeen": "2026-05-01T14:04:37Z",
    "SpanMinutes": 2,
    "UserPrincipalName": "finance.director@contoso.com",
    "AppDisplayName": "Microsoft Azure CLI",
    "AppId": "04b07795-8542-4bc9-aaaa-59d79c0a3df9",
    "AppIdCount": 1,
    "ResourceCount": 6,
    "Resources": "[\"Microsoft Graph\",\"Exchange / Outlook\",\"MS Teams\",\"Azure Management\",\"Azure Key Vault\",\"Office Management\"]",
    "IPs": "[\"45.142.212.100\"]",
    "ASNs": "[\"AS62282 DataCamp Limited\"]",
    "UserAgents": "[\"python-requests/2.31.0\"]",
    "RequestUris": "[\"https://graph.microsoft.com/v1.0/me/messages?$search=invoice\"]",
    "SignInCount": 6
  }
}
Analysis:

Device code authentication completed from a datacenter IP at 14:00. Within 2 minutes, the same IP performed six FOCI token exchanges across all targeted resources using Azure CLI. GraphLogs show an immediate mail search for "invoice" via Graph API. Python-requests user agent confirms automated tooling. DeviceCodeIP matches the burst IPs — single C2 operator machine performing the full phish-to-pivot chain.