diff --git a/README.md b/README.md index c5e34d4..e6a1b38 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Every run also emits a cleaned, DSInternals-friendly `khdb-clean.txt` beside the When `-ForcePlainText` is specified the script automatically keeps a checkpoint (default: `/khdb.checkpoint.json`) and resumes from it on the next run so massive inputs don’t restart from scratch. Use `-CheckpointPath` to relocate that file or `-NoCheckpoint` to disable the behavior entirely. ### Test Weak AD passwords Run script Elysium.ps1 as an administrator and choose option 2 (Test Weak AD Passwords). -The script will list domains in the same order as they appear in `ElysiumSettings.txt` and, after you pick one, prompt for the corresponding domain administrator password (the username is taken from the settings file). +The script lists domains in the same order as they appear in `ElysiumSettings.txt`. After you pick one, it prompts for credentials and validates them against the selected domain controller before running the password-quality test. The tool connects to the selected Domain Controller and compares accounts against KHDB (respecting the optional `CheckOnlyEnabledUsers` flag if configured). A timestamped text report is saved under `Reports`, and accounts with dictionary hits are also exported to a dedicated UPN-only text file to support follow-up automation. The KHDB file is consumed via binary search as a sorted hash list (plain text lines like `HASH:count`); ensure the file you place at `khdb.txt` keeps that ordering and omits stray blank lines. @@ -58,6 +58,14 @@ The DSInternals cmdlets (`Get-ADReplAccount`/`Test-PasswordQuality`) pull replic 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. +#### Common errors +- `The server has rejected the client credentials.` or `Credentials ... were rejected`: + The supplied username/password is invalid for the selected domain controller, or the session is not running in the expected domain context. Re-run and provide valid domain credentials. +- `Get-ADReplAccount: Access is denied`: + Credentials are valid, but the account does not have the three replication permissions listed above. +- `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. + #### Optional usage beacon If you want to know the script was executed without collecting telemetry, set a pre-signed URL (for example, an S3 `PUT` URL) in `UsageBeaconUrl` inside `ElysiumSettings.txt`. When present, the weak-password script issues a single request as soon as it loads the settings. Only the script name, its version, a UTC timestamp, and the optional `UsageBeaconInstanceId` value are sent, and network failures never block the run. Choose the HTTP verb via `UsageBeaconMethod` (`GET`, `POST`, or `PUT`) and adjust the timeout with `UsageBeaconTimeoutSeconds` if your storage endpoint needs more time. diff --git a/Test-WeakADPasswords.ps1 b/Test-WeakADPasswords.ps1 index e47a403..ca952e9 100644 --- a/Test-WeakADPasswords.ps1 +++ b/Test-WeakADPasswords.ps1 @@ -402,12 +402,42 @@ function Get-UserUPN { # (removed stray top-level loop; UPN enrichment happens during report generation below) +function Get-ValidatedADCredential { + param ( + [Parameter(Mandatory)][string]$DomainName, + [Parameter(Mandatory)][string]$Server, + [int]$MaxAttempts = 3 + ) + + for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { + $credential = Get-Credential -Message "Enter AD credentials with replication rights for $DomainName (attempt $attempt/$MaxAttempts)" + if ($null -eq $credential) { + throw "Credential prompt was cancelled." + } + + try { + Get-ADDomain -Server $Server -Credential $credential -ErrorAction Stop | Out-Null + Write-Verbose ("Credential pre-check succeeded for '{0}' against '{1}'." -f $credential.UserName, $Server) + return $credential + } catch { + $message = $_.Exception.Message + if ($message -match 'rejected the client credentials|unknown user name|bad password|logon failure') { + Write-Warning ("Credentials were rejected for '{0}' (attempt {1}/{2})." -f $credential.UserName, $attempt, $MaxAttempts) + if ($attempt -lt $MaxAttempts) { continue } + throw "Credentials were rejected by domain controller '$Server' after $MaxAttempts attempts." + } + throw "Credential pre-check failed against '$Server': $message" + } + } +} + # Function to test for weak AD passwords function Test-WeakADPasswords { param ( [hashtable]$DomainDetails, [string]$FilePath, - [bool]$CheckOnlyEnabledUsers = $false + [bool]$CheckOnlyEnabledUsers = $false, + [System.Management.Automation.PSCredential]$Credential ) # User selects a domain @@ -423,8 +453,17 @@ function Test-WeakADPasswords { $selectedDomain = $DomainDetails[$selection] Write-Verbose "Selected domain: $($selectedDomain.Name)" - # Prompt for DA credentials - $credential = Get-Credential -Message "Enter AD credentials with replication rights for $($selectedDomain.Name)" + if ([string]::IsNullOrWhiteSpace($selectedDomain["DC"])) { + Write-Error ("Domain '{0}' does not have a configured DC in ElysiumSettings.txt." -f $selectedDomain.Name) + return + } + + if ($null -eq $Credential) { + $credential = Get-ValidatedADCredential -DomainName $selectedDomain.Name -Server $selectedDomain["DC"] + } else { + $credential = $Credential + Write-Verbose ("Using credential supplied by caller: {0}" -f $credential.UserName) + } # Performing the test Write-Verbose "Testing password quality for $($selectedDomain.Name)..." @@ -440,7 +479,16 @@ function Test-WeakADPasswords { $testResults = $accounts | Test-PasswordQuality -WeakPasswordHashesSortedFile $FilePath Write-Verbose "Password quality test completed." } catch { - Write-Error ("An error occurred while testing passwords: {0}" -f $_.Exception.Message) + $message = $_.Exception.Message + if ($message -match 'Access is denied') { + Write-Error ("Access denied while reading replication data from '{0}' using '{1}'. Ensure this account has Replicating Directory Changes, Replicating Directory Changes All, and Replicating Directory Changes In Filtered Set on the domain." -f $selectedDomain["DC"], $credential.UserName) + return + } + if ($message -match 'rejected the client credentials|unknown user name|bad password|logon failure') { + Write-Error ("Credentials for '{0}' were rejected by '{1}'. Re-run and provide valid domain credentials." -f $credential.UserName, $selectedDomain["DC"]) + return + } + Write-Error ("An error occurred while testing passwords: {0}" -f $message) return }