3 Commits

Author SHA1 Message Date
tomas.kracmar 1d98b908c6 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.
2026-06-15 08:38:04 +02:00
tomas.kracmar 906bb52638 fix(Test-WeakADPasswords): add comprehensive DCSync diagnostic dump
When Get-ADReplAccount or Test-PasswordQuality throws, the catch
block now dumps the full exception chain (type, message, HResult,
source, target site, stack trace, inner exceptions) along with
runtime context (Elysium version, PS version, DSInternals version,
DC, domain, account). Output goes to console and a timestamped
 diagnostic file under Reports/ for offline analysis.
2026-06-09 16:23:38 +02:00
tomas.kracmar af945f529e Release v2.4.3: fix tokenGroups retrieval and DirectoryEntry LDAP paths
Test-ReplicationPermissions:
- Replaced DirectoryEntry.RefreshCache tokenGroups retrieval with
  Get-ADUser -Properties tokenGroups. DirectoryEntry does not
  understand URI percent-encoding, so the v2.4.1 EscapeDataString
  fix caused 'invalid dn syntax' errors.
- Removed EscapeDataString from the ACL DirectoryEntry path as
  well; DirectoryEntry expects raw LDAP ADSI path syntax.

All versions bumped to unified v2.4.3.
2026-06-09 14:14:45 +02:00
12 changed files with 180 additions and 74 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
##################################################
## Project: Elysium ##
## File: Bump-Version.ps1 ##
## Version: 2.4.2 ##
## Version: 2.4.4 ##
## Support: support@cqre.net ##
##################################################
+20
View File
@@ -6,6 +6,26 @@ 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
- Replaced the `DirectoryEntry` + `RefreshCache` tokenGroups retrieval in `Test-ReplicationPermissions` with `Get-ADUser -Properties tokenGroups`. The previous `DirectoryEntry` approach was broken by the v2.4.1 URI-escaping "fix" (`EscapeDataString` produces percent-encoded paths that ADSI `DirectoryEntry` cannot parse, causing "invalid dn syntax" errors).
- Removed `EscapeDataString` from the ACL-reading `DirectoryEntry` path in `Test-ReplicationPermissions` as well, since `DirectoryEntry` expects raw LDAP path syntax, not URI encoding.
---
## [2.4.2] — 2026-06-09
### Fixed
+44 -24
View File
@@ -1,4 +1,4 @@
$script:ElysiumVersion = '2.4.2'
$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 '^.*\\', ''
@@ -343,14 +355,10 @@ function Test-ReplicationPermissions {
# tokenGroups is a constructed attribute containing all SIDs in the user's token,
# including nested group memberships - more reliable than walking MemberOf recursively
$userDe = New-Object System.DirectoryServices.DirectoryEntry(
"LDAP://$Server/$([System.Uri]::EscapeDataString($adUser.DistinguishedName))",
$Credential.UserName,
$Credential.GetNetworkCredential().Password
)
$userDe.RefreshCache(@('tokenGroups'))
foreach ($sidBytes in $userDe.Properties['tokenGroups']) {
$sid = New-Object System.Security.Principal.SecurityIdentifier($sidBytes, 0)
$adUserWithTokenGroups = Get-ADUser -Identity $samName -Server $Server -Credential $Credential `
-Properties tokenGroups -ErrorAction Stop
foreach ($sidBytes in $adUserWithTokenGroups.tokenGroups) {
$sid = New-Object System.Security.Principal.SecurityIdentifier(@([byte[]]$sidBytes), 0)
[void]$callerSids.Add($sid.Value)
}
@@ -371,28 +379,33 @@ function Test-ReplicationPermissions {
return
}
$allMissingLines = @()
foreach ($ncEntry in $ncsToCheck.GetEnumerator()) {
$ncDN = $ncEntry.Key
$rightsToCheck = $ncEntry.Value
$acl = $null
try {
$de = New-Object System.DirectoryServices.DirectoryEntry(
"LDAP://$Server/$([System.Uri]::EscapeDataString($DomainDN))",
"LDAP://$Server/$ncDN",
$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
Write-Warning ("Could not read ACL on '$ncDN' for replication permission pre-check: {0}. Skipping." -f $_.Exception.Message)
continue
}
$missing = @()
foreach ($rightName in $requiredRights.Keys) {
$guid = $requiredRights[$rightName]
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 domain root itself is not covered
# 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)
@@ -407,20 +420,27 @@ function Test-ReplicationPermissions {
}
if (-not $granted) {
$hint = if ($aceExistsForGuid) {
' (ACE exists on the domain object but is not assigned to this account or any of its groups)'
' (ACE exists but not assigned to this account or any of its groups)'
} else {
' (no ACE found for this right on the domain object at all)'
' (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
View File
@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Elysium.ps1 ##
## Version: 2.4.2 ##
## Version: 2.4.4 ##
## Support: support@cqre.net ##
##################################################
+1 -1
View File
@@ -8,7 +8,7 @@
##################################################
## Project: Elysium ##
## File: ElysiumSettings.txt ##
## Version: 2.4.2 ##
## Version: 2.4.4 ##
## Support: support@cqre.net ##
##################################################
+1 -1
View File
@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Extract-NTHashes.ps1 ##
## Version: 2.4.2 ##
## Version: 2.4.4 ##
## Support: support@cqre.net ##
##################################################
+1 -1
View File
@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Prepare-KHDBStorage.ps1 ##
## Version: 2.4.2 ##
## Version: 2.4.4 ##
## Support: support@cqre.net ##
##################################################
+24 -6
View File
@@ -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.
+56 -8
View File
@@ -8,7 +8,7 @@
##################################################
## Project: Elysium ##
## File: Test-WeakADPasswords.ps1 ##
## Version: 2.4.2 ##
## Version: 2.4.4 ##
## Support: support@cqre.net ##
##################################################
@@ -631,16 +631,64 @@ function Test-WeakADPasswords {
$testResults = $accounts | Test-PasswordQuality -WeakPasswordHashesSortedFile $resolvedHashFile.Path
Write-Verbose "Password quality test completed."
} catch {
$message = $_.Exception.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)
return
$ex = $_.Exception
$diagLines = [System.Collections.Generic.List[string]]::new()
$diagLines.Add('========================================')
$diagLines.Add('ELYSLUM DCSYNC DIAGNOSTIC DUMP')
$diagLines.Add('========================================')
$diagLines.Add("Timestamp : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
$diagLines.Add("Script Ver : $ElysiumVersion")
$diagLines.Add("PS Version : $($PSVersionTable.PSVersion)")
$diagLines.Add("PS Edition : $($PSVersionTable.PSEdition)")
$diagLines.Add("DSInternals : $((Get-Module -Name DSInternals).Version)")
$diagLines.Add("DC : $($selectedDomain['DC'])")
$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
$currentEx = $ex
while ($null -ne $currentEx) {
$diagLines.Add("Exception $depth : $($currentEx.GetType().FullName)")
$diagLines.Add(" Message : $($currentEx.Message)")
$diagLines.Add(" HResult : 0x$($currentEx.HResult.ToString('X8'))")
$diagLines.Add(" Source : $($currentEx.Source)")
if ($currentEx.TargetSite) {
$diagLines.Add(" TargetSite : $($currentEx.TargetSite)")
}
if ($message -match 'rejected the client credentials|unknown user name|bad password|logon failure') {
if ($currentEx.StackTrace) {
$diagLines.Add(" StackTrace :`n$($currentEx.StackTrace -replace '^', ' ')")
}
$diagLines.Add('')
$currentEx = $currentEx.InnerException
$depth++
}
$diagLines.Add('--- END DIAGNOSTIC DUMP ---')
$diagText = $diagLines -join "`r`n"
Write-Host $diagText -ForegroundColor Red
$diagPath = Join-Path -Path $reportPathBase -ChildPath "dcsync-diag-$timestamp.txt"
try {
New-Item -ItemType Directory -Path $reportPathBase -Force | Out-Null
[System.IO.File]::WriteAllText($diagPath, $diagText, [System.Text.Encoding]::UTF8)
Write-Host ("Diagnostic dump written to: {0}" -f $diagPath)
} catch {
Write-Warning ("Could not write diagnostic dump to disk: {0}" -f $_.Exception.Message)
}
# Still emit the concise error for the operator
$message = $ex.Message
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"])
return
}
} else {
Write-Error ("An error occurred while testing passwords: {0}" -f $message)
}
return
} finally {
if ($resolvedHashFile -and $resolvedHashFile.IsTemporary -and (Test-Path -LiteralPath $resolvedHashFile.Path)) {
+1 -1
View File
@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Uninstall.ps1 ##
## Version: 2.4.2 ##
## Version: 2.4.4 ##
## Support: support@cqre.net ##
##################################################
+1 -1
View File
@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Update-KHDB.ps1 ##
## Version: 2.4.2 ##
## Version: 2.4.4 ##
## Support: support@cqre.net ##
##################################################
+1 -1
View File
@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Update-LithnetStore.ps1 ##
## Version: 2.4.2 ##
## Version: 2.4.4 ##
## Support: support@cqre.net ##
##################################################