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

hunt-2025-044 v1.0 by Paul Newton

FOCI Cross-Resource Token Pivot — Non-Interactive Sign-In Burst

Platform

Entra ID Azure

Data Sources

AADNonInteractiveUserSignInLogs

MITRE ATT&CK

Tactics

Credential Access Collection Lateral Movement

Threat Actors

Unknown

Tags

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

Hunt Hypothesis

Microsoft's Family of Client IDs (FOCI) allows a refresh token obtained via one first-party application to be exchanged for access tokens scoped to any other service in the family — Exchange, Teams, Azure Management, Key Vault, Office Management — without re-authenticating the user. After capturing a single device code token, an attacker can silently pivot to every Microsoft service the victim can access with a single API call per resource. This produces a burst of non-interactive sign-ins from the same client_id against multiple distinct resource endpoints within a short window — a pattern that does not occur in legitimate first-party application behaviour. FOCI-capable client IDs include Azure CLI (04b07795-8542-4bc9-aaaa-59d79c0a3df9), Azure PowerShell (1950a258-227b-4e31-a9cf-717495945fc2), and Microsoft Authentication Broker (29d9ed98-a469-4536-ade2-f981bc1d605e).

FOCI Multi-Resource Token Exchange Burst

Analytic #1

Detects a single OAuth client_id being used to exchange a refresh token for access tokens against three or more distinct resource endpoints within a 10-minute window, via non-interactive sign-ins. This is the direct behavioural signature of FOCI pivoting — one captured token silently unlocked across multiple Microsoft services.

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 GraphLogs =
    MicrosoftGraphActivityLogs
    | where TimeGenerated > ago(1h)
    | 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(1h)
    | 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)
| project FirstSeen, LastSeen, SpanMinutes,
          UserPrincipalName, AppDisplayName, AppId,
          AppIdCount, ResourceCount, Resources,
          IPs, ASNs, UserAgents, RequestUris, SignInCount
| order by FirstSeen desc

Triage Steps

  1. Review AppId against known FOCI client IDs — Azure CLI (04b07795-8542-4bc9-aaaa-59d79c0a3df9), Azure PowerShell (1950a258-227b-4e31-a9cf-717495945fc2), Microsoft Authentication Broker (29d9ed98-a469-4536-ade2-f981bc1d605e), Microsoft Office (d3590ed6-52b3-4102-aeff-aad2292ab01c)
  2. Check IPs against the user's normal sign-in baseline — attacker FOCI pivots will originate from the C2 server or residential proxy, not the user's device
  3. Review Resources — a pivot covering Exchange + Teams + Azure Management in one burst is high confidence; Exchange only may be lower confidence
  4. Check whether a device code sign-in (interactive, AuthenticationProtocol == "deviceCode") for the same user preceded this burst within the last hour
  5. If confirmed, revoke all refresh tokens via Entra and audit downstream Graph API activity (mail search, file access, Teams messages) for the affected user
  6. Review AADNonInteractiveUserSignInLogs for the same user over the prior 30 days to establish whether this AppId has been used before

True Positive Example

Log Entry:
{
  "log_entry": {
    "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",
    "ResourceCount": 6,
    "Resources": "[\"Microsoft Graph\",\"Exchange / Outlook\",\"MS Teams\",\"Azure Management\",\"Azure Key Vault\",\"Office Management\"]",
    "IPs": "[\"45.142.212.100\"]",
    "SignInCount": 6
  }
}
Analysis:

Six non-interactive token exchanges for a FOCI-eligible client (Azure CLI) against all six AUTHOV-targeted resources within 2 minutes, all from a single datacenter IP not in the user's baseline. A device code sign-in for the same user to Azure CLI was recorded 8 minutes prior. This is the textbook FOCI pivot pattern following a successful device code phish.