Detection rules › Kusto
Credential added after admin consented to Application
This query will identify instances where Service Principal credentials were added to an application by one user after the application was granted admin consent rights by another user. If a threat actor obtains access to an account with sufficient privileges and adds the alternate authentication material triggering this event, the threat actor can now authenticate as the Application or Service Principal using this credential. Additional information on OAuth Credential Grants can be found in RFC 6749 Section 4.4 or https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow. For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation | |
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Add service principal credentials |
| Azure | Consent to application |
Rules detecting the same action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
Rule body
id: 707494a5-8e44-486b-90f8-155d1797a8eb
name: Credential added after admin consented to Application
description: |
'This query will identify instances where Service Principal credentials were added to an application by one user after the application was granted admin consent rights by another user.
If a threat actor obtains access to an account with sufficient privileges and adds the alternate authentication material triggering this event, the threat actor can now authenticate as the Application or Service Principal using this credential.
Additional information on OAuth Credential Grants can be found in RFC 6749 Section 4.4 or https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow.
For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1555
- T1098
tags:
- Solorigate
- NOBELIUM
query: |
let auditLookbackStart = 2d;
let auditLookbackEnd = 1d;
AuditLogs
| where TimeGenerated >= ago(auditLookbackStart)
| where OperationName =~ "Consent to application"
| where Result =~ "success"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend targetResourceName = tostring(TargetResource.displayName),
targetResourceID = tostring(TargetResource.id),
targetResourceType = tostring(TargetResource.type),
targetModifiedProp = TargetResource.modifiedProperties
)
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "ConsentContext.IsAdminConsent"
| extend isAdminConsent = trim(@'"',tostring(Property.newValue))
)
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "ConsentAction.Permissions"
| extend Consent_TargetPermissions = trim(@'"',tostring(Property.newValue))
)
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "TargetId.ServicePrincipalNames"
| extend Consent_TargetServicePrincipalNames = tostring(extract_all(@"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})",trim(@'"',tostring(Property.newValue)))[0])
)
| extend Consent_InitiatingUserOrApp = iff(isnotempty(InitiatedBy.user.userPrincipalName),tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| extend Consent_InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend Consent_InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend Consent_InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend Consent_InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend Consent_InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| join kind=inner (
AuditLogs
| where TimeGenerated >= ago(auditLookbackEnd)
| where OperationName =~ "Add service principal credentials"
| where Result =~ "success"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend targetResourceName = tostring(TargetResource.displayName),
targetResourceID = tostring(TargetResource.id),
targetModifiedProp = TargetResource.modifiedProperties
)
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "KeyDescription"
| extend Credential_TargetKeyDescription = trim(@'"',tostring(Property.newValue))
)
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "Included Updated Properties"
| extend UpdatedProperties = trim(@'"',tostring(Property.newValue))
)
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "TargetId.ServicePrincipalNames"
| extend Credential_TargetServicePrincipalNames = tostring(extract_all(@"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})",trim(@'"',tostring(Property.newValue)))[0])
)
| extend Credential_InitiatingUserOrApp = iff(isnotempty(InitiatedBy.user.userPrincipalName),tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| extend Credential_InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend Credential_InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend Credential_InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend Credential_InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend Credential_InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
) on targetResourceName, targetResourceID
| extend TimeConsent = TimeGenerated, TimeCred = TimeGenerated1
| where TimeConsent < TimeCred
| project TimeConsent, TimeCred, targetResourceName, targetResourceType, isAdminConsent,
Consent_InitiatingUserOrApp, Consent_TargetServicePrincipalNames, Consent_TargetPermissions,
Consent_InitiatingAppName, Consent_InitiatingAppServicePrincipalId, Consent_InitiatingUserPrincipalName, Consent_InitiatingAadUserId, Consent_InitiatingIpAddress,
Credential_InitiatingUserOrApp, Credential_TargetServicePrincipalNames, Credential_TargetKeyDescription,
Credential_InitiatingAppName, Credential_InitiatingAppServicePrincipalId, Credential_InitiatingUserPrincipalName, Credential_InitiatingAadUserId, Credential_InitiatingIpAddress
| extend Consent_AccountName = tostring(split(Consent_InitiatingUserPrincipalName, "@")[0]), Consent_UPNSuffix = tostring(split(Consent_InitiatingUserPrincipalName, "@")[1])
| extend Credential_AccountName = tostring(split(Credential_InitiatingUserPrincipalName, "@")[0]), Credential_UPNSuffix = tostring(split(Credential_InitiatingUserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Consent_InitiatingAppName
- identifier: AadUserId
columnName: Consent_InitiatingAppServicePrincipalId
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Credential_InitiatingAppName
- identifier: AadUserId
columnName: Credential_InitiatingAppServicePrincipalId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Consent_InitiatingUserPrincipalName
- identifier: Name
columnName: Consent_AccountName
- identifier: UPNSuffix
columnName: Consent_UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: Consent_InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Credential_InitiatingUserPrincipalName
- identifier: Name
columnName: Credential_AccountName
- identifier: UPNSuffix
columnName: Credential_UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: Credential_InitiatingAadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: Consent_InitiatingIpAddress
- entityType: IP
fieldMappings:
- identifier: Address
columnName: Credential_InitiatingIpAddress
version: 1.1.1
kind: Scheduled
Stages and Predicates
Parameters
let auditLookbackStart = 2d;
let auditLookbackEnd = 1d;
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated >= ago(auditLookbackStart)
Stage 3: where
| where OperationName =~ "Consent to application"
Stage 4: where
| where Result =~ "success"
Stage 5: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend targetResourceName = tostring(TargetResource.displayName),
targetResourceID = tostring(TargetResource.id),
targetResourceType = tostring(TargetResource.type),
targetModifiedProp = TargetResource.modifiedProperties
)
Stage 6: kusto:mv-apply
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "ConsentContext.IsAdminConsent"
| extend isAdminConsent = trim(@'"',tostring(Property.newValue))
)
Stage 7: kusto:mv-apply
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "ConsentAction.Permissions"
| extend Consent_TargetPermissions = trim(@'"',tostring(Property.newValue))
)
Stage 8: kusto:mv-apply
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "TargetId.ServicePrincipalNames"
| extend Consent_TargetServicePrincipalNames = tostring(extract_all(@"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})",trim(@'"',tostring(Property.newValue)))[0])
)
Stage 9: extend (6 consecutive steps)
| extend Consent_InitiatingUserOrApp = iff(isnotempty(InitiatedBy.user.userPrincipalName),tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| extend Consent_InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend Consent_InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend Consent_InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend Consent_InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend Consent_InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
Consent_InitiatingUserOrApp =isnotempty(userPrincipalName)tostring(InitiatedBy.user.userPrincipalName)tostring(InitiatedBy.app.displayName)Stage 10: join
| join kind=inner (
AuditLogs
| where TimeGenerated >= ago(auditLookbackEnd)
| where OperationName =~ "Add service principal credentials"
| where Result =~ "success"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend targetResourceName = tostring(TargetResource.displayName),
targetResourceID = tostring(TargetResource.id),
targetModifiedProp = TargetResource.modifiedProperties
)
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "KeyDescription"
| extend Credential_TargetKeyDescription = trim(@'"',tostring(Property.newValue))
)
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "Included Updated Properties"
| extend UpdatedProperties = trim(@'"',tostring(Property.newValue))
)
| mv-apply Property = targetModifiedProp on
(
where Property.displayName =~ "TargetId.ServicePrincipalNames"
| extend Credential_TargetServicePrincipalNames = tostring(extract_all(@"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})",trim(@'"',tostring(Property.newValue)))[0])
)
| extend Credential_InitiatingUserOrApp = iff(isnotempty(InitiatedBy.user.userPrincipalName),tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| extend Credential_InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend Credential_InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend Credential_InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend Credential_InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend Credential_InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
) on targetResourceName, targetResourceID
Stage 11: extend
| extend TimeConsent = TimeGenerated, TimeCred = TimeGenerated1
Stage 12: where
| where TimeConsent < TimeCred
Stage 13: project
| project TimeConsent, TimeCred, targetResourceName, targetResourceType, isAdminConsent,
Consent_InitiatingUserOrApp, Consent_TargetServicePrincipalNames, Consent_TargetPermissions,
Consent_InitiatingAppName, Consent_InitiatingAppServicePrincipalId, Consent_InitiatingUserPrincipalName, Consent_InitiatingAadUserId, Consent_InitiatingIpAddress,
Credential_InitiatingUserOrApp, Credential_TargetServicePrincipalNames, Credential_TargetKeyDescription,
Credential_InitiatingAppName, Credential_InitiatingAppServicePrincipalId, Credential_InitiatingUserPrincipalName, Credential_InitiatingAadUserId, Credential_InitiatingIpAddress
Stage 14: extend
| extend Consent_AccountName = tostring(split(Consent_InitiatingUserPrincipalName, "@")[0]), Consent_UPNSuffix = tostring(split(Consent_InitiatingUserPrincipalName, "@")[1])
Stage 15: extend
| extend Credential_AccountName = tostring(split(Credential_InitiatingUserPrincipalName, "@")[0]), Credential_UPNSuffix = tostring(split(Credential_InitiatingUserPrincipalName, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq |
Result | eq |
| field:"Result" kind:eq value:"success" |
TimeConsent | cross_field_compare |
| field:"TimeConsent" kind:cross_field_compare value:"TimeCred" |
displayName | eq |
| field:"displayName" kind:eq |
type | eq |
| field:"type" kind:eq value:"ServicePrincipal" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Consent_InitiatingAadUserId | project |
Consent_InitiatingAppName | project |
Consent_InitiatingAppServicePrincipalId | project |
Consent_InitiatingIpAddress | project |
Consent_InitiatingUserOrApp | project |
Consent_InitiatingUserPrincipalName | project |
Consent_TargetPermissions | project |
Consent_TargetServicePrincipalNames | project |
Credential_InitiatingAadUserId | project |
Credential_InitiatingAppName | project |
Credential_InitiatingAppServicePrincipalId | project |
Credential_InitiatingIpAddress | project |
Credential_InitiatingUserOrApp | project |
Credential_InitiatingUserPrincipalName | project |
Credential_TargetKeyDescription | project |
Credential_TargetServicePrincipalNames | project |
TimeConsent | project |
TimeCred | project |
isAdminConsent | project |
targetResourceName | project |
targetResourceType | project |
Consent_AccountName | extend |
Consent_UPNSuffix | extend |
Credential_AccountName | extend |
Credential_UPNSuffix | extend |