9 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
tomas.kracmar 03aa72f999 Release v2.4.2: replace em-dashes with ASCII hyphens to fix encoding parse errors
UTF-8 em-dashes (U+2014) in Elysium.Common.ps1 string literals were
being misinterpreted by Windows PowerShell as containing quote
characters when the file was read without a UTF-8 BOM. This caused
cascading parse errors: unexpected tokens, missing closing braces,
and missing catch blocks.

All em-dashes in .ps1 files have been replaced with ASCII hyphens.
All versions bumped to unified v2.4.2.
2026-06-09 13:51:13 +02:00
tomas.kracmar 10cbf0285d Release v2.4.1: URI-escape DNs in DirectoryEntry LDAP URLs
Test-ReplicationPermissions and Test-DCClockSkew now escape
Distinguished Names via [System.Uri]::EscapeDataString before
constructing DirectoryEntry LDAP URLs. This prevents URL
mis-parsing when DNs contain /, #, or other reserved characters.

All versions bumped to unified v2.4.1.
2026-06-09 13:42:34 +02:00
tomas.kracmar fc91f0d6b0 Release v2.4.0: DC clock skew check, SDProp/Protected Users warnings, and DSInternals install fix
Added pre-flight diagnostics:
- Test-DCClockSkew: validates local/DC clock skew before DCSync to
  catch Kerberos auth failures early.
- Test-ReplicationPermissions now warns on adminCount=1 (SDProp
  protected) and Protected Users group membership (RID 525), both
  of which can silently block or revert replication rights.

Fixed DSInternals update flow:
- Replaced Update-Module with Install-Module -Force -AllowClobber
  to work around a PowerShellGet null PublishedDate bug.

All versions bumped to unified v2.4.0.
2026-06-09 13:32:21 +02:00
tomas.kracmar 6b2ae6c8b5 Release v2.3.0: add DSInternals version check and auto-update
Test-WeakADPasswords.ps1 now validates the installed DSInternals
version at startup:
- v6.2 (unsigned) warns that native DLLs are blocked and replication
  will fail; directs operator to Update-Module DSInternals.
- Below v7.0 prompts to auto-update via Update-Module -Force and
  exits cleanly so the new version is loaded on re-run.
- v7.0+ passes silently.

All versions bumped to unified v2.3.0.
2026-06-09 13:16:47 +02:00
tomas.kracmar 37d1a8d971 Release v2.2.5: resolve DSInternals module path in block error
The Zone.Identifier block detection now dynamically resolves the
actual DSInternals module installation path via Get-Module instead
of hardcoding a ProgramFiles path, so the Unblock-File command in
the error message is always correct.

All versions bumped to unified v2.2.5.
2026-06-09 13:10:36 +02:00
tomas.kracmar 0175864e72 Release v2.2.4: permission check InheritOnly fix and DSInternals block detection
Test-ReplicationPermissions:
- Skip InheritOnly ACEs since they do not apply to the domain root
  object itself, only to child objects.

Test-WeakADPasswords:
- Detect Windows Zone.Identifier blocks on DSInternals DLLs and
  emit a clear error with the exact Unblock-File remediation
  command instead of a vague warning.

