ConsentFix: Browser‑Native OAuth Token Theft via Azure CLI
This month, we observed a browser‑native social engineering technique dubbed ConsentFix, which abuses OAuth authorisation flows to achieve covert Microsoft account compromise without malware, credential harvesting, or endpoint execution. The campaign blends elements of ClickFix, OAuth consent phishing, and search‑engine watering hole attacks, relying entirely on user interaction within the browser context.
Stage Analysis: The attack chain begins with victims being redirected from Google Search results to compromised, high‑reputation websites presenting a fake Cloudflare Turnstile CAPTCHA. After conditional email‑based targeting and IP suppression to evade analysis, victims are redirected to a legitimate Microsoft login flow. Upon authenticating, victims are instructed to copy and paste a localhost redirect URL containing an OAuth authorisation code into the attacker’s page. The attacker exchanges this code for a valid OAuth token using Azure CLI, enabling non‑interactive access to Microsoft Graph and Azure APIs—often blending in with legitimate Azure CLI usage patterns.
Community Detection Opportunities
Detection Opportunity (KQL) 1
// Detects potential ConsentFix activity by correlating a successful interactive Azure CLI sign-in
// with a subsequent non-interactive Azure CLI sign-in for the same user and session within a short time window.
let Lookback = 7d;
let Window = 10m;
let AzureCLI_AppID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46";
let Interactive =
SigninLogs
| where TimeGenerated >= ago(Lookback)
| where AppId == AzureCLI_AppID
| where ResultType == 0
| project
InteractiveTime = TimeGenerated,
UserPrincipalName,
SessionId,
InteractiveIP = IPAddress,
InteractiveASN = tostring(AutonomousSystemNumber),
InteractiveLocation = tostring(Location);
let NonInteractive =
AADNonInteractiveUserSignInLogs
| where TimeGenerated >= ago(Lookback)
| where AppId == AzureCLI_AppID
| where ResultType == 0
| project
NonInteractiveTime = TimeGenerated,
UserPrincipalName,
SessionId,
NonInteractiveIP = IPAddress,
NonInteractiveASN = tostring(AutonomousSystemNumber),
NonInteractiveLocation = tostring(Location);
Interactive
| join kind=inner NonInteractive on UserPrincipalName, SessionId
| where NonInteractiveTime between (InteractiveTime .. InteractiveTime + Window)
| extend
IPMismatch = iff(InteractiveIP != NonInteractiveIP, true, false),
ASNMismatch = iff(InteractiveASN != NonInteractiveASN, true, false)
| project
UserPrincipalName,
InteractiveTime, InteractiveIP, InteractiveASN, InteractiveLocation,
NonInteractiveTime, NonInteractiveIP, NonInteractiveASN, NonInteractiveLocation,
IPMismatch, ASNMismatch, SessionId
| order by InteractiveTime desc
Detection Opportunity (KQL) 2
// Correlates browser visits to CAPTCHA/Turnstile-themed URLs with Azure CLI OAuth sign-ins
let SuspiciousVisits =
DeviceNetworkEvents
| where TimeGenerated >= ago(7d)
| where isnotempty(RemoteUrl)
| where RemoteUrl has_any ("cloudflare-verify", "captcha", "turnstile")
| where InitiatingProcessFileName in~ ("chrome.exe", "msedge.exe", "brave.exe", "firefox.exe")
| project VisitTime = TimeGenerated, DeviceName, VisitUrl = RemoteUrl, DeviceUpn = InitiatingProcessAccountUpn;
let CLILogins =
SigninLogs
| where TimeGenerated >= ago(7d)
| where AppId == "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
| project LoginTime = TimeGenerated, SigninUpn = UserPrincipalName, IPAddress;
SuspiciousVisits
| join kind=inner (
CLILogins
| project LoginTime, SigninUpnLower = tolower(SigninUpn), IPAddress
) on $left.DeviceUpn == $right.SigninUpnLower
| where datetime_diff('minute', LoginTime, VisitTime) between (0 .. 10)
| project VisitTime, LoginTime, DeviceName, DeviceUpn, IPAddress, VisitUrl
| order by VisitTime desc
Bridewell CSIRT Detection Rules & Analytics
The following detection content was generated through our managed threat intelligence, threat hunting, and detection and response (MDR) services. The content automatically protects our customers from known and emerging threats.
| Detection Analytic Concept | Category |
|---|---|
| KQL_ConsentFix_Triage_RareAzureCLISignins | Initial Access |
| KQL_ConsentFix_AzureCLI_AppConsent_Events | Persistence |
| KQL_Turnstile_CaptchaVisit_Correlated_AzureCLI_Signin | Initial Access |
| KQL_IOC_IPAddress_SigninDetection | Initial Access |
| KQL_NonInteractive_IOC_IPAddress_Signin | Initial Access |
| YARA_ConsentFix_Initial_Cloudflare_Lure | Initial Access |
| YARA_ConsentFix_OAuth_Phishing | Initial Access |
| YARA_ConsentFix_OAuth_Phishing_Landing | Initial Access |