From fc91f0d6b0fa9809a754b3b4ed6194849b160e10 Mon Sep 17 00:00:00 2001 From: Tomas Kracmar Date: Tue, 9 Jun 2026 13:32:21 +0200 Subject: [PATCH] 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. --- Bump-Version.ps1 | 2 +- CHANGELOG.md | 12 ++++++++++ Elysium.Common.ps1 | 46 ++++++++++++++++++++++++++++++++++++-- Elysium.ps1 | 2 +- ElysiumSettings.txt.sample | 2 +- Extract-NTHashes.ps1 | 2 +- Prepare-KHDBStorage.ps1 | 2 +- Test-WeakADPasswords.ps1 | 11 +++++---- Uninstall.ps1 | 2 +- Update-KHDB.ps1 | 2 +- Update-LithnetStore.ps1 | 2 +- 11 files changed, 71 insertions(+), 14 deletions(-) diff --git a/Bump-Version.ps1 b/Bump-Version.ps1 index 1db4fcd..fe6827a 100644 --- a/Bump-Version.ps1 +++ b/Bump-Version.ps1 @@ -8,7 +8,7 @@ ################################################## ## Project: Elysium ## ## File: Bump-Version.ps1 ## -## Version: 2.3.0 ## +## Version: 2.4.0 ## ## Support: support@cqre.net ## ################################################## diff --git a/CHANGELOG.md b/CHANGELOG.md index 6928340..136f1aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ Starting with **v2.2.0**, Elysium uses a **unified project version**. All script --- +## [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 diff --git a/Elysium.Common.ps1 b/Elysium.Common.ps1 index fb22832..60a3843 100644 --- a/Elysium.Common.ps1 +++ b/Elysium.Common.ps1 @@ -1,4 +1,4 @@ -$script:ElysiumVersion = '2.3.0' +$script:ElysiumVersion = '2.4.0' function Invoke-RestartWithExecutable { param( @@ -338,7 +338,7 @@ function Test-ReplicationPermissions { try { $samName = $Credential.UserName -replace '^.*\\', '' $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) # tokenGroups is a constructed attribute containing all SIDs in the user's token, @@ -353,6 +353,19 @@ function Test-ReplicationPermissions { $sid = New-Object System.Security.Principal.SecurityIdentifier($sidBytes, 0) [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 { Write-Warning ("Could not resolve account SIDs for replication permission pre-check: {0}. Skipping." -f $_.Exception.Message) return @@ -409,3 +422,32 @@ function Test-ReplicationPermissions { 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) + } +} diff --git a/Elysium.ps1 b/Elysium.ps1 index bab6d14..8b0a9d4 100644 --- a/Elysium.ps1 +++ b/Elysium.ps1 @@ -7,7 +7,7 @@ ################################################## ## Project: Elysium ## ## File: Elysium.ps1 ## -## Version: 2.3.0 ## +## Version: 2.4.0 ## ## Support: support@cqre.net ## ################################################## diff --git a/ElysiumSettings.txt.sample b/ElysiumSettings.txt.sample index 11676c5..5767bfd 100644 --- a/ElysiumSettings.txt.sample +++ b/ElysiumSettings.txt.sample @@ -8,7 +8,7 @@ ################################################## ## Project: Elysium ## ## File: ElysiumSettings.txt ## -## Version: 2.3.0 ## +## Version: 2.4.0 ## ## Support: support@cqre.net ## ################################################## diff --git a/Extract-NTHashes.ps1 b/Extract-NTHashes.ps1 index 5c220f8..978cf7c 100644 --- a/Extract-NTHashes.ps1 +++ b/Extract-NTHashes.ps1 @@ -7,7 +7,7 @@ ################################################## ## Project: Elysium ## ## File: Extract-NTHashes.ps1 ## -## Version: 2.3.0 ## +## Version: 2.4.0 ## ## Support: support@cqre.net ## ################################################## diff --git a/Prepare-KHDBStorage.ps1 b/Prepare-KHDBStorage.ps1 index 6467088..cb3adcf 100644 --- a/Prepare-KHDBStorage.ps1 +++ b/Prepare-KHDBStorage.ps1 @@ -7,7 +7,7 @@ ################################################## ## Project: Elysium ## ## File: Prepare-KHDBStorage.ps1 ## -## Version: 2.3.0 ## +## Version: 2.4.0 ## ## Support: support@cqre.net ## ################################################## diff --git a/Test-WeakADPasswords.ps1 b/Test-WeakADPasswords.ps1 index 5f7e7e8..965d64d 100644 --- a/Test-WeakADPasswords.ps1 +++ b/Test-WeakADPasswords.ps1 @@ -8,7 +8,7 @@ ################################################## ## Project: Elysium ## ## File: Test-WeakADPasswords.ps1 ## -## Version: 2.3.0 ## +## Version: 2.4.0 ## ## Support: support@cqre.net ## ################################################## @@ -398,12 +398,14 @@ $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+: Update-Module DSInternals" -f $dsInternalsVersion) + 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 { - Update-Module -Name DSInternals -Force -ErrorAction Stop + # 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 { @@ -602,9 +604,10 @@ function Test-WeakADPasswords { 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 { $domainInfo = Get-ADDomain -Server $selectedDomain["DC"] -Credential $credential -ErrorAction Stop + Test-DCClockSkew -Server $selectedDomain["DC"] -Credential $credential Test-ReplicationPermissions -DomainDN $domainInfo.DistinguishedName ` -Server $selectedDomain["DC"] -Credential $credential } catch { diff --git a/Uninstall.ps1 b/Uninstall.ps1 index acbae86..636676f 100644 --- a/Uninstall.ps1 +++ b/Uninstall.ps1 @@ -7,7 +7,7 @@ ################################################## ## Project: Elysium ## ## File: Uninstall.ps1 ## -## Version: 2.3.0 ## +## Version: 2.4.0 ## ## Support: support@cqre.net ## ################################################## diff --git a/Update-KHDB.ps1 b/Update-KHDB.ps1 index 2345a04..c617e4a 100644 --- a/Update-KHDB.ps1 +++ b/Update-KHDB.ps1 @@ -7,7 +7,7 @@ ################################################## ## Project: Elysium ## ## File: Update-KHDB.ps1 ## -## Version: 2.3.0 ## +## Version: 2.4.0 ## ## Support: support@cqre.net ## ################################################## diff --git a/Update-LithnetStore.ps1 b/Update-LithnetStore.ps1 index 9adc184..134b66a 100644 --- a/Update-LithnetStore.ps1 +++ b/Update-LithnetStore.ps1 @@ -7,7 +7,7 @@ ################################################## ## Project: Elysium ## ## File: Update-LithnetStore.ps1 ## -## Version: 2.3.0 ## +## Version: 2.4.0 ## ## Support: support@cqre.net ## ##################################################