Detection rules › Kusto

Possible SignIn from Azure Backdoor

Severity
medium
Time window
1h
Group by
DomainName, InitiatedBy, UserDomain
Source
github.com/Azure/Azure-Sentinel

Identifies when a user adds an unverified domain as an authentication method, followed by a sign-in from a user the newly added domain. Threat actors may add custom domains to create a backdoor to your tenant. It's important to monitor whenever custom domains are added to the tenant.

MITRE ATT&CK coverage

TacticTechniques
PersistenceT1098 Account Manipulation

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: fa00014c-c5f4-4715-8f5b-ba567e19e41e
name: Possible SignIn from Azure Backdoor
description: |
  'Identifies when a user adds an unverified domain as an authentication method, followed by a sign-in from a user the newly added domain. Threat actors may add custom domains to create a backdoor to your tenant. It's important to monitor whenever custom domains are added to the tenant.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
      - AuditLogs 
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1098
query: |
  // Microsoft Entra ID  Backdoors: Identity Federation
  //Ref: https://www.inversecos.com/2021/11/how-to-detect-azure-active-directory.html
  AuditLogs
  | where OperationName == "Add unverified domain"
  | where Result == "success"
  | extend InitiatedBy = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
  | extend DomainName = tostring(TargetResources[0].displayName)
  | summarize DomainAddedTime = min(TimeGenerated), ModifiedProperties = make_set(parse_json(TargetResources[0].modifiedProperties),1048576) by InitiatedBy, DomainName
  | join kind=inner (
  SigninLogs
  | where ResultType == "0"
  | extend UserDomain = tostring(parse_json(split(UserPrincipalName,"@",1)[0]))
  | summarize SignInTime = min(TimeGenerated)  by UserPrincipalName, IPAddress, tostring(LocationDetails),AppDisplayName,ResourceDisplayName,UserDomain
  ) on $left.DomainName == $right.UserDomain
  // Getting UserName and Domain
  | extend Name = split(UserPrincipalName,"@",0), Domain = split(UserPrincipalName,"@",1)
  | mv-expand Name,Domain
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: NTDomain
        columnName: UserDomain
      - identifier: FullName
        columnName: UserPrincipalName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
customDetails:
  AppDisplayName: AppDisplayName
  ResourceDisplayName: ResourceDisplayName
  DomainAdded: DomainName
  InitiatedBy: InitiatedBy
  ModifiedProperties: ModifiedProperties
  DomainAddedTime: DomainAddedTime
  SignInTime: SignInTime
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName == "Add unverified domain"

Stage 3: where

| where Result == "success"

Stage 4: extend

| extend InitiatedBy = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)

Stage 5: extend

| extend DomainName = tostring(TargetResources[0].displayName)

Stage 6: summarize

| summarize DomainAddedTime = min(TimeGenerated), ModifiedProperties = make_set(parse_json(TargetResources[0].modifiedProperties),1048576) by InitiatedBy, DomainName

Stage 7: join

| join kind=inner (
SigninLogs
| where ResultType == "0"
| extend UserDomain = tostring(parse_json(split(UserPrincipalName,"@",1)[0]))
| summarize SignInTime = min(TimeGenerated)  by UserPrincipalName, IPAddress, tostring(LocationDetails),AppDisplayName,ResourceDisplayName,UserDomain
) on $left.DomainName == $right.UserDomain

Stage 8: extend

| extend Name = split(UserPrincipalName,"@",0), Domain = split(UserPrincipalName,"@",1)

Stage 9: mv-expand

| mv-expand Name,Domain

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
OperationNameeq
  • Add unverified domain transforms: cased
Resulteq
  • success transforms: cased
ResultTypeeq
  • 0 transforms: cased

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
DomainAddedTimesummarize
DomainNamesummarize
InitiatedBysummarize
ModifiedPropertiessummarize
Domainextend
Nameextend