Service principal or application credential addition by a rarely observed actor


Description

Hunting query that looks for credential additions or updates on service principals and applications performed by actors (users or apps) that have not been observed initiating the same operations in the previous 90 days. Covered operations are "Add service principal credentials" and "Update application - Certificates and secrets management", which correspond to adding passwordCredentials or keyCredentials to an existing registration. A rarely observed actor performing these operations can indicate persistence activity, such as a compromised account adding a backdoor credential to a high-privilege application. This query is hypothesis-driven and requires analyst validation. Benign matches include newly onboarded administrators, infrastructure-as-code pipelines running for the first time, and certificate rotation performed by a different operator than usual. References: - https://learn.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities - https://attack.mitre.org/techniques/T1098/001/

Query · kql

let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let baselineLookback = starttime - 90d;
let credOps = dynamic([
    "Add service principal credentials",
    "Update application - Certificates and secrets management"
]);
// Build 90-day baseline of actors who have previously performed credential operations
let KnownActors =
    AuditLogs
    | where TimeGenerated between (baselineLookback .. starttime)
    | where OperationName in~ (credOps)
    | extend ActorUpn = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
    | extend ActorApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
    | extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
    | where isnotempty(Actor)
    | summarize by Actor;
// Credential operations in the current window
AuditLogs
| where TimeGenerated between (starttime .. endtime)
| where OperationName in~ (credOps)
| extend ActorUpn = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend ActorApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
| extend ActorIp  = iff(
      isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)),
      tostring(parse_json(tostring(InitiatedBy.user)).ipAddress),
      tostring(parse_json(tostring(InitiatedBy.app)).ipAddress))
| extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| where isnotempty(Actor)
// Keep only actors not seen in the 90-day baseline
| join kind=leftanti KnownActors on Actor
| extend TargetDisplayName = tostring(TargetResources[0].displayName)
| extend TargetId = tostring(TargetResources[0].id)
| extend TargetType = tostring(TargetResources[0].type)
| extend AccountName = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[0]), Actor)
| extend AccountUPNSuffix = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[1]), "")
| project
    TimeGenerated,
    OperationName,
    Actor,
    AccountName,
    AccountUPNSuffix,
    ActorApp,
    ActorIp,
    TargetDisplayName,
    TargetId,
    TargetType,
    CorrelationId,
    Result
| sort by TimeGenerated desc
Raw source Service principal or application credential addition by a rarely observed actor · KQL
Esc
Published by Azure/Azure-Sentinel ↗, licensed under MIT ↗. Reproduced here unmodified.
id: 138381e3-95d5-4d21-ab0b-13f941b82acc
name: Service principal or application credential addition by a rarely observed actor
description: |
  Hunting query that looks for credential additions or updates on service principals and
  applications performed by actors (users or apps) that have not been observed initiating
  the same operations in the previous 90 days. Covered operations are
  "Add service principal credentials" and "Update application - Certificates and secrets
  management", which correspond to adding passwordCredentials or keyCredentials to an
  existing registration. A rarely observed actor performing these operations can indicate
  persistence activity, such as a compromised account adding a backdoor credential to a
  high-privilege application.
  This query is hypothesis-driven and requires analyst validation. Benign matches include
  newly onboarded administrators, infrastructure-as-code pipelines running for the first
  time, and certificate rotation performed by a different operator than usual.
  References:
  - https://learn.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities
  - https://attack.mitre.org/techniques/T1098/001/
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - Persistence
relevantTechniques:
  - T1098.001
query: |
  let starttime = todatetime('{{StartTimeISO}}');
  let endtime = todatetime('{{EndTimeISO}}');
  let baselineLookback = starttime - 90d;
  let credOps = dynamic([
      "Add service principal credentials",
      "Update application - Certificates and secrets management"
  ]);
  // Build 90-day baseline of actors who have previously performed credential operations
  let KnownActors =
      AuditLogs
      | where TimeGenerated between (baselineLookback .. starttime)
      | where OperationName in~ (credOps)
      | extend ActorUpn = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
      | extend ActorApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
      | extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
      | where isnotempty(Actor)
      | summarize by Actor;
  // Credential operations in the current window
  AuditLogs
  | where TimeGenerated between (starttime .. endtime)
  | where OperationName in~ (credOps)
  | extend ActorUpn = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
  | extend ActorApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
  | extend ActorIp  = iff(
        isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)),
        tostring(parse_json(tostring(InitiatedBy.user)).ipAddress),
        tostring(parse_json(tostring(InitiatedBy.app)).ipAddress))
  | extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
  | where isnotempty(Actor)
  // Keep only actors not seen in the 90-day baseline
  | join kind=leftanti KnownActors on Actor
  | extend TargetDisplayName = tostring(TargetResources[0].displayName)
  | extend TargetId = tostring(TargetResources[0].id)
  | extend TargetType = tostring(TargetResources[0].type)
  | extend AccountName = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[0]), Actor)
  | extend AccountUPNSuffix = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[1]), "")
  | project
      TimeGenerated,
      OperationName,
      Actor,
      AccountName,
      AccountUPNSuffix,
      ActorApp,
      ActorIp,
      TargetDisplayName,
      TargetId,
      TargetType,
      CorrelationId,
      Result
  | 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" ]

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.