Query Details

Added Credential To Privileged Workload By Lower Or Non Privileged User Workload Identity Info

Query

id: 8a790672-0ad9-403a-a80d-f67bd0b7fea6
name: Added Credential to privileged workload by lower or non-privileged user (WorkloadIdentityInfo)
version: 1.0.0
kind: Scheduled
description: This will alert when a unprivileged or lower privileged user adds a new credential to an Application or Service Principal object. Monitor these changes to avoid account take over (for persistent access) and breach of tiering model. If a threat actor obtains access to the workload identity 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.
severity: Medium
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
- DefenseEvasion
relevantTechniques:
- T1078
- T1550
query: |+
  // Exclude automated rotation of certificates for Managed Identities by Azure Back-end / IDMS
  let ExcludedActor = dynamic (["Managed Service Identity", "Windows Azure Service Management API"]);
  AuditLogs
  // Captures "Add service principal", "Add service principal credentials", and "Update application - Certificates and secrets management" events
  | where OperationName has_any ("Add service principal", "Certificates and secrets management", "Update application")
  | where Result =~ "success"
  | where parse_json(tostring(InitiatedBy.app)).displayName !in~ (ExcludedActor) and parse_json(tostring(InitiatedBy.app)).appId != "null"
  | extend InitiatingUserOrAppId = iff(isnotempty(InitiatedBy.user.id), tostring(InitiatedBy.user.id), tostring(InitiatedBy.app.id))
  | extend InitiatingIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
  | join kind=leftouter (UnifiedIdentityInfo
      | project
          ObjectId,
          InitiatingUserOrAppClassification = Classification,
          InitiatingUserOrAppName = ObjectDisplayName,
          InitiatingUserOrAppDirectoryRoles = EntraIdRoles,
          InitiatingUserOrAppGraphRoles = AppRoles)
      on $left.InitiatingUserOrAppId == $right.ObjectId
  | mv-apply TargetResource = TargetResources on 
      (
      where TargetResource.type =~ "Application" or TargetResource.type =~ "ServicePrincipal"
      | extend
          TargetName = tostring(TargetResource.displayName),
          ResourceId = tostring(TargetResource.id),
          WorkloadIdentityObjectType = tostring(TargetResource.type),
          keyEvents = TargetResource.modifiedProperties
      )
  | mv-apply Property = keyEvents on 
      (
      where Property.displayName =~ "KeyDescription" or Property.displayName =~ "FederatedIdentityCredentials"
      | extend
          new_value_set = parse_json(tostring(Property.newValue)),
          old_value_set = parse_json(tostring(Property.oldValue))
      )
  | 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" or isnotempty(parse_json(tostring(diff[0].Audiences))[0])
  | mv-apply AdditionalDetail = AdditionalDetails on 
      (
      where AdditionalDetail.key =~ "User-Agent"
      | extend UserAgent = tostring(AdditionalDetail.value)
      )
  | mv-apply AdditionalDetail = AdditionalDetails on 
      (
      where AdditionalDetail.key =~ "AppId"
      | extend ApplicationId = tostring(AdditionalDetail.value)
      )
  | join kind=leftouter (PrivilegedWorkloadIdentityInfo
      | extend WorkloadIdClassification = EnterpriseAccessModelTiering
      | extend WorkloadIdentityObjectId = ServicePrincipalObjectId)
      on ApplicationId
  | extend CredentialName = iff(isnotempty(keyDisplayName), keyDisplayName, diff[0].Name)
  | extend CredentialIdentifier = iff(isnotempty(keyIdentifier), keyIdentifier, diff[0].Subject)
  | extend CredentialType = iff(isnotempty(keyType), keyType, keyEvents[0].displayName)
  | extend CredentialUsage = iff(isnotempty(keyUsage), keyUsage, tostring(diff[0].Audiences))
  // Use Target Name if Workload Identity Name is unknown
  | extend WorkloadIdentityName = iff(isempty(WorkloadIdentityName), TargetName, WorkloadIdentityName)
  // Compare Classification of Application with Actor to detect "Tiering" breach, Allowlist all Control Plane roles
  | where parse_json(tostring(parse_json(InitiatingUserOrAppClassification))) !contains WorkloadIdClassification and (parse_json(tostring(parse_json(InitiatingUserOrAppClassification))) !contains "ControlPlane")
  // Optional: Exclude all Apps with Unclassified Permissions
  //| where WorkloadIdClassification != "Unclassified"
  | extend Severity = iff(WorkloadIdClassification contains "ControlPlane", "High", "Medium")

suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AllEntities
    groupByEntities:
    - Account
    - IP
    - CloudApplication
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: Added {{CredentialType}} to workload with {{WorkloadIdClassification}} privileges by {{InitiatingUserOrAppClassification}} user
  alertDescriptionFormat: '{{InitiatingUserOrAppName}} has added a credential for {{WorkloadIdentityName}} on {{WorkloadIdClassification}}. Verify this change to avoid account take over (for persistent access) and breach of tiering model. If a threat actor obtains access to the workload identity 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.'
  alertSeverityColumnName: Severity
  alertDynamicProperties: []
customDetails:
  WorkloadIdentityName: WorkloadIdentityName
  WorkloadIdentityType: WorkloadIdentityType
  ServicePrincipalId: ServicePrincipalObjectId
  ApplicationId: ServicePrincipalObjectId
  IsFirstPartyApps: IsFirstPartyApp
  PrivilegedAccess: WorkloadIdClassification
  EntraDirectoryRoles: EntraIdRoles
  MSGraphRoles: AppRolePermissions
  CredentialIdentifier: CredentialIdentifier
  CredentialType: CredentialType
  CredentialUsage: CredentialUsage
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: AadUserId
    columnName: InitiatingUserOrAppId
- entityType: CloudApplication
  fieldMappings:
  - identifier: AppId
    columnName: ApplicationId
- entityType: CloudApplication
  fieldMappings:
  - identifier: Name
    columnName: WorkloadIdentityName
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: InitiatingIpAddress
suppressionDuration: 5h

Explanation

This query is designed to alert when an unprivileged or lower privileged user adds a new credential to an Application or Service Principal object. The purpose is to monitor these changes and prevent account takeovers and breaches of the tiering model. The query filters for specific events related to adding service principals and managing certificates and secrets. It also excludes automated rotations of certificates for Managed Identities by Azure Back-end/IDMS. The query joins different data sources to gather information about the user or application that initiated the change, the target resource, and the specific details of the credential change. The query also includes incident configuration settings for creating and grouping incidents, as well as alert details override for customizing the alert display name and description.

Details

Thomas Naunheim profile picture

Thomas Naunheim

Released: December 18, 2023

Tables

AuditLogsUnifiedIdentityInfoPrivilegedWorkloadIdentityInfo

Keywords

Devices,Intune,User,Application,ServicePrincipal,Credential,AuditLogs,UnifiedIdentityInfo,PrivilegedWorkloadIdentityInfo

Operators

has_anywhere=~!in~!=isnotemptyiffjoinextendmv-applyparse_jsonset_differenceisnotemptywithisnotemptyorcontains

Actions