Release v2.4.4: check schema NC replication rights for DSInternals 7.0
DSInternals 7.0 fetches the AD schema via DRS (GetNCChanges) before replicating accounts, so the schema NC has its own ACL requirement. - Test-ReplicationPermissions now validates rights on both the domain NC and the configuration NC (schema NC inherits from it). - Updated README with dsacls delegation examples and dual-NC least-privilege requirements. - Improved 'Replication access was denied' error message to name both NCs and explain the DSInternals 7.0 change. - Diagnostic dump now includes SchemaDN. All versions bumped to unified v2.4.4.
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Bump-Version.ps1 ##
|
||||
## Version: 2.4.3 ##
|
||||
## Version: 2.4.4 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
|
||||
@@ -6,6 +6,18 @@ Starting with **v2.2.0**, Elysium uses a **unified project version**. All script
|
||||
|
||||
---
|
||||
|
||||
## [2.4.4] — 2026-06-15
|
||||
|
||||
### Fixed
|
||||
- `Test-ReplicationPermissions` now checks **both** the domain NC (`DC=…`) and the schema NC (`CN=Schema,CN=Configuration,DC=…`) for the required DCSync extended rights. DSInternals 7.0 changed schema fetching from LDAP to DRS (`GetNCChanges`), so the schema NC now requires its own ACL entry. Previously the pre-flight check passed (domain NC rights present) while `Get-ADReplAccount` immediately failed at `FetchSchema()` with "Replication access was denied".
|
||||
- The `Replication access was denied` catch block in `Test-WeakADPasswords` now emits a structured, actionable error message that names the exact DNs to target and explains the DSInternals 7.0 schema NC change, replacing the previous generic "ensure this account has replication rights on the domain" message.
|
||||
- Diagnostic dump (`dcsync-diag-*.txt`) now includes a `SchemaDN` field so the schema NC path is immediately visible when triaging a dump.
|
||||
|
||||
### Changed
|
||||
- Least-privilege requirement updated: the DCSync service account now needs the three replication extended rights on **both** the domain NC *and* `CN=Configuration,DC=…` (which covers the schema NC via inheritance). See *Least privileges* in the README for delegation steps.
|
||||
|
||||
---
|
||||
|
||||
## [2.4.3] — 2026-06-09
|
||||
|
||||
### Fixed
|
||||
|
||||
+68
-44
@@ -1,4 +1,4 @@
|
||||
$script:ElysiumVersion = '2.4.3'
|
||||
$script:ElysiumVersion = '2.4.4'
|
||||
|
||||
function Invoke-RestartWithExecutable {
|
||||
param(
|
||||
@@ -328,12 +328,24 @@ function Test-ReplicationPermissions {
|
||||
[Parameter(Mandatory)][System.Management.Automation.PSCredential]$Credential
|
||||
)
|
||||
|
||||
$requiredRights = [ordered]@{
|
||||
$allThreeRights = [ordered]@{
|
||||
'Replicating Directory Changes' = [guid]'1131f6aa-9c07-11d1-f79f-00c04fc2dcd2'
|
||||
'Replicating Directory Changes All' = [guid]'1131f6ab-9c07-11d1-f79f-00c04fc2dcd2'
|
||||
'Replicating Directory Changes In Filtered Set' = [guid]'89e95b76-444d-4c62-991a-0facbeda640c'
|
||||
}
|
||||
|
||||
# DSInternals 7.0 fetches the AD schema via DRS (GetNCChanges) before replicating accounts.
|
||||
# The schema NC has its own ACL - rights on the domain NC do not cover it.
|
||||
# Older DSInternals read schema via LDAP (no special rights needed); v7.0 switched to DRS.
|
||||
$schemaDN = "CN=Schema,CN=Configuration,$DomainDN"
|
||||
|
||||
$ncsToCheck = [ordered]@{
|
||||
$DomainDN = $allThreeRights
|
||||
$schemaDN = [ordered]@{
|
||||
'Replicating Directory Changes' = [guid]'1131f6aa-9c07-11d1-f79f-00c04fc2dcd2'
|
||||
}
|
||||
}
|
||||
|
||||
$callerSids = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||
try {
|
||||
$samName = $Credential.UserName -replace '^.*\\', ''
|
||||
@@ -367,56 +379,68 @@ function Test-ReplicationPermissions {
|
||||
return
|
||||
}
|
||||
|
||||
$acl = $null
|
||||
try {
|
||||
$de = New-Object System.DirectoryServices.DirectoryEntry(
|
||||
"LDAP://$Server/$DomainDN",
|
||||
$Credential.UserName,
|
||||
$Credential.GetNetworkCredential().Password
|
||||
)
|
||||
$acl = $de.ObjectSecurity.GetAccessRules(
|
||||
$true, $true, [System.Security.Principal.SecurityIdentifier])
|
||||
} catch {
|
||||
Write-Warning ("Could not read domain object ACL for replication permission pre-check: {0}. Skipping." -f $_.Exception.Message)
|
||||
return
|
||||
}
|
||||
$allMissingLines = @()
|
||||
|
||||
$missing = @()
|
||||
foreach ($rightName in $requiredRights.Keys) {
|
||||
$guid = $requiredRights[$rightName]
|
||||
$granted = $false
|
||||
$aceExistsForGuid = $false
|
||||
foreach ($ace in $acl) {
|
||||
if ($ace.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { continue }
|
||||
# InheritOnly ACEs apply to child objects only - the domain root itself is not covered
|
||||
if ([bool]($ace.PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::InheritOnly)) { continue }
|
||||
$rights = $ace.ActiveDirectoryRights
|
||||
$hasExtended = [bool]($rights -band [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight)
|
||||
$hasGenericAll = [bool]($rights -band [System.DirectoryServices.ActiveDirectoryRights]::GenericAll)
|
||||
# Match: exact GUID, OR ExtendedRight with empty ObjectType (all extended rights), OR GenericAll
|
||||
$isMatch = $hasGenericAll `
|
||||
-or ($hasExtended -and $ace.ObjectType -eq [guid]::Empty) `
|
||||
-or ($hasExtended -and $ace.ObjectType -eq $guid)
|
||||
if (-not $isMatch) { continue }
|
||||
if ($ace.ObjectType -eq $guid) { $aceExistsForGuid = $true }
|
||||
if ($callerSids.Contains($ace.IdentityReference.Value)) { $granted = $true; break }
|
||||
foreach ($ncEntry in $ncsToCheck.GetEnumerator()) {
|
||||
$ncDN = $ncEntry.Key
|
||||
$rightsToCheck = $ncEntry.Value
|
||||
|
||||
$acl = $null
|
||||
try {
|
||||
$de = New-Object System.DirectoryServices.DirectoryEntry(
|
||||
"LDAP://$Server/$ncDN",
|
||||
$Credential.UserName,
|
||||
$Credential.GetNetworkCredential().Password
|
||||
)
|
||||
$acl = $de.ObjectSecurity.GetAccessRules(
|
||||
$true, $true, [System.Security.Principal.SecurityIdentifier])
|
||||
} catch {
|
||||
Write-Warning ("Could not read ACL on '$ncDN' for replication permission pre-check: {0}. Skipping." -f $_.Exception.Message)
|
||||
continue
|
||||
}
|
||||
if (-not $granted) {
|
||||
$hint = if ($aceExistsForGuid) {
|
||||
' (ACE exists on the domain object but is not assigned to this account or any of its groups)'
|
||||
} else {
|
||||
' (no ACE found for this right on the domain object at all)'
|
||||
|
||||
foreach ($rightName in $rightsToCheck.Keys) {
|
||||
$guid = $rightsToCheck[$rightName]
|
||||
$granted = $false
|
||||
$aceExistsForGuid = $false
|
||||
foreach ($ace in $acl) {
|
||||
if ($ace.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { continue }
|
||||
# InheritOnly ACEs apply to child objects only - the NC root itself is not covered
|
||||
if ([bool]($ace.PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::InheritOnly)) { continue }
|
||||
$rights = $ace.ActiveDirectoryRights
|
||||
$hasExtended = [bool]($rights -band [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight)
|
||||
$hasGenericAll = [bool]($rights -band [System.DirectoryServices.ActiveDirectoryRights]::GenericAll)
|
||||
# Match: exact GUID, OR ExtendedRight with empty ObjectType (all extended rights), OR GenericAll
|
||||
$isMatch = $hasGenericAll `
|
||||
-or ($hasExtended -and $ace.ObjectType -eq [guid]::Empty) `
|
||||
-or ($hasExtended -and $ace.ObjectType -eq $guid)
|
||||
if (-not $isMatch) { continue }
|
||||
if ($ace.ObjectType -eq $guid) { $aceExistsForGuid = $true }
|
||||
if ($callerSids.Contains($ace.IdentityReference.Value)) { $granted = $true; break }
|
||||
}
|
||||
if (-not $granted) {
|
||||
$hint = if ($aceExistsForGuid) {
|
||||
' (ACE exists but not assigned to this account or any of its groups)'
|
||||
} else {
|
||||
' (no ACE found for this right on this object)'
|
||||
}
|
||||
$allMissingLines += "[on $ncDN] $rightName$hint"
|
||||
}
|
||||
$missing += $rightName + $hint
|
||||
}
|
||||
}
|
||||
|
||||
if ($missing.Count -gt 0) {
|
||||
throw ("Account '{0}' failed replication permission check on '{1}':`n - {2}`n`nGrant these extended rights on the domain object to allow DCSync-based hash retrieval." -f `
|
||||
$Credential.UserName, $DomainDN, ($missing -join "`n - "))
|
||||
if ($allMissingLines.Count -gt 0) {
|
||||
$schemaNote = ''
|
||||
if ($allMissingLines | Where-Object { $_ -match [regex]::Escape($schemaDN) }) {
|
||||
$schemaNote = ("`n`nNOTE: DSInternals 7.0 fetches the AD schema via DRS before replicating accounts." +
|
||||
" Grant 'Replicating Directory Changes' on CN=Configuration,$DomainDN" +
|
||||
" (covers Schema NC via inheritance) in addition to the domain NC rights.")
|
||||
}
|
||||
throw ("Account '{0}' failed replication permission check:`n - {1}{2}" -f `
|
||||
$Credential.UserName, ($allMissingLines -join "`n - "), $schemaNote)
|
||||
}
|
||||
|
||||
Write-Host ("[+] Replication permissions verified for '{0}'." -f $Credential.UserName)
|
||||
Write-Host ("[+] Replication permissions verified for '{0}' on domain NC and schema NC." -f $Credential.UserName)
|
||||
}
|
||||
|
||||
function Test-DCClockSkew {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Elysium.ps1 ##
|
||||
## Version: 2.4.3 ##
|
||||
## Version: 2.4.4 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: ElysiumSettings.txt ##
|
||||
## Version: 2.4.3 ##
|
||||
## Version: 2.4.4 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Extract-NTHashes.ps1 ##
|
||||
## Version: 2.4.3 ##
|
||||
## Version: 2.4.4 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Prepare-KHDBStorage.ps1 ##
|
||||
## Version: 2.4.3 ##
|
||||
## Version: 2.4.4 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Sensitive operations are confined only to the dedicated host. In the third step,
|
||||
## Prerequisities
|
||||
* **Windows Host:** A Windows machine with PowerShell and DSInternals suite installed.
|
||||
* **Administrative Access:** Local admin privileges on the host for installation and updating.
|
||||
* **Domain Credentials:** For weak-password testing (option 2), an account with the three replication rights (`Replicating Directory Changes`, `Replicating Directory Changes All`, `Replicating Directory Changes In Filtered Set`) on the domain naming context; Domain Admin also works but is not required. Keep this account disabled and enable only when running tests.
|
||||
* **Domain Credentials:** For weak-password testing (option 2), an account with the three replication rights (`Replicating Directory Changes`, `Replicating Directory Changes All`, `Replicating Directory Changes In Filtered Set`) on **both** the domain naming context **and** `CN=Configuration,DC=…` (which covers the schema NC via inheritance). Domain Admin also works but is not required. See *Least privileges* below for exact delegation steps. Keep this account disabled and enable only when running tests.
|
||||
* **Network Requirements:** A stable connection to the domain controller in each tested AD domain and internet access (specific hostnames/IP addresses will be provided).
|
||||
|
||||
## Versioning and Releases
|
||||
@@ -58,20 +58,38 @@ The tool connects to the selected Domain Controller and compares accounts agains
|
||||
The KHDB file is consumed by DSInternals as a sorted hash list with one NT hash per line (for example `HASH`). Do not include `:count` suffixes in `khdb.txt`; the packaging and update scripts normalize legacy `HASH:count` input to the hash-only format automatically.
|
||||
|
||||
#### Least privileges for password-quality testing
|
||||
The DSInternals cmdlets (`Get-ADReplAccount`/`Test-PasswordQuality`) pull replicated password data, which requires DCSync-style rights. The account that runs option 2 does not have to be a Domain Admin if it has these permissions on the domain naming context:
|
||||
The DSInternals cmdlets (`Get-ADReplAccount`/`Test-PasswordQuality`) pull replicated password data using the MS-DRSR (DCSync) protocol. The account does not need to be a Domain Admin; delegate these three extended rights on **two** AD objects:
|
||||
|
||||
| Object | Why |
|
||||
|--------|-----|
|
||||
| Domain NC root — e.g. `DC=admin,DC=lan` | Required to replicate account password hashes |
|
||||
| Configuration NC root — e.g. `CN=Configuration,DC=admin,DC=lan` | Required by DSInternals 7.0+ to fetch the AD schema via DRS before replication; covers the schema NC (`CN=Schema,CN=Configuration,DC=…`) via inheritance |
|
||||
|
||||
Rights to delegate on both objects:
|
||||
- `Replicating Directory Changes`
|
||||
- `Replicating Directory Changes All`
|
||||
- `Replicating Directory Changes In Filtered Set` (needed on 2008 R2+ to read password hashes)
|
||||
- `Replicating Directory Changes In Filtered Set` (required on 2008 R2+ to read password hashes)
|
||||
|
||||
To delegate, enable Advanced Features in ADUC, right-click the domain, choose *Delegate Control…*, pick the service account, select *Create a custom task*, apply to *This object and all descendant objects*, and tick the three replication permissions above. Keep this account disabled and only activate it for scheduled tests.
|
||||
**To delegate in ADUC:** enable *Advanced Features*, right-click each object above, choose *Properties* > *Security* > *Advanced* > *Add*, select the service account, set *Applies to: This object only*, and tick the three rights. Repeat for both objects.
|
||||
|
||||
**To delegate via `dsacls`** (replace `DC=admin,DC=lan` and `DOMAIN\svc` as appropriate):
|
||||
```powershell
|
||||
foreach ($nc in @('DC=admin,DC=lan', 'CN=Configuration,DC=admin,DC=lan')) {
|
||||
dsacls $nc /I:T /G "DOMAIN\svc:CA;Replicating Directory Changes"
|
||||
dsacls $nc /I:T /G "DOMAIN\svc:CA;Replicating Directory Changes All"
|
||||
dsacls $nc /I:T /G "DOMAIN\svc:CA;Replicating Directory Changes In Filtered Set"
|
||||
}
|
||||
```
|
||||
|
||||
Keep the service account disabled and only activate it for scheduled tests.
|
||||
|
||||
#### Common errors
|
||||
- `The server has rejected the client credentials.` or `Credentials ... were rejected`:
|
||||
The supplied username/password is invalid for the selected domain controller, or the session is not running in the expected domain context. Re-run and provide valid domain credentials.
|
||||
- `Account '<user>' is missing the following replication permissions ...`:
|
||||
Starting with v2.2.0, the script pre-validates the three required replication extended rights against the domain object ACL before attempting DCSync. If this error appears, delegate the listed rights (see *Least privileges* above) and retry.
|
||||
- `Get-ADReplAccount: Access is denied`:
|
||||
Credentials are valid, but the account does not have the three replication permissions listed above. This error should now be rare because the pre-check catches most permission issues early; if it still occurs, verify the account is not restricted by an additional conditional access or Group Policy setting.
|
||||
- `Replication access was denied` (from `Get-ADReplAccount`):
|
||||
DSInternals 7.0+ fetches the AD schema via DRS (`GetNCChanges`) as its first step, before replicating any accounts. This fails if the service account lacks `Replicating Directory Changes` on the **schema NC** (`CN=Schema,CN=Configuration,DC=…`). Grant the three rights on `CN=Configuration,DC=…` (covers schema NC via inheritance) in addition to the domain NC — see *Least privileges* above. The pre-flight permission check in v2.4.4+ catches this mismatch before attempting replication.
|
||||
- `Only FIPS certified cryptographic algorithms are enabled in .NET`:
|
||||
This warning comes from DSInternals under FIPS-enforced environments. Hash-quality operations that rely on MD5 may be limited.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Test-WeakADPasswords.ps1 ##
|
||||
## Version: 2.4.3 ##
|
||||
## Version: 2.4.4 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
@@ -645,6 +645,7 @@ function Test-WeakADPasswords {
|
||||
$diagLines.Add("Domain : $($selectedDomain.Name)")
|
||||
$diagLines.Add("Account : $($credential.UserName)")
|
||||
$diagLines.Add("DomainDN : $($domainInfo.DistinguishedName)")
|
||||
$diagLines.Add("SchemaDN : CN=Schema,CN=Configuration,$($domainInfo.DistinguishedName)")
|
||||
$diagLines.Add('')
|
||||
$diagLines.Add('--- EXCEPTION CHAIN ---')
|
||||
$depth = 0
|
||||
@@ -680,8 +681,9 @@ function Test-WeakADPasswords {
|
||||
|
||||
# Still emit the concise error for the operator
|
||||
$message = $ex.Message
|
||||
if ($message -match 'Access is denied') {
|
||||
Write-Error ("Access denied while reading replication data from '{0}' using '{1}'. Ensure this account has Replicating Directory Changes, Replicating Directory Changes All, and Replicating Directory Changes In Filtered Set on the domain." -f $selectedDomain["DC"], $credential.UserName)
|
||||
if ($message -match 'Replication access was denied|Access is denied') {
|
||||
Write-Error ("Replication access denied from '{0}' using '{1}'.`n`nDSInternals 7.0 fetches the AD schema via DRS before replicating accounts. The schema NC has its own ACL.`nGrant the 3 DCSync extended rights on BOTH:`n 1. {2} (domain NC - for accounts)`n 2. CN=Configuration,{2} (config NC - covers schema NC via inheritance)`n`nIn ADUC: right-click each object > Properties > Security > Advanced > Add the extended rights for '{1}'." -f `
|
||||
$selectedDomain["DC"], $credential.UserName, $domainInfo.DistinguishedName)
|
||||
} elseif ($message -match 'rejected the client credentials|unknown user name|bad password|logon failure') {
|
||||
Write-Error ("Credentials for '{0}' were rejected by '{1}'. Re-run and provide valid domain credentials." -f $credential.UserName, $selectedDomain["DC"])
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Uninstall.ps1 ##
|
||||
## Version: 2.4.3 ##
|
||||
## Version: 2.4.4 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Update-KHDB.ps1 ##
|
||||
## Version: 2.4.3 ##
|
||||
## Version: 2.4.4 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Update-LithnetStore.ps1 ##
|
||||
## Version: 2.4.3 ##
|
||||
## Version: 2.4.4 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
|
||||
Reference in New Issue
Block a user