# 3 OAuth TTPs Seen This Month — and How to Detect Them with Entra ID Logs

Device-code phishing and ROPC abuse against Microsoft Entra ID let attackers obtain tokens, register devices, and gain 90-day PRT persistence.

- Payout: Real-world hack
- Severity: high
- Program: Microsoft
- Analysis by: Wiz Research
- Disclosed: 2025-11-27
- Write-up quality: 7/10
- Original: https://www.wiz.io/blog/recent-oauth-attacks-detection-strategies
- Library page: https://bugbountylibrary.com/r/blog%3Awiz-io-blog-recent-oauth-attacks-detection-strategies
- Tags: area:cloud domain:cloud impact:ato impact:privesc kind:incident platform:threat-intel sev:high tech:azure tech:oauth technique:chained technique:recon vuln:auth-bypass vuln:mfa-bypass vuln:privilege-escalation

**Target:** OAuth authentication flows (device code, ROPC) in Entra ID sign-in
**Root cause:** Device code flow authenticates on a separate browser/device while returning the token to the client that requested the code, so a phished victim's MFA-backed token lands with the attacker; ROPC exchanges username/password directly for tokens with no MFA or browser consent.
**Steps:**
1. Attacker generates a device code via the device code flow for a Microsoft client.
2. Attacker lures victim to a Microsoft-domain link to enter the code and authenticate (possibly with MFA).
3. Token with amr ["mfa"] is delivered to attacker's client; logs show 3 events same sessionId with deviceCodeFlow, mismatched user agents/locations.
4. Attacker requests token for Microsoft Authentication Broker app + Intune Enrollment resource (permitted without registered device).
5. Attacker registers a device, obtaining a PRT valid up to 90 days.
6. Optionally provisions WHfB (requires MFA-capable token, so device-code path) to satisfy higher assurance CA.
7. RoadTx automates: device code, phishing, token exchange for enrollment, device registration, WHfB provisioning.
**How found:** Wiz telemetry analysis of Entra sign-in logs: device-code sessions show 3 events sharing a sessionId with deviceCodeFlow transferMethod but inconsistent user agents and geolocations; ~1% of device-code auths anomalous, 43% of multi-UA+multi-country ones have scripty user agents. ROPC surfaced via authenticationProtocol field and app/resource pair anomalies.
**Exploitation:** Attacker phishes victim into completing device-code auth, receives MFA-claimed token, exchanges it for Microsoft Authentication Broker/Intune Enrollment scopes, registers a device to bypass device-based Conditional Access, obtains a 90-day PRT, and optionally enrolls WHfB for phishing-resistant persistence.
**Escalation:** Token from device-code phishing chained to device registration then WHfB enrollment to bypass higher assurance-level Conditional Access and establish durable persistence.
**Insight:** Correlate same-sessionId device-code sign-ins for user-agent/geo mismatch (browser victim vs script attacker), and watch for the Microsoft Authentication Broker app + Intune Enrollment resource pair followed by device registration or WHfB enrollment from a different IP.

## Proof of concept (copied exactly from the original report)

**KQL detecting device code phishing session anomalies**

```
union AADNonInteractiveUserSignInLogs, SigninLogs
| where originalTransferMethod == "deviceCodeFlow"
 and clientAppUsed == "Mobile Apps and Desktop clients"
 and ['status']['errorCode'] in ("0", "50199")
 and isnotempty(userId)
 and isnotempty(ipAddress)
 and isnotempty(location['countryOrRegion'])
 and isnotempty(userAgent)
 and isnotempty(sessionId)
| summarize 
 set_status_errorCode = make_set(['status']['errorCode']),
 set_userAgent = make_set(userAgent),
 set_location_countryOrRegion = make_set(location['countryOrRegion']),
 max_createdDateTime = max(createdDateTime)
 by sessionId, userId, userPrincipalName, ipAddress
| where array_length(set_status_errorCode) == 2
 and (array_length(set_userAgent) == 2 or array_length(set_location_countryOrRegion) == 2)
```

**KQL correlating ROPC brute force with success**

```
let failed_attempts =
 union AADNonInteractiveUserSignInLogs, SigninLogs
 | where status["errorCode"] == "50126"
 and isnotempty(userId)
 and tostring(authenticationDetails["authenticationMethodDetail"]) == "Password in the cloud"
 and clientAppUsed == "Mobile Apps and Desktop clients"
 | summarize
 min_createdDateTime = min(createdDateTime),
 max_createdDateTime = max(createdDateTime),
 attempt_count = count()
 by bin(createdDateTime, 1d), userId, ipAddress, resourceDisplayName, appDisplayName
 | where attempt_count > 15
 | extend time_diff = datetime_diff("hour", max_createdDateTime, min_createdDateTime)
 | where time_diff < 3 and time_diff > -1;

union AADNonInteractiveUserSignInLogs, SigninLogs
| where status["errorCode"] == "0"
 and isnotempty(userId)
 and authenticationProtocol == "ropc"
| join kind=inner failed_attempts on userId, appDisplayName, resourceDisplayName
| extend time_diff_success = datetime_diff("hour", createdDateTime, max_createdDateTime)
| where time_diff_success < 3 and time_diff_success > -1
```

**KQL joining ROPC sign-in to device registration**

```
let device_reg_ropc =
 union AADNonInteractiveUserSignInLogs, SigninLogs
 | where isnotempty(userId)
 and status["errorCode"] == "0"
 and AuthenticationProtocol == "ropc"
 and appId == "29d9ed98-a469-4536-ade2-f981bc1d605e"
 and resourceId == "d4ebce55-015a-49b5-a083-c84d1797ae8c"
 | project ropc_time = createdDateTime, userId;

AuditLogs
| where result == "success"
 and activityDisplayName == "Register device"
 and isnotempty(initiatedBy["user"]["id"])
| extend userId = tostring(initiatedBy["user"]["id"])
| join kind=inner device_reg_ropc on userId
| extend time_diff_days = datetime_diff("day", createdDateTime, ropc_time)
| where time_diff_days between (0 .. 2)
| project userId, ropc_time, createdDateTime, time_diff_days, activityDisplayName
```

