This hunt detects adversaries who subtly weaken security posture by adding user or location exclusions to Conditional Access policies, allowing them to bypass MFA and device compliance requirements without triggering standard state-change alerts. A proactive search is essential in Azure Sentinel because these stealthy modifications often go unnoticed until an attacker leverages the exclusion to gain unauthorized access or pivot within the environment.
let timeframe = 14d;
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where Category =~ "Policy"
| where OperationName =~ "Update conditional access policy"
| where Result =~ "success"
| mv-expand ModProp = TargetResources[0].modifiedProperties
| extend PropName = tostring(ModProp.displayName)
| extend OldValue = tostring(ModProp.oldValue)
| extend NewValue = tostring(ModProp.newValue)
| where PropName !~ "State"
| where PropName has "Condition" or NewValue has "exclude" or OldValue has "exclude"
| extend PolicyName = tostring(TargetResources[0].displayName)
| extend PolicyId = tostring(TargetResources[0].id)
| extend ActorUpn = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| extend ActorIp = iff(
isnotempty(tostring(InitiatedBy.user.ipAddress)),
tostring(InitiatedBy.user.ipAddress),
tostring(InitiatedBy.app.ipAddress))
| extend AccountName = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[0]), Actor)
| extend AccountUPNSuffix = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[1]), "")
| project
TimeGenerated,
PolicyName,
PolicyId,
PropName,
OldValue,
NewValue,
Actor,
AccountName,
AccountUPNSuffix,
ActorIp,
CorrelationId
| sort by TimeGenerated desc
id: a14106c1-af19-41fb-9fbd-6d9ef402537b
name: Conditional Access policy exclusion added
description: |
Identifies Conditional Access policy updates that narrow scope via exclusions rather than change the enabled state, a technique attackers use to bypass enforcement without triggering state-change monitoring.
description-detailed: |
An attacker with Conditional Access Administrator or Global Administrator
access can narrow a policy's scope by adding their own account, a compromised
guest account, or a service principal to its exclusion list. This achieves the
same practical bypass as disabling the policy, without the state change that
most defenses monitor for, and without interrupting enforcement for every other
user in the tenant.
This query intentionally excludes plain enabled/disabled state transitions,
which are covered separately, and instead surfaces condition-level edits so
analysts can review the raw old and new values for scope-narrowing changes
such as a growing exclude-users or exclude-groups list.
Analysts must validate every result. Benign matches include legitimate policy
tuning, break-glass account exclusions documented in change records, and
scheduled policy reviews. The signal is highest when the excluded identity is
privileged, recently created, or not a known break-glass account.
MITRE ATT&CK documents this exact pattern under T1556.009: threat actors,
including Scattered Spider, have added trusted locations and exclusions to
Conditional Access policies in real intrusions to maintain access after
initial compromise.
References:
- https://learn.microsoft.com/entra/identity/conditional-access/overview
- https://learn.microsoft.com/entra/identity/monitoring-health/reference-audit-activities
- https://attack.mitre.org/techniques/T1556/009/
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
tactics:
- DefenseEvasion
relevantTechniques:
- T1556.009
query: |
let timeframe = 14d;
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where Category =~ "Policy"
| where OperationName =~ "Update conditional access policy"
| where Result =~ "success"
| mv-expand ModProp = TargetResources[0].modifiedProperties
| extend PropName = tostring(ModProp.displayName)
| extend OldValue = tostring(ModProp.oldValue)
| extend NewValue = tostring(ModProp.newValue)
| where PropName !~ "State"
| where PropName has "Condition" or NewValue has "exclude" or OldValue has "exclude"
| extend PolicyName = tostring(TargetResources[0].displayName)
| extend PolicyId = tostring(TargetResources[0].id)
| extend ActorUpn = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| extend ActorIp = iff(
isnotempty(tostring(InitiatedBy.user.ipAddress)),
tostring(InitiatedBy.user.ipAddress),
tostring(Initiate
| Sentinel Table | Notes |
|---|---|
AuditLogs | Ensure this data connector is enabled |
Here are 4 specific false positive scenarios for the Conditional Access policy exclusion added detection rule, including suggested filters and exclusions:
Scenario: Automated Onboarding of New Department via Azure AD Connect Sync
Created By user is a known service account (e.g., svc-identity-sync) AND the policy name contains specific onboarding keywords like “Onboard” or “NewDept”. Additionally, filter out changes occurring within 15 minutes of a scheduled Azure AD Connect sync job completion.Scenario: Temporary Exclusion for Legacy Application Migration
Policy Name containing “Legacy” or “Migration”. Furthermore, if the exclusion type is a specific Application ID that matches a known list of legacy app IDs (e.g., app-legacy-exchange-2019), suppress the alert.Scenario: Scheduled Maintenance Window for Policy Refinement