New service principal granted admin consent within one hour of creation


Description

Identifies service principals that received an app role assignment or admin consent within one hour of being registered in the tenant. Register-then-consent is a documented persistence pattern after privileged account compromise.

Query · kql

let timeframe = 1d;
let correlationWindow = 1h;
let NewSP =
    AuditLogs
    | where TimeGenerated >= ago(timeframe)
    | where OperationName =~ "Add service principal"
    | where Result =~ "success"
    | extend CreatingActorUpn = tostring(InitiatedBy.user.userPrincipalName)
    | extend CreatingActorApp = tostring(InitiatedBy.app.displayName)
    | mv-expand TargetResource = TargetResources
    | where tostring(TargetResource.type) =~ "ServicePrincipal"
    | project
        SpCreatedTime    = TimeGenerated,
        SpId             = tostring(TargetResource.id),
        SpName           = tostring(TargetResource.displayName),
        CreatingActorUpn,
        CreatingActorApp;
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName in~ (
      "Add app role assignment to service principal",
      "Consent to application"
  )
| where Result =~ "success"
| extend ConsentActorUpn  = tostring(InitiatedBy.user.userPrincipalName)
| extend ConsentActorApp  = tostring(InitiatedBy.app.displayName)
| extend ConsentActorIp   = iff(
      isnotempty(tostring(InitiatedBy.user.ipAddress)),
      tostring(InitiatedBy.user.ipAddress),
      tostring(InitiatedBy.app.ipAddress))
| mv-expand TargetResource = TargetResources
| where tostring(TargetResource.type) =~ "ServicePrincipal"
| extend ConsentTargetId  = tostring(TargetResource.id)
| join kind=inner NewSP on $left.ConsentTargetId == $right.SpId
| where TimeGenerated between (SpCreatedTime .. (SpCreatedTime + correlationWindow))
| extend CreatingActor = iff(isnotempty(CreatingActorUpn), CreatingActorUpn, CreatingActorApp)
| extend ConsentActor  = iff(isnotempty(ConsentActorUpn),  ConsentActorUpn,  ConsentActorApp)
| extend AccountName      = iff(ConsentActor has "@",
      tostring(split(ConsentActor, "@")[0]), ConsentActor)
| extend AccountUPNSuffix = iff(ConsentActor has "@",
      tostring(split(ConsentActor, "@")[1]), "")
| project
    TimeGenerated,
    SpName,
    SpId,
    OperationName,
    CreatingActor,
    ConsentActor,
    AccountName,
    AccountUPNSuffix,
    ConsentActorIp,
    SpCreatedTime,
    CorrelationId
| sort by TimeGenerated desc
Raw source New service principal granted admin consent within one hour of creation · KQL
Esc
Published by Azure/Azure-Sentinel ↗, licensed under MIT ↗. Reproduced here unmodified.
id: dc9c0f2a-68f4-4415-aa7e-81d87149c222
name: New service principal granted admin consent within one hour of creation
description: |
  Identifies service principals that received an app role assignment or admin consent
  within one hour of being registered in the tenant. Register-then-consent is a
  documented persistence pattern after privileged account compromise.
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1528
  - T1098.003
query: |
  let timeframe = 1d;
  let correlationWindow = 1h;
  let NewSP =
      AuditLogs
      | where TimeGenerated >= ago(timeframe)
      | where OperationName =~ "Add service principal"
      | where Result =~ "success"
      | extend CreatingActorUpn = tostring(InitiatedBy.user.userPrincipalName)
      | extend CreatingActorApp = tostring(InitiatedBy.app.displayName)
      | mv-expand TargetResource = TargetResources
      | where tostring(TargetResource.type) =~ "ServicePrincipal"
      | project
          SpCreatedTime    = TimeGenerated,
          SpId             = tostring(TargetResource.id),
          SpName           = tostring(TargetResource.displayName),
          CreatingActorUpn,
          CreatingActorApp;
  AuditLogs
  | where TimeGenerated >= ago(timeframe)
  | where OperationName in~ (
        "Add app role assignment to service principal",
        "Consent to application"
    )
  | where Result =~ "success"
  | extend ConsentActorUpn  = tostring(InitiatedBy.user.userPrincipalName)
  | extend ConsentActorApp  = tostring(InitiatedBy.app.displayName)
  | extend ConsentActorIp   = iff(
        isnotempty(tostring(InitiatedBy.user.ipAddress)),
        tostring(InitiatedBy.user.ipAddress),
        tostring(InitiatedBy.app.ipAddress))
  | mv-expand TargetResource = TargetResources
  | where tostring(TargetResource.type) =~ "ServicePrincipal"
  | extend ConsentTargetId  = tostring(TargetResource.id)
  | join kind=inner NewSP on $left.ConsentTargetId == $right.SpId
  | where TimeGenerated between (SpCreatedTime .. (SpCreatedTime + correlationWindow))
  | extend CreatingActor = iff(isnotempty(CreatingActorUpn), CreatingActorUpn, CreatingActorApp)
  | extend ConsentActor  = iff(isnotempty(ConsentActorUpn),  ConsentActorUpn,  ConsentActorApp)
  | extend AccountName      = iff(ConsentActor has "@",
        tostring(split(ConsentActor, "@")[0]), ConsentActor)
  | extend AccountUPNSuffix = iff(ConsentActor has "@",
        tostring(split(ConsentActor, "@")[1]), "")
  | project
      TimeGenerated,
      SpName,
      SpId,
      OperationName,
      CreatingActor,
      ConsentActor,
      AccountName,
      AccountUPNSuffix,
      ConsentActorIp,
      SpCreatedTime,
      CorrelationId
  | sort by TimeGenerated desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ConsentActor
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: SpName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ConsentActorIp
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.