Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d98b908c6 | |||
| 906bb52638 | |||
| af945f529e | |||
| 03aa72f999 | |||
| 10cbf0285d | |||
| fc91f0d6b0 | |||
| 6b2ae6c8b5 |
+2
-2
@@ -8,7 +8,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Bump-Version.ps1 ##
|
## File: Bump-Version.ps1 ##
|
||||||
## Version: 2.2.5 ##
|
## 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)
|
||||||
|
|||||||
@@ -6,6 +6,62 @@ 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
|
## [2.2.5] — 2026-06-09
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+118
-56
@@ -1,4 +1,4 @@
|
|||||||
$script:ElysiumVersion = '2.2.5'
|
$script:ElysiumVersion = '2.4.4'
|
||||||
|
|
||||||
function Invoke-RestartWithExecutable {
|
function Invoke-RestartWithExecutable {
|
||||||
param(
|
param(
|
||||||
@@ -328,84 +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 }
|
|
||||||
# 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 }
|
|
||||||
}
|
|
||||||
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
@@ -7,7 +7,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Elysium.ps1 ##
|
## File: Elysium.ps1 ##
|
||||||
## Version: 2.2.5 ##
|
## Version: 2.4.4 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: ElysiumSettings.txt ##
|
## File: ElysiumSettings.txt ##
|
||||||
## Version: 2.2.5 ##
|
## Version: 2.4.4 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Extract-NTHashes.ps1 ##
|
## File: Extract-NTHashes.ps1 ##
|
||||||
## Version: 2.2.5 ##
|
## Version: 2.4.4 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Prepare-KHDBStorage.ps1 ##
|
## File: Prepare-KHDBStorage.ps1 ##
|
||||||
## Version: 2.2.5 ##
|
## Version: 2.4.4 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Test-WeakADPasswords.ps1 ##
|
## File: Test-WeakADPasswords.ps1 ##
|
||||||
## Version: 2.2.5 ##
|
## Version: 2.4.4 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
@@ -392,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 }
|
||||||
@@ -582,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 {
|
||||||
@@ -608,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
@@ -7,7 +7,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Uninstall.ps1 ##
|
## File: Uninstall.ps1 ##
|
||||||
## Version: 2.2.5 ##
|
## Version: 2.4.4 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Update-KHDB.ps1 ##
|
## File: Update-KHDB.ps1 ##
|
||||||
## Version: 2.2.5 ##
|
## Version: 2.4.4 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Update-LithnetStore.ps1 ##
|
## File: Update-LithnetStore.ps1 ##
|
||||||
## Version: 2.2.5 ##
|
## Version: 2.4.4 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user