4 Commits

Author SHA1 Message Date
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
11 changed files with 118 additions and 17 deletions
+2 -2
View File
@@ -8,7 +8,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Bump-Version.ps1 ## ## File: Bump-Version.ps1 ##
## Version: 2.2.5 ## ## Version: 2.4.2 ##
## 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)
+36
View File
@@ -6,6 +6,42 @@ Starting with **v2.2.0**, Elysium uses a **unified project version**. All script
--- ---
## [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
+48 -6
View File
@@ -1,4 +1,4 @@
$script:ElysiumVersion = '2.2.5' $script:ElysiumVersion = '2.4.2'
function Invoke-RestartWithExecutable { function Invoke-RestartWithExecutable {
param( param(
@@ -338,13 +338,13 @@ function Test-ReplicationPermissions {
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( $userDe = New-Object System.DirectoryServices.DirectoryEntry(
"LDAP://$Server/$($adUser.DistinguishedName)", "LDAP://$Server/$([System.Uri]::EscapeDataString($adUser.DistinguishedName))",
$Credential.UserName, $Credential.UserName,
$Credential.GetNetworkCredential().Password $Credential.GetNetworkCredential().Password
) )
@@ -353,6 +353,19 @@ function Test-ReplicationPermissions {
$sid = New-Object System.Security.Principal.SecurityIdentifier($sidBytes, 0) $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
@@ -361,7 +374,7 @@ function Test-ReplicationPermissions {
$acl = $null $acl = $null
try { try {
$de = New-Object System.DirectoryServices.DirectoryEntry( $de = New-Object System.DirectoryServices.DirectoryEntry(
"LDAP://$Server/$DomainDN", "LDAP://$Server/$([System.Uri]::EscapeDataString($DomainDN))",
$Credential.UserName, $Credential.UserName,
$Credential.GetNetworkCredential().Password $Credential.GetNetworkCredential().Password
) )
@@ -379,7 +392,7 @@ function Test-ReplicationPermissions {
$aceExistsForGuid = $false $aceExistsForGuid = $false
foreach ($ace in $acl) { foreach ($ace in $acl) {
if ($ace.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { continue } 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 domain root itself is not covered
if ([bool]($ace.PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::InheritOnly)) { continue } if ([bool]($ace.PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::InheritOnly)) { continue }
$rights = $ace.ActiveDirectoryRights $rights = $ace.ActiveDirectoryRights
$hasExtended = [bool]($rights -band [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight) $hasExtended = [bool]($rights -band [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight)
@@ -409,3 +422,32 @@ function Test-ReplicationPermissions {
Write-Host ("[+] Replication permissions verified for '{0}'." -f $Credential.UserName) Write-Host ("[+] Replication permissions verified for '{0}'." -f $Credential.UserName)
} }
function Test-DCClockSkew {
param(
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][System.Management.Automation.PSCredential]$Credential
)
try {
$rootDse = New-Object System.DirectoryServices.DirectoryEntry(
"LDAP://$Server/RootDSE",
$Credential.UserName,
$Credential.GetNetworkCredential().Password
)
$dcTimeStr = $rootDse.Properties['currentTime'][0]
$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 {
Write-Warning ("Could not check clock skew against '{0}': {1}" -f $Server, $_.Exception.Message)
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Elysium.ps1 ## ## File: Elysium.ps1 ##
## Version: 2.2.5 ## ## Version: 2.4.2 ##
## 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.5 ## ## Version: 2.4.2 ##
## 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.5 ## ## Version: 2.4.2 ##
## 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.5 ## ## Version: 2.4.2 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
+25 -2
View File
@@ -8,7 +8,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Test-WeakADPasswords.ps1 ## ## File: Test-WeakADPasswords.ps1 ##
## Version: 2.2.5 ## ## Version: 2.4.2 ##
## 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 {
+1 -1
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Uninstall.ps1 ## ## File: Uninstall.ps1 ##
## Version: 2.2.5 ## ## Version: 2.4.2 ##
## 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.5 ## ## Version: 2.4.2 ##
## 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.5 ## ## Version: 2.4.2 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################