Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Detect and investigate Azure service principal abuse including privilege escalation, credential compromise, admin consent bypass, and unauthorized enumeration in Microsoft Entra ID environments.
.claude/skills/detecting-azure-service-principal-abuse/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-21 | ✗→✓ | ▲ Improved | — | — |
| case-14 | ✗→✓ | ▲ Improved | — | — |
| case-18 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✓→✓ | = Same ✓ | — | — |
Azure service principals are identity objects used by applications, services, and automation tools to access Azure resources. Attackers exploit service principals for privilege escalation, lateral movement, and persistent access. Key abuse patterns include: adding credentials to existing principals, assigning privileged roles, bypassing admin consent, and enumerating service principals for attack paths. Application ownership grants the ability to manage credentials and configure permissions, creating hidden privilege escalation paths.
Attackers add new client secrets or certificates to gain persistent access:
Detection Query (KQL - Sentinel):
kqlAuditLogs | where OperationName has "Add service principal credentials" or OperationName has "Update application - Certificates and secrets management" | extend InitiatedBy = tostring(InitiatedBy.user.userPrincipalName) | extend TargetSP = tostring(TargetResources[0].displayName) | extend TargetSPId = tostring(TargetResources[0].id) | project TimeGenerated, InitiatedBy, OperationName, TargetSP, TargetSPId | sort by TimeGenerated desc
Detection Query (SPL - Splunk):
splindex=azure sourcetype="azure:aad:audit" operationName="Add service principal credentials" OR operationName="Update application*Certificates and secrets*" | stats count by initiatedBy.user.userPrincipalName, targetResources{}.displayName, _time | sort -_time
kqlAuditLogs | where OperationName == "Add member to role" | extend RoleName = tostring(TargetResources[0].modifiedProperties[1].newValue) | where RoleName has_any ("Global Administrator", "Application Administrator", "Privileged Role Administrator", "Cloud Application Administrator") | extend TargetSP = tostring(TargetResources[0].displayName) | extend InitiatedBy = tostring(InitiatedBy.user.userPrincipalName) | project TimeGenerated, InitiatedBy, TargetSP, RoleName, OperationName
kqlMicrosoftGraphActivityLogs | where RequestMethod == "GET" | where RequestUri has "/servicePrincipals" | summarize RequestCount = count() by UserAgent, IPAddress, bin(TimeGenerated, 1h) | where RequestCount > 10 | sort by RequestCount desc
kqlAuditLogs | where OperationName == "Consent to application" | extend ConsentType = tostring(TargetResources[0].modifiedProperties[4].newValue) | where ConsentType has "AllPrincipals" | extend AppName = tostring(TargetResources[0].displayName) | extend InitiatedBy = tostring(InitiatedBy.user.userPrincipalName) | project TimeGenerated, InitiatedBy, AppName, ConsentType
kqlAuditLogs | where OperationName == "Add app role assignment to service principal" | extend AppRoleValue = tostring(TargetResources[0].modifiedProperties[1].newValue) | where AppRoleValue has_any ("RoleManagement.ReadWrite.Directory", "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "Directory.ReadWrite.All", "Mail.ReadWrite") | extend TargetApp = tostring(TargetResources[0].displayName) | project TimeGenerated, TargetApp, AppRoleValue, CorrelationId
powershell# List service principals with recently added credentials Connect-MgGraph -Scopes "Application.Read.All" $suspiciousSPs = Get-MgServicePrincipal -All | ForEach-Object { $sp = $_ $creds = Get-MgServicePrincipalPasswordCredential -ServicePrincipalId $sp.Id $recentCreds = $creds | Where-Object { $_.StartDateTime -gt (Get-Date).AddDays(-7) } if ($recentCreds) { [PSCustomObject]@{ DisplayName = $sp.DisplayName AppId = $sp.AppId ObjectId = $sp.Id NewCredsCount = $recentCreds.Count LatestCredAdded = ($recentCreds | Sort-Object StartDateTime -Descending | Select-Object -First 1).StartDateTime } } } $suspiciousSPs | Sort-Object LatestCredAdded -Descending
powershell# Check role assignments for a specific service principal $spId = "<service-principal-object-id>" Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $spId | ForEach-Object { $resource = Get-MgServicePrincipal -ServicePrincipalId $_.ResourceId [PSCustomObject]@{ AppRoleId = $_.AppRoleId ResourceDisplayName = $resource.DisplayName CreatedDateTime = $_.CreatedDateTime } }
powershell# List owners of all applications (ownership = credential control) Get-MgApplication -All | ForEach-Object { $app = $_ $owners = Get-MgApplicationOwner -ApplicationId $app.Id foreach ($owner in $owners) { [PSCustomObject]@{ AppName = $app.DisplayName AppId = $app.AppId OwnerUPN = $owner.AdditionalProperties.userPrincipalName OwnerType = $owner.AdditionalProperties.'@odata.type' } } } | Where-Object { $_.OwnerUPN -ne $null }
kqlAADServicePrincipalSignInLogs | where ServicePrincipalId == "<target-sp-id>" | project TimeGenerated, ServicePrincipalName, IPAddress, Location, ResourceDisplayName, Status.errorCode | sort by TimeGenerated desc
powershell# Disable user ability to register applications Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{ AllowedToCreateApps = $false }
powershell# Require admin approval for all app consent requests New-MgPolicyPermissionGrantPolicy -Id "admin-only-consent" ` -DisplayName "Admin Only Consent" ` -Description "Only admins can consent to applications"
Create analytics rules for:
| Technique | ID | Description | |-----------|-----|-------------| | Account Manipulation: Additional Cloud Credentials | T1098.001 | Adding credentials to service principal | | Valid Accounts: Cloud Accounts | T1078.004 | Using compromised service principal | | Account Discovery: Cloud Account | T1087.004 | Enumerating service principals | | Steal Application Access Token | T1528 | OAuth token theft via service principal |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.