Adversaries may exploit high-privilege application roles assigned to service principals to gain unauthorized access to sensitive data and directory management capabilities. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential privilege escalation and lateral movement opportunities.
KQL Query
let timeframe = 1d;
let HighRiskRoles = dynamic([
"Mail.ReadWrite",
"Mail.Send",
"Files.ReadWrite.All",
"Directory.ReadWrite.All",
"RoleManagement.ReadWrite.Directory",
"Application.ReadWrite.All",
"AppRoleAssignment.ReadWrite.All",
"User.ReadWrite.All",
"full_access_as_app"
]);
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName =~ "Add app role assignment to service principal"
| where Result =~ "success"
| extend ActorUpn = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| extend ActorIp = iff(
isnotempty(tostring(InitiatedBy.user.ipAddress)),
tostring(InitiatedBy.user.ipAddress),
tostring(InitiatedBy.app.ipAddress))
| extend TargetSpName = tostring(TargetResources[0].displayName)
| extend TargetSpId = tostring(TargetResources[0].id)
| mv-expand ModProp = TargetResources[0].modifiedProperties
| where tostring(ModProp.displayName) =~ "AppRole.Value"
| extend AppRoleName = trim('"', tostring(ModProp.newValue))
| where AppRoleName in~ (HighRiskRoles)
| extend AccountName = iff(Actor has "@", tostring(split(Actor, "@")[0]), Actor)
| extend AccountUPNSuffix = iff(Actor has "@", tostring(split(Actor, "@")[1]), "")
| project TimeGenerated, Actor, AccountName, AccountUPNSuffix, ActorIp,
TargetSpName, TargetSpId, AppRoleName, CorrelationId
| sort by TimeGenerated desc
id: 840c673e-a712-49eb-a6a9-6759d5259c4b
name: High-privilege application role assigned to service principal
description: |
Identifies application role assignments to service principals granting high-risk
permissions such as Mail.ReadWrite, Directory.ReadWrite.All, or
RoleManagement.ReadWrite.Directory, which provide tenant-wide API access
without user context.
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
tactics:
- Persistence
- CredentialAccess
relevantTechniques:
- T1098.003
- T1528
query: |
let timeframe = 1d;
let HighRiskRoles = dynamic([
"Mail.ReadWrite",
"Mail.Send",
"Files.ReadWrite.All",
"Directory.ReadWrite.All",
"RoleManagement.ReadWrite.Directory",
"Application.ReadWrite.All",
"AppRoleAssignment.ReadWrite.All",
"User.ReadWrite.All",
"full_access_as_app"
]);
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName =~ "Add app role assignment to service principal"
| where Result =~ "success"
| extend ActorUpn = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| extend ActorIp = iff(
isnotempty(tostring(InitiatedBy.user.ipAddress)),
tostring(InitiatedBy.user.ipAddress),
tostring(InitiatedBy.app.ipAddress))
| extend TargetSpName = tostring(TargetResources[0].displayName)
| extend TargetSpId = tostring(TargetResources[0].id)
| mv-expand ModProp = TargetResources[0].modifiedProperties
| where tostring(ModProp.displayName) =~ "AppRole.Value"
| extend AppRoleName = trim('"', tostring(ModProp.newValue))
| where AppRoleName in~ (HighRiskRoles)
| extend AccountName = iff(Actor has "@", tostring(split(Actor, "@")[0]), Actor)
| extend AccountUPNSuffix = iff(Actor has "@", tostring(split(Actor, "@")[1]), "")
| project TimeGenerated, Actor, AccountName, AccountUPNSuffix, ActorIp,
TargetSpName, TargetSpId, AppRoleName, CorrelationId
| sort by TimeGenerated desc
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Actor
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ActorIp
version: 1.0.0
metadata:
source:
kind: Community
author:
name: descambiado
support:
tier: Community
categories:
domains: [ "Security - Threat Protection", "Identity" ]
| Sentinel Table | Notes |
|---|---|
AuditLogs | Ensure this data connector is enabled |
Scenario: Scheduled job for Microsoft Purge Service running under a service principal
Filter/Exclusion: Exclude service principals associated with the “Microsoft Purge Service” or use a filter like servicePrincipalNames contains "purge-service"
Scenario: Azure AD Connect synchronization service using a service principal
Filter/Exclusion: Exclude service principals with names like “AzureADConnect” or use a filter like servicePrincipalNames contains "AzureADConnect"
Scenario: Microsoft Intune service principal assigned for device management
Filter/Exclusion: Exclude service principals with names like “Intune” or use a filter like servicePrincipalNames contains "Intune"
Scenario: Azure Key Vault access policy assigned to a service principal for secret management
Filter/Exclusion: Exclude service principals associated with Key Vault operations using a filter like resourceAppId equals "https://vault.azure.net"
Scenario: Azure DevOps service principal used for CI/CD pipeline tasks
Filter/Exclusion: Exclude service principals with names like “devops-pipeline” or use a filter like servicePrincipalNames contains "devops"