Detection rules › Kusto

Credential added after admin consented to Application

Status
available
Severity
medium
Time window
2d
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
PersistenceT1098 Account Manipulation
Privilege EscalationT1098 Account Manipulation
Credential AccessT1555 Credentials from Password Stores

Event coverage

Rule body kusto

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 =
ifisnotempty(userPrincipalName)tostring(InitiatedBy.user.userPrincipalName)
elsetostring(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

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
OperationNameeq
  • Add service principal credentials
  • Consent to application
Resulteq
  • success
TimeConsentlt
  • TimeCred transforms: cased
displayNameeq
  • ConsentAction.Permissions
  • ConsentContext.IsAdminConsent
  • Included Updated Properties
  • KeyDescription
  • TargetId.ServicePrincipalNames
typeeq
  • ServicePrincipal

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
Consent_InitiatingAadUserIdproject
Consent_InitiatingAppNameproject
Consent_InitiatingAppServicePrincipalIdproject
Consent_InitiatingIpAddressproject
Consent_InitiatingUserOrAppproject
Consent_InitiatingUserPrincipalNameproject
Consent_TargetPermissionsproject
Consent_TargetServicePrincipalNamesproject
Credential_InitiatingAadUserIdproject
Credential_InitiatingAppNameproject
Credential_InitiatingAppServicePrincipalIdproject
Credential_InitiatingIpAddressproject
Credential_InitiatingUserOrAppproject
Credential_InitiatingUserPrincipalNameproject
Credential_TargetKeyDescriptionproject
Credential_TargetServicePrincipalNamesproject
TimeConsentproject
TimeCredproject
isAdminConsentproject
targetResourceNameproject
targetResourceTypeproject
Consent_AccountNameextend
Consent_UPNSuffixextend
Credential_AccountNameextend
Credential_UPNSuffixextend