All versions bumped to unified v2.2.4.
2026-06-09 13:07:46 +02:00
12 changed files with 310 additions and 79 deletions
+2 -2
View File
@@ -8,7 +8,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Bump-Version.ps1 ## ## File: Bump-Version.ps1 ##
## Version: 2.2.3 ## ## Version: 2.4.4 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
@@ -149,7 +149,7 @@ if (-not $SkipChangelog) {
--- ---
## [$NewVersion] $today ## [$NewVersion] - $today
### Changed ### Changed
- (describe your change here) - (describe your change here)
+71
View File
@@ -6,6 +6,77 @@ 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
- Replaced UTF-8 em-dashes (`\u2014`) in `Elysium.Common.ps1` and `Bump-Version.ps1` with ASCII hyphens. On Windows PowerShell without a UTF-8 BOM, the three-byte em-dash sequence was misinterpreted as containing a quote character, causing cascading parse errors (unexpected token, missing closing `)`/`}`/`catch`, etc.).
---
## [2.4.1] — 2026-06-09
### Fixed
- `Test-ReplicationPermissions` and `Test-DCClockSkew` now URI-escape Distinguished Names via `[System.Uri]::EscapeDataString` before embedding them in `DirectoryEntry` LDAP URLs. DNs containing `/`, `#`, or other reserved characters previously caused URL mis-parsing and constructor failures.
---
## [2.4.0] — 2026-06-09
### Added
- **DC clock skew pre-flight check** (`Test-DCClockSkew` in `Elysium.Common.ps1`): compares the local machine clock against the target DC's `RootDSE.currentTime` before attempting DCSync. Warns if skew exceeds 300s (Kerberos hard limit) or 60s (approaching limit), and provides the `w32tm /resync /force` remediation command.
- **SDProp protection warning** in `Test-ReplicationPermissions`: detects `adminCount=1` on the service account and warns that SDProp runs every 60 minutes and may silently revert replication rights or group memberships.
- **Protected Users group warning** in `Test-ReplicationPermissions`: detects membership in the Protected Users group (RID 525) and warns that it restricts Kerberos delegation and RC4 authentication required by DSInternals for DRS replication.
### Fixed
- DSInternals auto-update flow now uses `Install-Module -Force -AllowClobber` instead of `Update-Module` to avoid a PowerShellGet bug where null `PublishedDate` metadata causes "cannot convert null to type system.datetime".
---
## [2.3.0] — 2026-06-09
### Added
- `Test-WeakADPasswords.ps1` now checks the installed DSInternals version at startup:
- **v6.2** (unsigned) is flagged with a warning explaining that unsigned native DLLs are blocked and replication will fail. Remediation: `Update-Module DSInternals`.
- **Below v7.0** triggers an interactive prompt offering to run `Update-Module DSInternals -Force` automatically. If accepted, the script updates the module and exits cleanly so the operator can re-run with the new version loaded.
- v7.0+ is required because it fixes intermittent CRC errors mid-replication and `Test-PasswordQuality` result truncation bugs.
---
## [2.2.5] — 2026-06-09
### Fixed
- The DSInternals `Zone.Identifier` block error message (added in v2.2.4) now dynamically resolves the actual DSInternals module path via `Get-Module` instead of hardcoding `$env:ProgramFiles\WindowsPowerShell\DSInternals`. The `Unblock-File` command in the error now points to the correct installation directory.
---
## [2.2.4] — 2026-06-09
### Fixed
- `Test-ReplicationPermissions` (in `Elysium.Common.ps1`) now skips `InheritOnly` ACEs when evaluating replication rights. An ACE marked `InheritOnly` applies only to child objects, not the domain root itself, so it does not grant the required extended rights for DCSync on the domain object.
- `Import-CompatModule` (in `Test-WeakADPasswords.ps1`) now detects DSInternals being blocked by Windows `Zone.Identifier` (alternate data stream from internet download) and throws a clear, actionable error with the exact `Unblock-File` command to run. Previously this surfaced as an opaque non-FIPS warning.
---
## [2.2.3] — 2026-06-09 ## [2.2.3] — 2026-06-09
### Fixed ### Fixed
+118 -54
View File
@@ -1,4 +1,4 @@
$script:ElysiumVersion = '2.2.3' $script:ElysiumVersion = '2.4.4'
function Invoke-RestartWithExecutable { function Invoke-RestartWithExecutable {
param( param(
@@ -328,82 +328,146 @@ function Test-ReplicationPermissions {
[Parameter(Mandatory)][System.Management.Automation.PSCredential]$Credential [Parameter(Mandatory)][System.Management.Automation.PSCredential]$Credential
) )
$requiredRights = [ordered]@{ $allThreeRights = [ordered]@{
'Replicating Directory Changes' = [guid]'1131f6aa-9c07-11d1-f79f-00c04fc2dcd2' 'Replicating Directory Changes' = [guid]'1131f6aa-9c07-11d1-f79f-00c04fc2dcd2'
'Replicating Directory Changes All' = [guid]'1131f6ab-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' '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) $callerSids = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
try { try {
$samName = $Credential.UserName -replace '^.*\\', '' $samName = $Credential.UserName -replace '^.*\\', ''
$adUser = Get-ADUser -Identity $samName -Server $Server -Credential $Credential ` $adUser = Get-ADUser -Identity $samName -Server $Server -Credential $Credential `
-Properties SID, DistinguishedName -ErrorAction Stop -Properties SID, DistinguishedName, adminCount -ErrorAction Stop
[void]$callerSids.Add($adUser.SID.Value) [void]$callerSids.Add($adUser.SID.Value)
# tokenGroups is a constructed attribute containing all SIDs in the user's token, # tokenGroups is a constructed attribute containing all SIDs in the user's token,
# including nested group memberships more reliable than walking MemberOf recursively # including nested group memberships - more reliable than walking MemberOf recursively
$userDe = New-Object System.DirectoryServices.DirectoryEntry( $adUserWithTokenGroups = Get-ADUser -Identity $samName -Server $Server -Credential $Credential `
"LDAP://$Server/$($adUser.DistinguishedName)", -Properties tokenGroups -ErrorAction Stop
$Credential.UserName, foreach ($sidBytes in $adUserWithTokenGroups.tokenGroups) {
$Credential.GetNetworkCredential().Password $sid = New-Object System.Security.Principal.SecurityIdentifier(@([byte[]]$sidBytes), 0)
)
$userDe.RefreshCache(@('tokenGroups'))
foreach ($sidBytes in $userDe.Properties['tokenGroups']) {
$sid = New-Object System.Security.Principal.SecurityIdentifier($sidBytes, 0)
[void]$callerSids.Add($sid.Value) [void]$callerSids.Add($sid.Value)
} }
# adminCount=1 means SDProp is managing this account; it runs every 60 min and can
# silently revert replication rights or group memberships granted to the account
if ($adUser.adminCount -eq 1) {
Write-Warning ("Account '{0}' has adminCount=1 (SDProp-protected). It is or was a member of a privileged group. SDProp runs every 60 minutes and may silently revert replication rights or group memberships on this account." -f $Credential.UserName)
}
# Protected Users group (RID 525) blocks the Kerberos mechanisms DSInternals uses for DRS
$domainSidStr = $adUser.SID.Value.Substring(0, $adUser.SID.Value.LastIndexOf('-'))
$protectedUsersSid = "$domainSidStr-525"
if ($callerSids.Contains($protectedUsersSid)) {
Write-Warning ("Account '{0}' is a member of Protected Users. This group restricts Kerberos delegation and RC4 authentication that DSInternals requires for DRS replication - access will be denied regardless of assigned rights." -f $Credential.UserName)
}
} catch { } catch {
Write-Warning ("Could not resolve account SIDs for replication permission pre-check: {0}. Skipping." -f $_.Exception.Message) Write-Warning ("Could not resolve account SIDs for replication permission pre-check: {0}. Skipping." -f $_.Exception.Message)
return return
} }
$acl = $null $allMissingLines = @()
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
}
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"
}
}
}
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}' on domain NC and schema NC." -f $Credential.UserName)
}
function Test-DCClockSkew {
param(
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][System.Management.Automation.PSCredential]$Credential
)
try { try {
$de = New-Object System.DirectoryServices.DirectoryEntry( $rootDse = New-Object System.DirectoryServices.DirectoryEntry(
"LDAP://$Server/$DomainDN", "LDAP://$Server/RootDSE",
$Credential.UserName, $Credential.UserName,
$Credential.GetNetworkCredential().Password $Credential.GetNetworkCredential().Password
) )
$acl = $de.ObjectSecurity.GetAccessRules( $dcTimeStr = $rootDse.Properties['currentTime'][0]
$true, $true, [System.Security.Principal.SecurityIdentifier]) $dcTime = [datetime]::ParseExact(
$dcTimeStr, 'yyyyMMddHHmmss.0Z',
[System.Globalization.CultureInfo]::InvariantCulture,
[System.Globalization.DateTimeStyles]::AssumeUniversal).ToUniversalTime()
$skewSeconds = [Math]::Abs(([datetime]::UtcNow - $dcTime).TotalSeconds)
if ($skewSeconds -gt 300) {
Write-Warning ("Clock skew of {0:N0}s with '{1}' exceeds Kerberos limit of 300s - authentication will fail. Sync the clock: w32tm /resync /force" -f $skewSeconds, $Server)
} elseif ($skewSeconds -gt 60) {
Write-Warning ("Clock skew of {0:N0}s detected with '{1}'. Kerberos allows up to 300s - approaching the limit." -f $skewSeconds, $Server)
} else {
Write-Host ("[+] Clock skew with '{0}': {1:N0}s (OK)." -f $Server, $skewSeconds)
}
} catch { } catch {
Write-Warning ("Could not read domain object ACL for replication permission pre-check: {0}. Skipping." -f $_.Exception.Message) Write-Warning ("Could not check clock skew against '{0}': {1}" -f $Server, $_.Exception.Message)
return
} }
$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 }
$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 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)'
}
$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 - "))
}
Write-Host ("[+] Replication permissions verified for '{0}'." -f $Credential.UserName)
} }
+1 -1
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Elysium.ps1 ## ## File: Elysium.ps1 ##
## Version: 2.2.3 ## ## Version: 2.4.4 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
+1 -1
View File
@@ -8,7 +8,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: ElysiumSettings.txt ## ## File: ElysiumSettings.txt ##
## Version: 2.2.3 ## ## Version: 2.4.4 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
+1 -1
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Extract-NTHashes.ps1 ## ## File: Extract-NTHashes.ps1 ##
## Version: 2.2.3 ## ## Version: 2.4.4 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
+1 -1
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Prepare-KHDBStorage.ps1 ## ## File: Prepare-KHDBStorage.ps1 ##
## Version: 2.2.3 ## ## Version: 2.4.4 ##
## Support: support@cqre.net ## ## 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 ## Prerequisities
* **Windows Host:** A Windows machine with PowerShell and DSInternals suite installed. * **Windows Host:** A Windows machine with PowerShell and DSInternals suite installed.
* **Administrative Access:** Local admin privileges on the host for installation and updating. * **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). * **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 ## 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. 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 #### 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`
- `Replicating Directory Changes All` - `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 #### Common errors
- `The server has rejected the client credentials.` or `Credentials ... were rejected`: - `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. 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 ...`: - `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. 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`: - `Replication access was denied` (from `Get-ADReplAccount`):
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. 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`: - `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. This warning comes from DSInternals under FIPS-enforced environments. Hash-quality operations that rely on MD5 may be limited.
+88 -10
View File
@@ -8,7 +8,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Test-WeakADPasswords.ps1 ## ## File: Test-WeakADPasswords.ps1 ##
## Version: 2.2.3 ## ## Version: 2.4.4 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
@@ -352,7 +352,14 @@ function Import-CompatModule {
$nonFipsErrors = @($importErrors | Where-Object { $_.Exception.Message -notmatch 'Only FIPS certified cryptographic algorithms are enabled in \.NET' }) $nonFipsErrors = @($importErrors | Where-Object { $_.Exception.Message -notmatch 'Only FIPS certified cryptographic algorithms are enabled in \.NET' })
if ($nonFipsErrors.Count -gt 0) { if ($nonFipsErrors.Count -gt 0) {
Write-Warning ("DSInternals import reported non-fatal warning(s): {0}" -f $nonFipsErrors[0].Exception.Message) $nonFipsMsg = $nonFipsErrors[0].Exception.Message
if ($nonFipsMsg -match 'Zone\.Identifier|alternate data stream') {
$dsModule = Get-Module -Name DSInternals -ErrorAction SilentlyContinue
if (-not $dsModule) { $dsModule = Get-Module -ListAvailable -Name DSInternals -ErrorAction SilentlyContinue | Select-Object -First 1 }
$dsPath = if ($dsModule) { $dsModule.ModuleBase } else { '<DSInternals module path>' }
throw ("DSInternals native DLL is blocked by Windows (Zone.Identifier). Run the following on the target machine and retry:`n Get-ChildItem -Path '$dsPath' -Recurse | Unblock-File")
}
Write-Warning ("DSInternals import reported non-fatal warning(s): {0}" -f $nonFipsMsg)
} }
Write-Verbose ("Imported module '{0}' (Core={1}, Windows={2})" -f $Name, $runningInPSCore, $onWindows) Write-Verbose ("Imported module '{0}' (Core={1}, Windows={2})" -f $Name, $runningInPSCore, $onWindows)
@@ -385,6 +392,28 @@ try {
} }
} }
# Version check: v6.2 was unsigned (blocks native DLLs, causes replication failures);
# v7.0 fixes intermittent CRC errors mid-replication and Test-PasswordQuality result truncation.
$dsInternalsVersion = (Get-Module -Name DSInternals).Version
$minimumVersion = [version]'7.0'
$unsignedVersion = [version]'6.2'
if ($dsInternalsVersion -eq $unsignedVersion) {
Write-Warning ("DSInternals {0} is not digitally signed, which blocks its native DLLs and causes replication failures. Update to v7.0+: Install-Module DSInternals -Force -AllowClobber" -f $dsInternalsVersion)
} elseif ($dsInternalsVersion -lt $minimumVersion) {
$resp = Read-Host ("DSInternals {0} is installed; v7.0 fixes intermittent replication CRC errors and result truncation. Update now? [Y/N]" -f $dsInternalsVersion)
if ($resp -match '^(?i:y|yes)$') {
try {
# Install-Module -Force is used instead of Update-Module to avoid a PowerShellGet bug
# where null PublishedDate metadata causes "cannot convert null to type system.datetime"
Install-Module -Name DSInternals -Force -AllowClobber -ErrorAction Stop
Write-Host '[+] DSInternals updated. Please re-run the script to load the new version.'
exit 0
} catch {
Write-Warning ("DSInternals update failed: {0}" -f $_.Exception.Message)
}
}
}
# Resolve KHDB path with fallbacks # Resolve KHDB path with fallbacks
$installationPath = $ElysiumSettings["InstallationPath"] $installationPath = $ElysiumSettings["InstallationPath"]
if ([string]::IsNullOrWhiteSpace($installationPath)) { $installationPath = $scriptRoot } if ([string]::IsNullOrWhiteSpace($installationPath)) { $installationPath = $scriptRoot }
@@ -575,9 +604,10 @@ function Test-WeakADPasswords {
Write-Verbose ("Using credential supplied by caller: {0}" -f $credential.UserName) Write-Verbose ("Using credential supplied by caller: {0}" -f $credential.UserName)
} }
# Verify the account has the three replication extended rights before attempting DCSync # Pre-flight checks before attempting DCSync
try { try {
$domainInfo = Get-ADDomain -Server $selectedDomain["DC"] -Credential $credential -ErrorAction Stop $domainInfo = Get-ADDomain -Server $selectedDomain["DC"] -Credential $credential -ErrorAction Stop
Test-DCClockSkew -Server $selectedDomain["DC"] -Credential $credential
Test-ReplicationPermissions -DomainDN $domainInfo.DistinguishedName ` Test-ReplicationPermissions -DomainDN $domainInfo.DistinguishedName `
-Server $selectedDomain["DC"] -Credential $credential -Server $selectedDomain["DC"] -Credential $credential
} catch { } catch {
@@ -601,16 +631,64 @@ function Test-WeakADPasswords {
$testResults = $accounts | Test-PasswordQuality -WeakPasswordHashesSortedFile $resolvedHashFile.Path $testResults = $accounts | Test-PasswordQuality -WeakPasswordHashesSortedFile $resolvedHashFile.Path
Write-Verbose "Password quality test completed." Write-Verbose "Password quality test completed."
} catch { } catch {
$message = $_.Exception.Message $ex = $_.Exception
if ($message -match 'Access is denied') { $diagLines = [System.Collections.Generic.List[string]]::new()
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) $diagLines.Add('========================================')
return $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 ($currentEx.StackTrace) {
$diagLines.Add(" StackTrace :`n$($currentEx.StackTrace -replace '^', ' ')")
}
$diagLines.Add('')
$currentEx = $currentEx.InnerException
$depth++
} }
if ($message -match 'rejected the client credentials|unknown user name|bad password|logon failure') { $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"]) 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)
} }
Write-Error ("An error occurred while testing passwords: {0}" -f $message)
return return
} finally { } finally {
if ($resolvedHashFile -and $resolvedHashFile.IsTemporary -and (Test-Path -LiteralPath $resolvedHashFile.Path)) { if ($resolvedHashFile -and $resolvedHashFile.IsTemporary -and (Test-Path -LiteralPath $resolvedHashFile.Path)) {
+1 -1
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Uninstall.ps1 ## ## File: Uninstall.ps1 ##
## Version: 2.2.3 ## ## Version: 2.4.4 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
+1 -1
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Update-KHDB.ps1 ## ## File: Update-KHDB.ps1 ##
## Version: 2.2.3 ## ## Version: 2.4.4 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
+1 -1
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Update-LithnetStore.ps1 ## ## File: Update-LithnetStore.ps1 ##
## Version: 2.2.3 ## ## Version: 2.4.4 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################