Detection rules › Kusto
Possible SignIn from Azure Backdoor
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
| Tactic | Techniques |
|---|---|
| Persistence | T1098 Account Manipulation |
Event coverage
| Provider | Event |
|---|---|
| Entra-AuditLogs | Add unverified domain |
Rules detecting the same action
Other rules on this platform that filter on the same API call or operation.
- Anomalous sign-in location by user account and authenticating application (Kusto)
- Anomalous Single Factor Signin (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Many Failed SignIns (Panther)
- Azure Portal sign in from another Azure Tenant (Kusto)
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access (Elastic)
- Azure SignIn via Legacy Authentication Protocol (Panther)
- Detect non-admin requesting token for admin applications (Kusto)
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.
| Field | Kind | Values |
|---|---|---|
OperationName | eq |
|
Result | eq |
|
ResultType | eq |
|
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.
| Field | Source |
|---|---|
DomainAddedTime | summarize |
DomainName | summarize |
InitiatedBy | summarize |
ModifiedProperties | summarize |
Domain | extend |
Name | extend |