Detection rules › Kusto

Suspicious linking of existing user to external User

Severity
medium
Time window
1d
Group by
CorrelationId
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query will detect when an attempt is made to update an existing user and link it to an guest or external identity. These activities are unusual and such linking of external identities should be investigated. In some cases you may see internal Entra ID sync accounts (Sync_) do this which may be benign

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

PlatformRecord / event type
AzureUpdate user

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: 22a320c2-e1e5-4c74-a35b-39fc9cdcf859
name: Suspicious linking of existing user to external User
description: |
  ' This query will detect when an attempt is made to update an existing user and link it to an guest or external identity. These activities are unusual and such linking of external 
  identities should be investigated. In some cases you may see internal Entra ID sync accounts (Sync_) do this which may be benign'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078.004
tags:
  - GuestorExternalIdentities
query: |
  AuditLogs
  | where OperationName=~ "Update user" 
  | where Result =~ "success" 
  | mv-expand TargetResources 
  | mv-expand TargetResources.modifiedProperties 
  | extend displayName = tostring(TargetResources_modifiedProperties.displayName), 
  TargetUPN_oldValue = tostring(parse_json(tostring(TargetResources_modifiedProperties.oldValue))[0]), 
  TargetUPN_newValue = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue))[0])
  | where displayName == "UserPrincipalName" and TargetUPN_oldValue !has "#EXT" and TargetUPN_newValue has "#EXT"
  | 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(InitiatedBy.user.ipAddress)
  | extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
  | summarize arg_max(TimeGenerated, *) by CorrelationId
  | project-reorder TimeGenerated, InitiatedBy, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, TargetUPN_oldValue, TargetUPN_newValue
  | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
  | extend TargetAccountName = tostring(split(TargetUPN_oldValue, "@")[0]), TargetUPNSuffix = tostring(split(TargetUPN_oldValue, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: InitiatingAppName
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetUPN_oldValue
      - identifier: Name
        columnName: TargetAccountName
      - identifier: UPNSuffix
        columnName: TargetUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIPAddress
version: 1.1.0
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName=~ "Update user"

Stage 3: where

| where Result =~ "success"

Stage 4: mv-expand

| mv-expand TargetResources

Stage 5: mv-expand

| mv-expand TargetResources.modifiedProperties

Stage 6: extend

| extend displayName = tostring(TargetResources_modifiedProperties.displayName), 
TargetUPN_oldValue = tostring(parse_json(tostring(TargetResources_modifiedProperties.oldValue))[0]), 
TargetUPN_newValue = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue))[0])

Stage 7: where

| where displayName == "UserPrincipalName" and TargetUPN_oldValue !has "#EXT" and TargetUPN_newValue has "#EXT"

Stage 8: extend (6 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(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))

Stage 9: summarize

| summarize arg_max(TimeGenerated, *) by CorrelationId

Stage 10: project-reorder

| project-reorder TimeGenerated, InitiatedBy, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, TargetUPN_oldValue, TargetUPN_newValue

Stage 11: extend

| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])

Stage 12: extend

| extend TargetAccountName = tostring(split(TargetUPN_oldValue, "@")[0]), TargetUPNSuffix = tostring(split(TargetUPN_oldValue, "@")[1])

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TargetUPN_oldValuematch#EXTexcludes:TargetUPN_oldValue field:"TargetUPN_oldValue" value:"#EXT"

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
CorrelationIdsummarize
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend
TargetAccountNameextend
TargetUPNSuffixextend