ConsentFix — Entra ID OAuth Phishing & Token Theft
Executive Summary
ConsentFix weaponizes OAuth itself rather than malware. The victim is lured (fake CAPTCHA / "fix your Azure/PowerShell login" pages reached via SEO poisoning or malvertising) into a real Entra ID sign-in for a pre-trusted first-party Microsoft app — Azure CLI (04b07795-8ddb-461a-bbee-02f9e1bf7b46), VS Code, or the Authentication Broker. Those apps have implicit tenant-wide consent for openid / profile / offline_access, show no consent prompt, and use a localhost redirect for native flows. After the legitimate (MFA-satisfied) sign-in, the browser lands on http://localhost:<port>/?code=<authcode>; the phishing page tells the victim to copy that full URL into a form, handing the authorization code to the attacker.
The attacker redeems the ~10-minute code at the token endpoint for access and refresh tokens carrying the victim's delegated permissions, then operates from their own infrastructure. MFA and most Conditional Access are bypassed because the interactive sign-in was genuine and token redemption happens server-side. The refresh token grants durable access; in observed escalations the attacker registers a device (DRS) and obtains a PRT for deeper persistence.
Source Review & Web Hunter Notes
| Tier | Source | Key Finding | Carry |
|---|---|---|---|
| 1 · Vendor research | Mitiga — ConsentFix OAuth Phishing Explained | Full chain: lure → first-party app → localhost code capture → token redemption → MFA bypass → refresh-token persistence | yes |
| 1 · Vendor research | Elastic Security Labs — Entra ID OAuth Phishing Detection | Sign-in/audit field detections: app IDs, incoming_token_type, token-protection unbound, DRS device-registration sequence, session reuse across IPs | yes |
| 2 · Vendor | Obsidian Security — OAuth vulnerabilities | OAuth token theft driving 2026 SaaS breaches; broader context | partial |
| 2 · Microsoft | Entra ID CA / Token Protection docs | Token binding, CAE, first-party app filtering as mitigations | yes |
Decisions: identity-log-first, not endpoint (token redemption occurs in the attacker's environment — the victim endpoint shows almost nothing); flag CQL as schema-dependent (Entra-log field names vary by NG-SIEM/LogScale parser); lead with Token Protection (token binding defeats the replay, not just detects it).
Hunt Brief & Attack Chain
Working hypothesis: A user completes a legitimate interactive sign-in to a first-party native client, after which the same identity shows a non-interactive token redemption (refresh token, .default/offline_access scopes) from a new IP/ASN with an unbound session — i.e., the auth code was redeemed elsewhere. Follow-on: session reuse across IPs, DRS device registration, PRT issuance, Graph/mailbox activity from the new origin.
| Step | Behavior | Telemetry | Hunt Angle |
|---|---|---|---|
| 1 · Lure | SEO-poisoned result / malvertising / fake CAPTCHA → "fix login" page | Proxy / web logs | N5 |
| 2 · Legit sign-in | Victim authenticates to first-party app (Azure CLI), MFA satisfied | Entra interactive sign-in | Baseline; pairs with step 4 |
| 3 · Code capture | Victim copies http://localhost:<port>/?code=… into attacker form | None (off-platform) | Gap detect the redemption |
| 4 · Token redemption | Attacker POSTs code to token endpoint from own infra → access+refresh tokens | Entra non-interactive sign-in | Q1 / Q3 / Q6 |
| 5 · Session reuse | Same session/token used from attacker IP/ASN | Entra sign-in (session_id) | Q2 |
| 6 · Persistence | DRS device registration → PRT issuance | Entra audit (Add device/owner) | Q4 / Q5; N3 |
| 7 · Objectives | Mailbox access, Graph enumeration/exfil, SaaS pivot | Graph / UnifiedAuditLog | N4; pivots |
Affected surface & telemetry
| Surface | Required Telemetry | Priority | Gap Risk |
|---|---|---|---|
| Entra ID sign-ins (interactive + non-interactive) | Entra sign-in logs ingested into NG-SIEM | Critical | High if not ingested — the whole pack depends on this |
| Entra ID audit (device registration, consent) | Entra audit logs | High | Medium — ensure DRS events captured |
| Token Protection status | Sign-in tokenProtectionStatusDetails | High | Requires Token Protection enabled to populate |
| Victim endpoint | Browser/process telemetry | Low | Gap code capture off-platform; near-zero signal |
Consolidated IOC Table
| Type | Value | Conf | Action | Context |
|---|---|---|---|---|
| App ID (context) | 04b07795-8ddb-461a-bbee-02f9e1bf7b46 | medium | hunt | Azure CLI — abused first-party native client |
| App ID (context) | aebc6443-996d-45c2-90f0-388ff96faa56 | medium | hunt | Visual Studio Code first-party client |
| App ID (context) | 29d9ed98-a469-4536-ade2-f981bc1d605e | medium | hunt | Microsoft Authentication Broker (ADRS/PRT) |
| Behavior | non-interactive refresh-token sign-in from new IP/ASN post interactive logon | high | detect | Token redemption from attacker environment |
| Behavior | token-protection signInSessionStatus = "unbound" | high | detect | Token replay indicator |
| Behavior | DRS "Add device" → "Add registered owner" ~1 min, same correlationId | high | detect | Automated device registration (ROADtools-style) |
ATT&CK Mapping
| Tactic | Technique | Observed Behavior | Query / Control |
|---|---|---|---|
| Initial Access | T1566.002 — Spearphishing Link | SEO-poisoned / malvertised "fix login" lure | N5; user training §10 |
| Credential Access | T1528 — Steal Application Access Token | OAuth authorization code captured via localhost paste | Q1, Q3 |
| Defense Evasion / Lateral | T1550.001 — Use Alternate Auth Material: App Token | Stolen tokens used from attacker infra; MFA/CA bypassed | Q1, Q2, Q6 |
| Persistence / Cred Access | T1606.002 — Forge Web Credentials: OAuth | Refresh token reused for durable access | Q3, Q6 |
| Persistence | T1098.005 — Account Manipulation: Device Registration | DRS device add + PRT issuance | Q4, Q5; N3 |
| Defense Evasion | T1078.004 — Valid Accounts: Cloud Accounts | Legitimate identity, anomalous origin | Q1, Q2 |
| Collection | T1114 — Email Collection | Mailbox access via Graph with stolen token | N4; pivots |
Native Audit-Log Hunts (Entra ID / Graph — primary surface)
| Hunt | Source | Logic | Response |
|---|---|---|---|
| N1 · Redemption after interactive sign-in | Entra sign-in logs | Interactive sign-in to a first-party app, then a non-interactive refresh-token sign-in for the same user from a different IP/ASN within minutes | Revoke sessions; confirm with user |
| N2 · Unbound token sessions | Entra sign-in logs | tokenProtectionStatusDetails.signInSessionStatus = "unbound" on first-party app sign-ins | Investigate origin; enforce token binding |
| N3 · DRS device-registration sequence | Entra audit logs | "Add device" (DRS, OS 10.0.19041.928) → "Add registered users/owner" ~1 min, same correlationId | Remove rogue device; revoke PRT |
| N4 · Post-takeover Graph/mailbox activity | UnifiedAuditLog / Graph | New inbox rules, mass mail read, Graph enumeration, or SaaS access from the new origin/session | Scope blast radius; preserve mailbox |
| N5 · Lure landing pages | Proxy / SWG | Users reaching fake-CAPTCHA / "azure login fix" pages from search/ad referrers shortly before a first-party sign-in | Block domains; warn users |
CrowdStrike LogScale CQL Hunt Queries
Looks for: non-interactive sign-ins to Azure CLI / VS Code / Auth Broker using a refresh token. Accomplishes: surfaces the token-redemption stage — the core ConsentFix signal. FP: legit Azure CLI/PowerShell automation — exclude sanctioned principals + known egress IPs.
// HUNT: Non-interactive first-party-app token redemption (ConsentFix) // MITRE: T1528, T1550.001, T1078.004 | CONF: med-high FP: med COST: low-med // REQUIRES: Entra ID sign-in logs ingested into NG-SIEM; NOTE: field tokens depend on your parser — validate // TUNING: these three appIds (Azure CLI, PowerShell, Auth Broker) are used non-interactively all day by sanctioned automation -- exclude your service principals and CI egress ASNs first. A real hit is a human userPrincipalName redeeming from an autonomousSystemNumber that user has no history with; rank by first-seen (user, ASN) pairs, not by raw count. appId=/^(04b07795-8ddb-461a-bbee-02f9e1bf7b46|aebc6443-996d-45c2-90f0-388ff96faa56|29d9ed98-a469-4536-ade2-f981bc1d605e)$/i | isInteractive=false | incomingTokenType=/refreshToken/i | groupBy([userPrincipalName, appDisplayName, ipAddress, autonomousSystemNumber], function=count()) | table([userPrincipalName, appDisplayName, ipAddress, autonomousSystemNumber, _count], limit=200)
Looks for: one session ID observed from two or more distinct IPs in a short window. Accomplishes: high-fidelity evidence of a hijacked/replayed token — victim and attacker sharing a session.
// HUNT: One session ID seen from multiple IPs (session/token reuse) // MITRE: T1550.001, T1078.004 | CONF: high FP: low-med COST: med // TUNING: require >=2 distinct ASNs or geo-distant IPs; window to ~30 min | groupBy([sessionId], function=[count(field=ipAddress, distinct=true, as=ipCount), collect([ipAddress, userPrincipalName, autonomousSystemNumber])]) | ipCount>=2 | table([sessionId, userPrincipalName, ipCount, ipAddress, autonomousSystemNumber], limit=200)
Looks for: non-interactive sign-ins carrying .default, offline_access, or adrs scopes via a first-party client. FP: apps request these routinely — combine with a new IP/ASN (Q1) or unbound status (Q6).
// HUNT: Non-interactive sign-in with broad/offline OAuth scopes // MITRE: T1528, T1606.002 | CONF: medium FP: medium COST: low-med // TUNING: AND with Q1 (new IP) or Q6 (unbound); scope to first-party native clients | isInteractive=false | scopeDetail=/(\.default|offline_access|adrs_access)/i | appId=/^(04b07795|aebc6443|29d9ed98)/i | table([userPrincipalName, appDisplayName, scopeDetail, ipAddress, resourceDisplayName], limit=200)
Looks for: "Add device" + "Add registered owner/users" by DRS sharing one correlation ID in a ~1-minute window. Accomplishes: catches automated device registration used for PRT-based persistence. FP: bulk Autopilot/Intune enrollment — usually a distinct correlation pattern.
// HUNT: Burst DRS device-registration sequence (persistence) // MITRE: T1098.005 | CONF: high FP: low COST: med // TUNING: scope to DRS-initiated events; flag OS build 10.0.19041.928 and >=2 ops/correlationId/minute operationName=/Add device|Add registered (owner|users) to device/i | groupBy([correlationId], function=[count(as=ops), collect([operationName, userPrincipalName, targetDeviceId])]) | ops>=2 | table([correlationId, ops, operationName, userPrincipalName, targetDeviceId], limit=200)
Looks for: Auth Broker / ADRS token requests producing a PRT for a device ID first seen in this window with an unbound session. Accomplishes: catches the persistence escalation after device registration. FP: genuine first-enrollment — correlate with Q4.
// HUNT: PRT issuance to a newly observed device (unbound) // MITRE: T1098.005, T1550.001 | CONF: med-high FP: low-med COST: med // TUNING: correlate with Q4 on userPrincipalName/time; require unbound session + first-seen deviceId appId=/^29d9ed98-a469-4536-ade2-f981bc1d605e$/i | incomingTokenType=/refreshToken|primaryRefreshToken/i | signInSessionStatus=/unbound/i | table([userPrincipalName, deviceId, ipAddress, signInSessionStatus, @timestamp], limit=200)
Looks for: sign-ins where the token-protection session status is unbound. Accomplishes: directly flags tokens not bound to their original device — the hallmark of replay. FP: clients not yet supporting token binding — baseline these first.
// HUNT: Unbound token-protection sessions on first-party apps // MITRE: T1550.001, T1606.002 | CONF: med-high FP: low-med COST: low // REQUIRES: Token Protection enabled in CA // TUNING: baseline expected unbound clients first | signInSessionStatus=/unbound/i | appId=/^(04b07795|aebc6443|29d9ed98)/i | table([userPrincipalName, appDisplayName, ipAddress, signInSessionStatus, @timestamp], limit=200)
Detection Recommendations (NG-SIEM / Entra ID Protection)
| Detection (scheduled search) | Logic | Benign Exclusions | Deployment Path |
|---|---|---|---|
| ConsentFix — interactive → non-interactive redemption from new IP | Correlate Q1 with a preceding interactive sign-in for the same user within N minutes from a different ASN | Sanctioned automation accounts; corporate VPN egress | NG-SIEM correlation rule / scheduled search → detection |
| ConsentFix — DRS burst + PRT issuance | Q4 followed by Q5 for the same user within minutes | Bulk managed-device enrollment windows | NG-SIEM correlation rule; Entra ID Protection risk policy |
Machine-Readable IOC Appendix
Grouped IOC Quick-Copy
One-click blocks for hunting and Conditional-Access audit. This is an identity technique — the app IDs are legitimate Microsoft apps (hunt context, never block); atomic IOCs are per-incident.
04b07795-8ddb-461a-bbee-02f9e1bf7b46 # Azure CLI (primary abused client) aebc6443-996d-45c2-90f0-388ff96faa56 # Visual Studio Code 29d9ed98-a469-4536-ade2-f981bc1d605e # Microsoft Authentication Broker (ADRS/PRT) 1950a258-227b-4e31-a9cf-717495945fc2 # Azure PowerShell (scope in CA app filter) # These are LEGITIMATE Microsoft apps — hunt on anomalous use, do not block
Non-interactive refresh-token sign-in to a first-party app from a new IP/ASN, minutes after an interactive sign-in by the same user (token redemption elsewhere) One sessionId observed from >=2 distinct IPs/ASNs within ~30 min (session/token reuse) signInSessionStatus = "unbound" on a first-party app sign-in (token replay) Non-interactive sign-in carrying .default / offline_access / adrs scopes DRS "Add device" -> "Add registered owner/users" within ~1 min, same correlationId PRT issued (Auth Broker) to a first-seen deviceId in unbound state
// Non-interactive first-party-app refresh-token redemption
SigninLogs
| where AppId in ("04b07795-8ddb-461a-bbee-02f9e1bf7b46","aebc6443-996d-45c2-90f0-388ff96faa56","29d9ed98-a469-4536-ade2-f981bc1d605e")
| where IsInteractive == false and IncomingTokenType == "refreshToken"
| project TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress, AutonomousSystemNumber
// DRS device-registration burst (audit)
AuditLogs
| where OperationName has_any ("Add device","Add registered owner to device","Add registered users to device")
| summarize ops=count() by CorrelationId, bin(TimeGenerated, 2m) | where ops >= 2
# Enable Token Protection (token binding) in a CA session control -> defeats replay # Enable Continuous Access Evaluation (CAE) -> fast token revocation # Scope first-party CLI apps (Azure CLI/PowerShell) to compliant/hybrid-joined devices # Restrict user consent + enable admin-consent workflow # Reduce refresh-token lifetime / enforce sign-in frequency # Revoke on incident: Revoke-MgUserSignInSession -UserId user@contoso.com
Hardening — Tiered & Deployable
OAuth abuse rides legitimate identity — prevention is identity configuration, not endpoint controls. Token binding is the one control that makes a stolen code/token useless to the attacker.
- Enable Token Protection (token binding) in Conditional Access — M1032 / M1018. Binds access tokens to the originating device/session so a redeemed code can't be replayed elsewhere. The decisive control.
- Enable Continuous Access Evaluation (CAE) — M1041 / M1018. Near-real-time token revocation, shrinking the refresh-token window.
- Scope first-party apps (Azure CLI / Az PowerShell) to compliant devices — M1018. CA app filter so these high-trust clients can't be used from arbitrary devices.
- User awareness: never paste localhost "fix-login" URLs — M1017. The lure depends entirely on the victim copying the redirect URL.
# Microsoft Graph PowerShell — CA policy enforcing Token Protection (pilot report-only first)
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess"
$params = @{
displayName = "CA - Require Token Protection (ConsentFix)"
state = "enabledForReportingButNotEnforced" # flip to "enabled" after pilot
conditions = @{ users = @{ includeUsers = @("<pilot-group-id>") }
applications = @{ includeApplications = @("Office365","04b07795-8ddb-461a-bbee-02f9e1bf7b46") }
clientAppTypes = @("all") }
sessionControls = @{ secureSignInSession = @{ isEnabled = $true } }
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $params
- CA: require compliant/hybrid-joined device for sensitive resources — M1032 / M1018. Blocks token use from unmanaged attacker hosts.
- Entra ID Protection sign-in & user-risk policies — M1047 / M1018. Challenge or block anomalous token redemption (new ASN, unfamiliar properties).
- Restrict user consent + admin-consent workflow — M1018. Closes the adjacent illicit-consent-grant path.
- Reduce refresh-token lifetime / enforce sign-in frequency — M1018 / M1026. Limits durability of a stolen refresh token.
- Phishing-resistant MFA (FIDO2 / passkeys / certificate-based) — M1032. With token binding, removes the replay value of intercepted material.
- Identity Threat Detection & Response (ITDR) — M1047. Correlate sign-in + Graph + SaaS activity by session ID continuously — the cross-surface visibility ConsentFix exploits the absence of.
- Zero-trust device posture / Global Secure Access with token binding everywhere — M1018 / M1032.
Containment Runbook
| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| Isolate | Revoke all sessions/refresh tokens for the user (Revoke-MgUserSignInSession -UserId <upn-or-objectId>); optionally disable the account; enforce sign-in frequency | SOC L2 / IAM | Revocation timestamp; Q1/Q2 hits |
| Triage | Pull the user's sign-in logs: attacker IP/ASN, session IDs, first-party app, unbound sessions; map redemption time vs. the interactive sign-in | SOC L2 | Sign-in timeline; session_id correlation |
| Eradicate | Remove rogue DRS device registrations and PRTs; delete attacker-created inbox rules; revoke any illicit app consents/credentials | IR / IAM | Device list; inbox-rule audit; consent grants |
| Recover | Reset credentials + re-register MFA on a trusted device; confirm token binding now enforced; review Graph/SaaS activity for exfil (N4) | IR + IT | Cred reset; CAE/token-binding status |
| Harden | Roll out §10 Immediate controls tenant-wide; enable the Q1×interactive and Q4→Q5 NG-SIEM detections | Detection Eng / IAM | CA policies enabled; detections live |
Detection Coverage Map & Validation
| Technique | Behavior | CQL | Native | Coverage |
|---|---|---|---|---|
| T1566.002 | Lure / code capture | — | N5 | GAP localhost paste off-platform |
| T1528 / T1550.001 | Token redemption | Q1, Q3 | N1 | Partial needs IP/ASN baselining |
| T1550.001 | Session/token replay | Q2, Q6 | N2 | Good Q6 needs Token Protection on |
| T1098.005 | Device-registration persistence | Q4 | N3 | Good |
| T1098.005 / T1550.001 | PRT persistence | Q5 | — | Partial correlate Q4→Q5 |
| T1114 | Objectives (mailbox/SaaS) | — | N4 | Partial Graph/UnifiedAuditLog |
| — | Whole technique vs. endpoint EDR | — | — | GAP identity-log ingestion mandatory |
Validation gates: (1) confirm Entra sign-in + audit logs ingested into NG-SIEM and field mapping validated for appId/isInteractive/sessionId/tokenProtectionStatus; (2) baseline legitimate Azure CLI/PowerShell users + expected unbound clients + normal ASNs; (3) in a test tenant, run a first-party auth-code flow and redeem from a second network to fire Q1/Q2 — do NOT phish real users; (4) promote the Q1×interactive correlation and Q4→Q5 chain to NG-SIEM detections + Entra ID Protection risk policies.
Hunt Summary Ticket
TITLE: Hunt — ConsentFix (Entra ID OAuth Phishing / Token Theft)
SEVERITY: High (active technique; bypasses MFA + most Conditional Access)
SCOPE: Entra ID tenant; M365 / Azure / connected SaaS; all member users
HYPOTHESIS: Victim completes a legit first-party-app sign-in, pastes the localhost
auth-code URL to an attacker who redeems it for access+refresh tokens and
operates from their own infra (MFA/CA bypassed). Persistence via DRS + PRT.
QUERIES RUN: Q1 non-interactive first-party redemption | Q2 sessionId across IPs |
Q3 broad/offline scopes | Q4 DRS burst | Q5 PRT to new device | Q6 unbound
+ Native N1-N5 (Entra sign-in/audit, Graph/UnifiedAuditLog, proxy)
DO FIRST: Q1 (x preceding interactive sign-in), Q2 — revoke sessions on confirm
FINDINGS: <pending analyst execution>
GAPS: Code capture off-platform; CQL field tokens schema-dependent; no endpoint
EDR signal; Q6 needs Token Protection enabled
ACTIONS: Enable Token Protection + CAE; scope first-party apps to compliant devices;
promote Q1xinteractive + Q4->Q5 NG-SIEM detections
OWNER: HuntPack
VERSION: v0.2 - 2026-06-12
Changelog
table() calls now carry an explicit row limit: the default is 200 and truncation is silent, so a capped result was indistinguishable from a complete one. Atomic-IOC sweeps (filters over 5+ hashes or C2 IPs) use limit=max so a wide infection is never silently under-scoped; behavioural hunts use limit=200, where exceeding the cap indicates the query needs tuning. Where present, event names that do not exist in the Falcon data model were corrected (e.g. ServiceInstalled is a Sysmon concept, not a Falcon event; ElfFileWritten is ELFFileWritten) — such queries could never return a row. No detection logic, fields, or IOCs changed.References
| Tier | Source | Used For |
|---|---|---|
| 1 | Mitiga — ConsentFix OAuth Phishing Explained | Full attack chain, MFA-bypass mechanics, mitigations |
| 1 | Elastic Security Labs — Entra ID OAuth Phishing Detection | Sign-in/audit field detections, app IDs, DRS sequence, session reuse |
| 2 | Obsidian Security — OAuth Vulnerabilities | 2026 OAuth/SaaS breach context |
| 2 | Microsoft — Conditional Access Token Protection | Token binding / CAE mitigation guidance |
HuntPack v0.1 · ConsentFix · Generated 2026-06-12 · Defensive use only. Identity technique — detections require Entra ID sign-in + audit log ingestion into NG-SIEM, and CQL field tokens are schema-dependent; validate against your connector before deploying. Token Protection is the decisive preventive control.