Detection rules › Kusto

Threat Essentials - Multiple admin membership removals from newly created admin.

Status
available
Severity
medium
Time window
7d
Group by
Initiator, OperationName, Result, RoleName, Target
Source
github.com/Azure/Azure-Sentinel

This query detects when newly created Global admin removes multiple existing global admins which can be an attempt by adversaries to lock down organization and retain sole access. Investigate reasoning and intention of multiple membership removal by new Global admins and take necessary actions accordingly.

MITRE ATT&CK coverage

TacticTechniques
ImpactT1531 Account Access Removal

Event coverage

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body kusto

id: 199978c5-cd6d-4194-b505-8ef5800739df
name: Threat Essentials - Multiple admin membership removals from newly created admin.
description: |
  'This query detects when newly created Global admin removes multiple existing global admins which can be an attempt by adversaries to lock down organization and retain sole access. 
   Investigate reasoning and intention of multiple membership removal by new Global admins and take necessary actions accordingly.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1531
tags:
  - DEV-0537
query: |
  let lookback = 7d; 
  let timeframe = 1h; 
  let GlobalAdminsRemoved = AuditLogs 
  | where TimeGenerated > ago(timeframe) 
  | where Category =~ "RoleManagement" 
  | where AADOperationType in~ ("Unassign", "RemoveEligibleRole") 
  | where ActivityDisplayName has_any ("Remove member from role", "Remove eligible member from role") 
  | mv-expand TargetResources 
  | mv-expand TargetResources.modifiedProperties 
  | extend displayName_ = tostring(TargetResources_modifiedProperties.displayName) 
  | where displayName_ =~ "Role.DisplayName" 
  | extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.oldValue))) 
  | where RoleName =~ "Global Administrator" // Add other Privileged role if applicable 
  | extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName) 
  | extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)) 
  | where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"  // Filtering PIM events 
  | extend Target = tostring(TargetResources.userPrincipalName) 
  | summarize RemovedGlobalAdminTime = max(TimeGenerated), TargetAdmins = make_set(Target) by OperationName,  RoleName, Initiator, Result; 
  let GlobalAdminsAdded = AuditLogs 
  | where TimeGenerated > ago(lookback) 
  | where Category =~ "RoleManagement" 
  | where AADOperationType in~ ("Assign", "AssignEligibleRole") 
  | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role") and Result =~ "success" 
  | mv-expand TargetResources 
  | mv-expand TargetResources.modifiedProperties 
  | extend displayName_ = tostring(TargetResources_modifiedProperties.displayName) 
  | where displayName_ =~ "Role.DisplayName" 
  | extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue))) 
  | where RoleName =~ "Global Administrator" // Add other Privileged role if applicable 
  | extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName) 
  | extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)) 
  | where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"  // Filtering PIM events 
  | extend Target = tostring(TargetResources.userPrincipalName) 
  | summarize AddedGlobalAdminTime = max(TimeGenerated) by OperationName,  RoleName, Target, Initiator, Result; 
  GlobalAdminsAdded 
  | join kind= inner GlobalAdminsRemoved on $left.Target == $right.Initiator 
  | where AddedGlobalAdminTime < RemovedGlobalAdminTime 
  | extend NoofAdminsRemoved = array_length(TargetAdmins) 
  | where NoofAdminsRemoved > 1
  | project AddedGlobalAdminTime, Initiator, Target, RemovedGlobalAdminTime, TargetAdmins, NoofAdminsRemoved
  | extend Name=split(Target, "@")[0], UPNSuffix=split(Target, "@")[1]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 7d;
let timeframe = 1h;

Let binding: GlobalAdminsRemoved

let GlobalAdminsRemoved = AuditLogs 
| where TimeGenerated > ago(timeframe) 
| where Category =~ "RoleManagement" 
| where AADOperationType in~ ("Unassign", "RemoveEligibleRole") 
| where ActivityDisplayName has_any ("Remove member from role", "Remove eligible member from role") 
| mv-expand TargetResources 
| mv-expand TargetResources.modifiedProperties 
| extend displayName_ = tostring(TargetResources_modifiedProperties.displayName) 
| where displayName_ =~ "Role.DisplayName" 
| extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.oldValue))) 
| where RoleName =~ "Global Administrator"
| extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName) 
| extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)) 
| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"
| extend Target = tostring(TargetResources.userPrincipalName) 
| summarize RemovedGlobalAdminTime = max(TimeGenerated), TargetAdmins = make_set(Target) by OperationName,  RoleName, Initiator, Result;

Derived from timeframe.

The stages below define let GlobalAdminsAdded (the rule's main pipeline source).

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated > ago(lookback)

Stage 3: where

| where Category =~ "RoleManagement"

Stage 4: where

| where AADOperationType in~ ("Assign", "AssignEligibleRole")

Stage 5: where

| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role") and Result =~ "success"

Stage 6: mv-expand

| mv-expand TargetResources

Stage 7: mv-expand

| mv-expand TargetResources.modifiedProperties

Stage 8: extend

| extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)

Stage 9: where

| where displayName_ =~ "Role.DisplayName"

Stage 10: extend

| extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))

Stage 11: where

| where RoleName =~ "Global Administrator"

Stage 12: extend

| extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)

Stage 13: extend

| extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))

Stage 14: where

| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"

Stage 15: extend

| extend Target = tostring(TargetResources.userPrincipalName)

Stage 16: summarize

| summarize AddedGlobalAdminTime = max(TimeGenerated) by OperationName,  RoleName, Target, Initiator, Result
Threshold
lt RemovedGlobalAdminTime

The stages below run on GlobalAdminsAdded (the outer pipeline).

Stage 17: join

GlobalAdminsAdded
| join kind= inner GlobalAdminsRemoved on $left.Target == $right.Initiator

Stage 18: where

| where AddedGlobalAdminTime < RemovedGlobalAdminTime

Stage 19: extend

| extend NoofAdminsRemoved = array_length(TargetAdmins)

Stage 20: where

| where NoofAdminsRemoved > 1

Stage 21: project

| project AddedGlobalAdminTime, Initiator, Target, RemovedGlobalAdminTime, TargetAdmins, NoofAdminsRemoved

Stage 22: extend

| extend Name=split(Target, "@")[0], UPNSuffix=split(Target, "@")[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
AADOperationTypein
  • Assign
  • AssignEligibleRole
  • RemoveEligibleRole
  • Unassign
ActivityDisplayNamematch
  • Add eligible member to role
  • Add member to role
  • Remove eligible member from role
  • Remove member from role
AddedGlobalAdminTimelt
  • RemovedGlobalAdminTime transforms: cased
Categoryeq
  • RoleManagement
Initiatorne
  • MS-PIM transforms: cased
  • MS-PIM-Fairfax transforms: cased
NoofAdminsRemovedgt
  • 1 transforms: cased
Resulteq
  • success
RoleNameeq
  • Global Administrator
displayName_eq
  • Role.DisplayName

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
AddedGlobalAdminTimeproject
Initiatorproject
NoofAdminsRemovedproject
RemovedGlobalAdminTimeproject
Targetproject
TargetAdminsproject
Nameextend
UPNSuffixextend