Detection rules › Kusto

New access credential added to Application or Service Principal

Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

This will alert when an admin or app owner account adds a new credential to an Application or Service Principal where a verify KeyCredential was already present for the app. 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

Event coverage

Rule body kusto

id: 79566f41-df67-4e10-a703-c38a6213afd8
name: New access credential added to Application or Service Principal
description: |
  'This will alert when an admin or app owner account adds a new credential to an Application or Service Principal where a verify KeyCredential was already present for the app.
  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: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1550.001
tags:
  - Solorigate
  - NOBELIUM
query: |
  AuditLogs
  | where OperationName has_any ("Add service principal", "Certificates and secrets management") // captures "Add service principal", "Add service principal credentials", and "Update application - Certificates and secrets management" events
  | where Result =~ "success"
  | where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "Application"
        | extend targetDisplayName = tostring(TargetResource.displayName),
                 targetId = tostring(TargetResource.id),
                 targetType = tostring(TargetResource.type),
                 keyEvents = TargetResource.modifiedProperties
    )
  | mv-apply Property = keyEvents on 
    (
        where Property.displayName =~ "KeyDescription"
        | extend new_value_set = parse_json(tostring(Property.newValue)),
                 old_value_set = parse_json(tostring(Property.oldValue))
    )
  | where old_value_set != "[]"
  | extend diff = set_difference(new_value_set, old_value_set)
  | where isnotempty(diff)
  | parse diff with * "KeyIdentifier=" keyIdentifier:string ",KeyType=" keyType:string ",KeyUsage=" keyUsage:string ",DisplayName=" keyDisplayName:string "]" *
  | where keyUsage =~ "Verify"
  | mv-apply AdditionalDetail = AdditionalDetails on 
    (
        where AdditionalDetail.key =~ "User-Agent"
        | extend UserAgent = tostring(AdditionalDetail.value)
    )
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
  // The below line is currently commented out but Microsoft Sentinel users can modify this query to show only Application or only Service Principal events in their environment
  //| where targetType =~ "Application" // or targetType =~ "ServicePrincipal"
  | project-away diff, new_value_set, old_value_set
  | project-reorder TimeGenerated, OperationName, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIpAddress, UserAgent, targetDisplayName, targetId, targetType, keyDisplayName, keyType, keyUsage, keyIdentifier, CorrelationId, TenantId
  | extend Name = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
version: 1.1.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName has_any ("Add service principal", "Certificates and secrets management")

Stage 3: where

| where Result =~ "success"

Stage 4: where

| where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"

Stage 5: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "Application"
      | extend targetDisplayName = tostring(TargetResource.displayName),
               targetId = tostring(TargetResource.id),
               targetType = tostring(TargetResource.type),
               keyEvents = TargetResource.modifiedProperties
  )

Stage 6: kusto:mv-apply

| mv-apply Property = keyEvents on 
  (
      where Property.displayName =~ "KeyDescription"
      | extend new_value_set = parse_json(tostring(Property.newValue)),
               old_value_set = parse_json(tostring(Property.oldValue))
  )

Stage 7: where

| where old_value_set != "[]"

Stage 8: extend

| extend diff = set_difference(new_value_set, old_value_set)

Stage 9: where

| where isnotempty(diff)

Stage 10: parse

| parse diff with * "KeyIdentifier=" keyIdentifier:string ",KeyType=" keyType:string ",KeyUsage=" keyUsage:string ",DisplayName=" keyDisplayName:string "]" *

Stage 11: where

| where keyUsage =~ "Verify"

Stage 12: kusto:mv-apply

| mv-apply AdditionalDetail = AdditionalDetails on 
  (
      where AdditionalDetail.key =~ "User-Agent"
      | extend UserAgent = tostring(AdditionalDetail.value)
  )

Stage 13: extend (5 consecutive steps)

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))

Stage 14: project-away

| project-away diff, new_value_set, old_value_set

Stage 15: project-reorder

| project-reorder TimeGenerated, OperationName, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIpAddress, UserAgent, targetDisplayName, targetId, targetType, keyDisplayName, keyType, keyUsage, keyIdentifier, CorrelationId, TenantId

Stage 16: extend

| extend Name = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])

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
OperationNamematch
  • Add service principal
  • Certificates and secrets management
Resulteq
  • success
diffis_not_null
  • (no value, null check)
displayNameeq
  • KeyDescription
displayNamematch
  • @ transforms: tostring, term
keyeq
  • User-Agent
keyUsageeq
  • Verify
old_value_setne
  • [] transforms: cased
typeeq
  • Application
userPrincipalNamematch
  • @ transforms: tostring, term

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
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
Nameextend
UPNSuffixextend