Compare commits
16 Commits
backup/ori
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7874c0e65b | |||
| be96cbf9a5 | |||
| 42fee2ff84 | |||
| 2ff4964537 | |||
| 6e4cc874b0 | |||
| ec27206453 | |||
| a55ef3713f | |||
| bda19432e2 | |||
| 7c2bb65a86 | |||
| 5a64558bb9 | |||
| 4b1b841383 | |||
| 964e91d20f | |||
| 353352eeb2 | |||
| 05e9358357 | |||
| 5799881418 | |||
| 0d9a460057 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -2,4 +2,8 @@
|
|||||||
khdb.txt
|
khdb.txt
|
||||||
khdb.txt.zip
|
khdb.txt.zip
|
||||||
ElysiumSettings.txt
|
ElysiumSettings.txt
|
||||||
/Reports
|
/Reports
|
||||||
|
/khdb-shards
|
||||||
|
khdb-manifest.json
|
||||||
|
/elysium/.vscode
|
||||||
|
/.vscode
|
||||||
58
CHANGELOG.md
58
CHANGELOG.md
@@ -1,5 +1,63 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-10-30
|
||||||
|
|
||||||
|
### Update-KHDB.ps1 v2.0.0
|
||||||
|
Changed:
|
||||||
|
- Replaced single-archive workflow with manifest-driven, two-hex shard downloads that verify SHA256/size before in-place updates.
|
||||||
|
- Added incremental refresh logic, stale shard cleanup, and automatic rebuild of the merged `khdb.txt` for downstream scripts.
|
||||||
|
- Hardened validation to stream-check merged output while preserving strict TLS, retry, and transcript behaviour.
|
||||||
|
|
||||||
|
### ElysiumSettings.txt.sample v1.3.0
|
||||||
|
Added:
|
||||||
|
- Documented `KhdbManifestPath`, `KhdbShardPrefix`, and `KhdbLocalShardDir` defaults for the shard-aware updater.
|
||||||
|
|
||||||
|
### README.md
|
||||||
|
Changed:
|
||||||
|
- Described the manifest/shard update flow so operators understand the incremental download model and automatic cleanup.
|
||||||
|
|
||||||
|
### Prepare-KHDBStorage.ps1 v1.0.0
|
||||||
|
Added:
|
||||||
|
- Helper script to split `khdb.txt` (or a directory/list of `.gz` HIBP slices) into two-hex shards, build the JSON manifest, and push the package to Azure Blob Storage or S3-compatible endpoints.
|
||||||
|
- Validation step that tallies and quarantines malformed hashes before sharding, writing `invalid-hashes.txt` plus a console summary so bad data never reaches storage.
|
||||||
|
- Optional `-ShowProgress` mode emitting periodic `Write-Progress` updates (interval configurable) so large ingests visibly tick forward.
|
||||||
|
- Automatic reconstruction of HIBP NTLM hashes (file-prefix + suffix) so partially stored hashes still produce full 32-hex values in the shards, plus per-prefix deduplication that keeps the highest observed count.
|
||||||
|
- `-ForcePlainText` switch to skip `.gz` expansions entirely and treat the source as pre-built hash lines (skipped entries are reported separately).
|
||||||
|
- Emits a merged `khdb-clean.txt` alongside the shards for DSInternals or offline review, including SHA256 fingerprints for both manifest and clean output.
|
||||||
|
- Automatic checkpoint/resume when `-ForcePlainText` is used (configurable via `-CheckpointPath`, disable with `-NoCheckpoint`) so large ingests can be paused and resumed without reprocessing prior shards.
|
||||||
|
|
||||||
|
## 2025-10-26
|
||||||
|
|
||||||
|
### Test-WeakADPasswords.ps1 v1.3.3
|
||||||
|
Added:
|
||||||
|
- Opt-in usage beacon that fires a single HTTP request (GET/POST/PUT) after settings load, suitable for pre-signed S3 URLs, and only includes script name, version, and a UTC timestamp (plus optional instance ID).
|
||||||
|
- Instance identifier header/body support and configurable timeout so adopters can differentiate deployments without collecting user data.
|
||||||
|
|
||||||
|
### ElysiumSettings.txt.sample v1.2.0
|
||||||
|
Added:
|
||||||
|
- Documented `UsageBeacon*` keys (URL, method, instance ID, timeout) so telemetry stays disabled by default but easy to enable.
|
||||||
|
|
||||||
|
### README.md
|
||||||
|
Added:
|
||||||
|
- Usage beacon section explaining how to configure the lightweight tracking call and what metadata is transmitted.
|
||||||
|
|
||||||
|
## 2025-10-21
|
||||||
|
|
||||||
|
### Extract-NTHashes.ps1 v1.2.1
|
||||||
|
Fixed:
|
||||||
|
- Corrected SigV4 host header formatting so non-default ports serialize without parser errors.
|
||||||
|
- Hardened hashing helpers to avoid `ComputeHash` overload ambiguity under Windows PowerShell.
|
||||||
|
- Domain selection menu now respects the configured numeric order.
|
||||||
|
|
||||||
|
### Test-WeakADPasswords.ps1 v1.3.2
|
||||||
|
Changed:
|
||||||
|
- Switched to the sorted KHDB path when driving `Test-PasswordQuality`, eliminating full linear scans and avoiding malformed-line crashes on massive datasets.
|
||||||
|
|
||||||
|
### Test-WeakADPasswords.ps1 v1.3.1
|
||||||
|
Fixed:
|
||||||
|
- Domain picker now renders in numeric order from settings for predictable operator workflows.
|
||||||
|
- UPN export now relies on structured weak-password results, so dictionary hit UPN lists are populated reliably.
|
||||||
|
|
||||||
## 2025-10-10
|
## 2025-10-10
|
||||||
|
|
||||||
### Test-WeakADPasswords.ps1 v1.3.0
|
### Test-WeakADPasswords.ps1 v1.3.0
|
||||||
|
|||||||
70
Elysium.Common.ps1
Normal file
70
Elysium.Common.ps1
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
function Invoke-RestartWithExecutable {
|
||||||
|
param(
|
||||||
|
[string]$ExecutablePath,
|
||||||
|
[hashtable]$BoundParameters,
|
||||||
|
[object[]]$UnboundArguments
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $ExecutablePath) { return }
|
||||||
|
if (-not $PSCommandPath) { return }
|
||||||
|
|
||||||
|
$argList = @('-NoLogo', '-NoProfile', '-File', $PSCommandPath)
|
||||||
|
|
||||||
|
if ($BoundParameters) {
|
||||||
|
foreach ($entry in $BoundParameters.GetEnumerator()) {
|
||||||
|
$key = "-$($entry.Key)"
|
||||||
|
$value = $entry.Value
|
||||||
|
if ($value -is [System.Management.Automation.SwitchParameter]) {
|
||||||
|
if ($value.IsPresent) { $argList += $key }
|
||||||
|
} else {
|
||||||
|
$argList += $key
|
||||||
|
$argList += $value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($UnboundArguments) {
|
||||||
|
$argList += $UnboundArguments
|
||||||
|
}
|
||||||
|
|
||||||
|
& $ExecutablePath @argList
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
function Restart-WithPwshIfAvailable {
|
||||||
|
param(
|
||||||
|
[hashtable]$BoundParameters,
|
||||||
|
[object[]]$UnboundArguments
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($PSVersionTable.PSVersion.Major -ge 7 -or $PSVersionTable.PSEdition -eq 'Core') { return }
|
||||||
|
$pwsh = Get-Command -Name 'pwsh' -ErrorAction SilentlyContinue
|
||||||
|
if (-not $pwsh) { return }
|
||||||
|
Write-Host ("PowerShell 7 detected at '{0}'; relaunching script under pwsh..." -f $pwsh.Path)
|
||||||
|
Invoke-RestartWithExecutable -ExecutablePath $pwsh.Path -BoundParameters $BoundParameters -UnboundArguments $UnboundArguments
|
||||||
|
}
|
||||||
|
|
||||||
|
function Restart-WithWindowsPowerShellIfAvailable {
|
||||||
|
param(
|
||||||
|
[hashtable]$BoundParameters,
|
||||||
|
[object[]]$UnboundArguments
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($PSVersionTable.PSEdition -eq 'Desktop') { return }
|
||||||
|
$powershellCmd = Get-Command -Name 'powershell.exe' -ErrorAction SilentlyContinue
|
||||||
|
$powershellPath = $null
|
||||||
|
if ($powershellCmd) {
|
||||||
|
$powershellPath = $powershellCmd.Path
|
||||||
|
} else {
|
||||||
|
$defaultPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
||||||
|
if (Test-Path -LiteralPath $defaultPath) {
|
||||||
|
$powershellPath = $defaultPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $powershellPath) {
|
||||||
|
Write-Warning 'Windows PowerShell (powershell.exe) was not found; continuing under current host.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Write-Host ("Windows PowerShell detected at '{0}'; relaunching script under powershell.exe..." -f $powershellPath)
|
||||||
|
Invoke-RestartWithExecutable -ExecutablePath $powershellPath -BoundParameters $BoundParameters -UnboundArguments $UnboundArguments
|
||||||
|
}
|
||||||
49
Elysium.ps1
49
Elysium.ps1
@@ -7,7 +7,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Elysium.ps1 ##
|
## File: Elysium.ps1 ##
|
||||||
## Version: 1.2.0 ##
|
## Version: 1.3.0 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
@@ -21,13 +21,13 @@ Elysium.ps1 offers a menu to perform various actions:
|
|||||||
2. Test Weak AD Passwords
|
2. Test Weak AD Passwords
|
||||||
3. Extract and Send Current Hashes for KHDB Update
|
3. Extract and Send Current Hashes for KHDB Update
|
||||||
4. Uninstall the tool
|
4. Uninstall the tool
|
||||||
5. Exit
|
5. Update Lithnet Password Protection store
|
||||||
|
6. Exit
|
||||||
#>
|
#>
|
||||||
|
|
||||||
# Safer defaults
|
# Safer defaults
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
# Define the path to the settings file
|
# Define the path to the settings file
|
||||||
$settingsFilePath = Join-Path -Path $PSScriptRoot -ChildPath "ElysiumSettings.txt"
|
$settingsFilePath = Join-Path -Path $PSScriptRoot -ChildPath "ElysiumSettings.txt"
|
||||||
|
|
||||||
@@ -69,6 +69,20 @@ function Start-OrchestratorTranscript {
|
|||||||
|
|
||||||
function Stop-OrchestratorTranscript { try { Stop-Transcript | Out-Null } catch {} }
|
function Stop-OrchestratorTranscript { try { Stop-Transcript | Out-Null } catch {} }
|
||||||
|
|
||||||
|
function Invoke-WindowsPowerShellScript {
|
||||||
|
param([string]$ScriptPath)
|
||||||
|
$powershellCmd = Get-Command -Name 'powershell.exe' -ErrorAction SilentlyContinue
|
||||||
|
if (-not $powershellCmd) {
|
||||||
|
throw "Windows PowerShell (powershell.exe) was not found. Install it or run the script from a Desktop edition session."
|
||||||
|
}
|
||||||
|
$args = @('-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $ScriptPath)
|
||||||
|
& $powershellCmd.Path @args
|
||||||
|
$exitCode = $LASTEXITCODE
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
throw ("Windows PowerShell script '{0}' exited with code {1}." -f $ScriptPath, $exitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Show-Menu {
|
function Show-Menu {
|
||||||
param (
|
param (
|
||||||
[string]$Title = 'Elysium Tool Main Menu'
|
[string]$Title = 'Elysium Tool Main Menu'
|
||||||
@@ -78,8 +92,9 @@ function Show-Menu {
|
|||||||
Write-Host "1: Download/Update Known-Hashes Database (KHDB)"
|
Write-Host "1: Download/Update Known-Hashes Database (KHDB)"
|
||||||
Write-Host "2: Test Weak AD Passwords"
|
Write-Host "2: Test Weak AD Passwords"
|
||||||
Write-Host "3: Extract and Send Current Hashes for KHDB Update"
|
Write-Host "3: Extract and Send Current Hashes for KHDB Update"
|
||||||
Write-Host "4: Uninstall"
|
Write-Host "4: Update Lithnet Password Protection Store"
|
||||||
Write-Host "5: Exit"
|
Write-Host "5: Uninstall"
|
||||||
|
Write-Host "6: Exit"
|
||||||
}
|
}
|
||||||
|
|
||||||
Start-OrchestratorTranscript -BasePath $PSScriptRoot
|
Start-OrchestratorTranscript -BasePath $PSScriptRoot
|
||||||
@@ -94,27 +109,41 @@ do {
|
|||||||
}
|
}
|
||||||
'2' {
|
'2' {
|
||||||
Write-Host "Testing Weak AD Passwords..."
|
Write-Host "Testing Weak AD Passwords..."
|
||||||
& (Join-Path -Path $PSScriptRoot -ChildPath 'Test-WeakADPasswords.ps1')
|
$testScript = Join-Path -Path $PSScriptRoot -ChildPath 'Test-WeakADPasswords.ps1'
|
||||||
|
if ($PSVersionTable.PSEdition -eq 'Desktop') {
|
||||||
|
& $testScript
|
||||||
|
} else {
|
||||||
|
Invoke-WindowsPowerShellScript -ScriptPath $testScript
|
||||||
|
}
|
||||||
}
|
}
|
||||||
'3' {
|
'3' {
|
||||||
Write-Host "Extracting and Sending Current Hashes..."
|
Write-Host "Extracting and Sending Current Hashes..."
|
||||||
& (Join-Path -Path $PSScriptRoot -ChildPath 'Extract-NTHashes.ps1')
|
$extractScript = Join-Path -Path $PSScriptRoot -ChildPath 'Extract-NTHashes.ps1'
|
||||||
|
if ($PSVersionTable.PSEdition -eq 'Desktop') {
|
||||||
|
& $extractScript
|
||||||
|
} else {
|
||||||
|
Invoke-WindowsPowerShellScript -ScriptPath $extractScript
|
||||||
|
}
|
||||||
}
|
}
|
||||||
'4' {
|
'4' {
|
||||||
|
Write-Host "Updating Lithnet Password Protection store..."
|
||||||
|
& (Join-Path -Path $PSScriptRoot -ChildPath 'Update-LithnetStore.ps1')
|
||||||
|
}
|
||||||
|
'5' {
|
||||||
Write-Host "Uninstalling..."
|
Write-Host "Uninstalling..."
|
||||||
& (Join-Path -Path $PSScriptRoot -ChildPath 'Uninstall.ps1')
|
& (Join-Path -Path $PSScriptRoot -ChildPath 'Uninstall.ps1')
|
||||||
}
|
}
|
||||||
'5' {
|
'6' {
|
||||||
Write-Host "Exiting..."
|
Write-Host "Exiting..."
|
||||||
# end loop; transcript will be stopped after the loop
|
# end loop; transcript will be stopped after the loop
|
||||||
$userSelection = '5'
|
$userSelection = '6'
|
||||||
}
|
}
|
||||||
default {
|
default {
|
||||||
Write-Host "Invalid selection, please try again."
|
Write-Host "Invalid selection, please try again."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pause
|
pause
|
||||||
} while ($userSelection -ne '5')
|
} while ($userSelection -ne '6')
|
||||||
} finally {
|
} finally {
|
||||||
Stop-OrchestratorTranscript
|
Stop-OrchestratorTranscript
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: ElysiumSettings.txt ##
|
## File: ElysiumSettings.txt ##
|
||||||
## Version: 1.1.0 ##
|
## Version: 1.3.0 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
@@ -36,6 +36,14 @@ s3SecretAccessKey =
|
|||||||
s3ForcePathStyle = true
|
s3ForcePathStyle = true
|
||||||
s3UseAwsTools = false
|
s3UseAwsTools = false
|
||||||
|
|
||||||
|
# KHDB Shard Settings
|
||||||
|
#####################
|
||||||
|
# The KHDB update script downloads a manifest plus per-prefix shards (default shard size 2).
|
||||||
|
# These values control the remote object names and local storage directory.
|
||||||
|
KhdbManifestPath=khdb/manifest.json
|
||||||
|
KhdbShardPrefix=khdb/shards
|
||||||
|
KhdbLocalShardDir=khdb-shards
|
||||||
|
|
||||||
# Application Settings
|
# Application Settings
|
||||||
######################
|
######################
|
||||||
InstallationPath=
|
InstallationPath=
|
||||||
@@ -43,6 +51,25 @@ ReportPathBase=Reports
|
|||||||
WeakPasswordsDatabase=khdb.txt
|
WeakPasswordsDatabase=khdb.txt
|
||||||
# CheckOnlyEnabledUsers=true
|
# CheckOnlyEnabledUsers=true
|
||||||
|
|
||||||
|
# Lithnet Password Protection Settings
|
||||||
|
######################################
|
||||||
|
LithnetStorePath=
|
||||||
|
LithnetSyncHibp=false
|
||||||
|
LithnetHashSources=khdb.txt
|
||||||
|
LithnetPlaintextSources=
|
||||||
|
LithnetBannedWordSources=
|
||||||
|
|
||||||
|
# Telemetry (optional)
|
||||||
|
######################
|
||||||
|
# These values are empty by default so no telemetry is sent.
|
||||||
|
# Provide a pre-signed URL (for example, an S3 PUT) to receive a single beacon
|
||||||
|
# when the weak-password test starts. Only script name, version, and timestamp
|
||||||
|
# are transmitted; you can set UsageBeaconInstanceId to differentiate deployments.
|
||||||
|
UsageBeaconUrl=
|
||||||
|
UsageBeaconMethod=GET # GET, POST, or PUT
|
||||||
|
UsageBeaconInstanceId=
|
||||||
|
UsageBeaconTimeoutSeconds=5
|
||||||
|
|
||||||
# Notes:
|
# Notes:
|
||||||
# - Required PowerShell modules: DSInternals, ActiveDirectory
|
# - Required PowerShell modules: DSInternals, ActiveDirectory
|
||||||
# For Azure uploads: Az.Storage
|
# For Azure uploads: Az.Storage
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Extract-NTLMHashes.ps1 ##
|
## File: Extract-NTLMHashes.ps1 ##
|
||||||
## Version: 1.2.0 ##
|
## Version: 1.3.0 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ This script will connect to selected domain (defined in ElysiumSettings.txt) usi
|
|||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
$scriptRoot = $PSScriptRoot
|
$scriptRoot = $PSScriptRoot
|
||||||
|
|
||||||
function Start-ExtractTranscript {
|
function Start-ExtractTranscript {
|
||||||
@@ -122,7 +123,14 @@ function New-S3Client {
|
|||||||
function Get-Bytes([string]$s) { return [System.Text.Encoding]::UTF8.GetBytes($s) }
|
function Get-Bytes([string]$s) { return [System.Text.Encoding]::UTF8.GetBytes($s) }
|
||||||
function Get-HashHex([byte[]]$bytes) {
|
function Get-HashHex([byte[]]$bytes) {
|
||||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||||
try { return ([BitConverter]::ToString($sha.ComputeHash($bytes))).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() }
|
try {
|
||||||
|
if ($null -eq $bytes) { $bytes = [byte[]]@() }
|
||||||
|
$ms = [System.IO.MemoryStream]::new($bytes)
|
||||||
|
try {
|
||||||
|
$hashBytes = $sha.ComputeHash($ms)
|
||||||
|
return ([BitConverter]::ToString($hashBytes)).Replace('-', '').ToLowerInvariant()
|
||||||
|
} finally { $ms.Dispose() }
|
||||||
|
} finally { $sha.Dispose() }
|
||||||
}
|
}
|
||||||
function Get-FileSha256Hex([string]$path) {
|
function Get-FileSha256Hex([string]$path) {
|
||||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||||
@@ -131,7 +139,11 @@ function Get-FileSha256Hex([string]$path) {
|
|||||||
}
|
}
|
||||||
function HmacSha256([byte[]]$key, [string]$data) {
|
function HmacSha256([byte[]]$key, [string]$data) {
|
||||||
$h = [System.Security.Cryptography.HMACSHA256]::new($key)
|
$h = [System.Security.Cryptography.HMACSHA256]::new($key)
|
||||||
try { return $h.ComputeHash((Get-Bytes $data)) } finally { $h.Dispose() }
|
try {
|
||||||
|
$dataBytes = Get-Bytes $data
|
||||||
|
$ms = [System.IO.MemoryStream]::new($dataBytes)
|
||||||
|
try { return $h.ComputeHash($ms) } finally { $ms.Dispose() }
|
||||||
|
} finally { $h.Dispose() }
|
||||||
}
|
}
|
||||||
function GetSignatureKey([string]$secret, [string]$dateStamp, [string]$regionName, [string]$serviceName) {
|
function GetSignatureKey([string]$secret, [string]$dateStamp, [string]$regionName, [string]$serviceName) {
|
||||||
$kDate = HmacSha256 (Get-Bytes ('AWS4' + $secret)) $dateStamp
|
$kDate = HmacSha256 (Get-Bytes ('AWS4' + $secret)) $dateStamp
|
||||||
@@ -159,7 +171,7 @@ function BuildAuthHeaders($method, [System.Uri]$uri, [string]$region, [string]$a
|
|||||||
$amzdate = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')
|
$amzdate = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')
|
||||||
$datestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd')
|
$datestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd')
|
||||||
$hostHeader = $uri.Host
|
$hostHeader = $uri.Host
|
||||||
if (-not $uri.IsDefaultPort) { $hostHeader = "$hostHeader:$($uri.Port)" }
|
if (-not $uri.IsDefaultPort) { $hostHeader = "{0}:{1}" -f $hostHeader, $uri.Port }
|
||||||
|
|
||||||
$canonicalUri = BuildCanonicalPath $uri
|
$canonicalUri = BuildCanonicalPath $uri
|
||||||
$canonicalQueryString = ''
|
$canonicalQueryString = ''
|
||||||
@@ -308,8 +320,8 @@ function Get-FileChecksum {
|
|||||||
$reportBase = Normalize-ReportPath -p $ElysiumSettings['ReportPathBase']
|
$reportBase = Normalize-ReportPath -p $ElysiumSettings['ReportPathBase']
|
||||||
if (-not (Test-Path $reportBase)) { New-Item -Path $reportBase -ItemType Directory -Force | Out-Null }
|
if (-not (Test-Path $reportBase)) { New-Item -Path $reportBase -ItemType Directory -Force | Out-Null }
|
||||||
|
|
||||||
# Build domain details from settings
|
# Build domain details from settings (ordered to keep numeric index order)
|
||||||
$DomainDetails = @{}
|
$DomainDetails = [ordered]@{}
|
||||||
for ($i = 1; $ElysiumSettings.ContainsKey("Domain${i}Name"); $i++) {
|
for ($i = 1; $ElysiumSettings.ContainsKey("Domain${i}Name"); $i++) {
|
||||||
$DomainDetails["$i"] = @{
|
$DomainDetails["$i"] = @{
|
||||||
Name = $ElysiumSettings["Domain${i}Name"]
|
Name = $ElysiumSettings["Domain${i}Name"]
|
||||||
@@ -320,7 +332,7 @@ for ($i = 1; $ElysiumSettings.ContainsKey("Domain${i}Name"); $i++) {
|
|||||||
|
|
||||||
# User selects a domain
|
# User selects a domain
|
||||||
Write-Host "Select a domain to extract NTLM hashes:"
|
Write-Host "Select a domain to extract NTLM hashes:"
|
||||||
$DomainDetails.GetEnumerator() | ForEach-Object { Write-Host "$($_.Key): $($_.Value.Name)" }
|
$DomainDetails.GetEnumerator() | Sort-Object { [int]$_.Key } | ForEach-Object { Write-Host "$($_.Key): $($_.Value.Name)" }
|
||||||
$selection = Read-Host "Enter the number of the domain"
|
$selection = Read-Host "Enter the number of the domain"
|
||||||
$selectedDomain = $DomainDetails[$selection]
|
$selectedDomain = $DomainDetails[$selection]
|
||||||
|
|
||||||
|
|||||||
1312
Prepare-KHDBStorage.ps1
Normal file
1312
Prepare-KHDBStorage.ps1
Normal file
File diff suppressed because it is too large
Load Diff
50
README.md
50
README.md
@@ -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:** A domain user account with Domain Admin privileges for each tested AD domain. This account should be active only during testing.
|
* **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.
|
||||||
* **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).
|
||||||
---
|
---
|
||||||
## Operation
|
## Operation
|
||||||
@@ -20,19 +20,57 @@ Sensitive operations are confined only to the dedicated host. In the third step,
|
|||||||
This tool is provided in private git repository. Installation and updating is done with cloning and pulling from this repository.
|
This tool is provided in private git repository. Installation and updating is done with cloning and pulling from this repository.
|
||||||
During first run, the tool will ask for passphrase that will be used to encrypt/decrypt sensitive content.
|
During first run, the tool will ask for passphrase that will be used to encrypt/decrypt sensitive content.
|
||||||
After installation, edit ElysiumSettings.txt, check all variables and add domains to test.
|
After installation, edit ElysiumSettings.txt, check all variables and add domains to test.
|
||||||
|
All scripts automatically relaunch under PowerShell 7 (`pwsh`) when it is installed so that features like parallel transfers are available; if pwsh is missing they continue under Windows PowerShell 5.1 with the legacy single-threaded behavior. The two DSInternals-driven workflows (menu options 2 and 3) load the legacy `ActiveDirectory` and `DSInternals` modules, so they automatically fall back to Windows PowerShell even if pwsh is present.
|
||||||
### Update Known-Hashed Database (KHDB)
|
### Update Known-Hashed Database (KHDB)
|
||||||
Run script Elysium.ps1 as an administrator and choose option 1 (Update Known-Hashes Database).
|
Run script Elysium.ps1 as an administrator and choose option 1 (Update Known-Hashes Database).
|
||||||
The script downloads the database from the configured storage (Azure Blob or S3-compatible), decompresses it and updates the current database.
|
The updater now pulls a manifest plus individual hash shards (two-hex prefix layout) from the configured storage (Azure Blob or S3-compatible), verifies checksums, replaces only changed shards, and rebuilds `khdb.txt` for local use. Deleted shards listed in the manifest are removed automatically. When PowerShell 7 is available the downloader automatically fetches up to `-MaxParallelTransfers` shards in parallel (default `5`); on Windows PowerShell 5.1 it reverts to the original sequential behavior. Override the concurrency as needed when running the script directly (for example `.\Update-KHDB.ps1 -MaxParallelTransfers 8`).
|
||||||
|
|
||||||
|
To publish an updated shard set, run `Prepare-KHDBStorage.ps1` against your sorted `khdb.txt` (or point it at the directory/list of the Have I Been Pwned `.gz` slices). The helper reconstructs the full 32‑hex NTLM values (prefix + remainder), deduplicates per hash (keeping the largest count), splits by the first two hex characters, writes a manifest (`version`, `sha256`, `size`, entry counts), and can upload the resulting files directly to Azure Blob Storage (via SAS) or S3-compatible endpoints using SigV4. Invalid or malformed entries are omitted automatically, and a short report (aggregate counts + `invalid-hashes.txt`) is produced for review. Example:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Prepare-KHDBStorage.ps1 -SourcePath .\khdb.txt `
|
||||||
|
-OutputRoot .\publish `
|
||||||
|
-StorageProvider S3 `
|
||||||
|
-S3EndpointUrl https://s3.example.com `
|
||||||
|
-S3BucketName private-khdb `
|
||||||
|
-S3AccessKeyId AKIA... `
|
||||||
|
-S3SecretAccessKey ... `
|
||||||
|
-ManifestRemotePath khdb/manifest.json `
|
||||||
|
-ShardRemotePrefix khdb/shards
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `-SkipUpload` to stage files locally, or `-StorageProvider Azure` with `storageAccountName`/`containerName`/`sasToken` when targeting Azure Blob Storage. Add `-ShowProgress` (optionally tune `-ProgressUpdateInterval`) if you want a running `Write-Progress` indicator while the hashes are being split. Pass `-ForcePlainText` when your `khdb.txt` already contains complete hashes and you want `.gz` references treated as invalid instead of being expanded. When you only need to push an already prepared package, combine `-UploadOnly` with `-OutputRoot` pointing at the existing shard directory and choose the storage provider to perform an upload-only run. Missing storage values are pulled from `ElysiumSettings.txt` automatically (override the path with `-SettingsPath`) so you don’t have to retype S3/Azure credentials for every run. On PowerShell 7, `Prepare-KHDBStorage.ps1` can push shards concurrently by setting `-MaxParallelTransfers` (default `5`); Windows PowerShell 5.1 automatically falls back to serial uploads.
|
||||||
|
|
||||||
|
Every run also emits a cleaned, DSInternals-friendly `khdb-clean.txt` beside the shards so you can inspect or distribute the merged list before publishing.
|
||||||
|
|
||||||
|
When `-ForcePlainText` is specified the script automatically keeps a checkpoint (default: `<output>/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
|
### Test Weak AD passwords
|
||||||
Run script Elysium.ps1 as an administrator and choose option 2 (Test Weak AD Passwords).
|
Run script Elysium.ps1 as an administrator and choose option 2 (Test Weak AD Passwords).
|
||||||
The script will then ask for the domain to be tested and upon choice will ask for domain administrator password. The DA username is already provided in the script for each domain.
|
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 tool then connects to Domain Controller and tests all enabled users in the domain against KHDB. PDF report with findings is then generated.
|
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.
|
||||||
|
|
||||||
|
#### 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:
|
||||||
|
- `Replicating Directory Changes`
|
||||||
|
- `Replicating Directory Changes All`
|
||||||
|
- `Replicating Directory Changes In Filtered Set` (needed 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.
|
||||||
|
|
||||||
|
#### 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.
|
||||||
|
|
||||||
### Send current hashes for update KHDB
|
### Send current hashes for update KHDB
|
||||||
Run script Elysium.ps1 as an administrator and choose option 3 (Extract and Send Hashes).
|
Run script Elysium.ps1 as an administrator and choose option 3 (Extract and Send Hashes).
|
||||||
The tool will then ask for domain and password of domain administrator. With correct credentials, the tool will then extract current hashes (no history) of non-disabled users, compresses and encrypts them and uploads them to the configured storage (Azure Blob or S3-compatible) for pickup by the tool provider.
|
Domains are listed in configuration order, after which the script prompts for the replication-capable account password. With valid credentials, it extracts current NTLM hashes (no history) for active accounts, compresses the results, encrypts them with the configured passphrase, and uploads the payload to the configured storage (Azure Blob or S3-compatible). A checksum-verified round-trip download confirms the upload before local artifacts are removed.
|
||||||
|
|
||||||
|
### Update Lithnet Password Protection store
|
||||||
|
Run script Elysium.ps1 as an administrator and choose option 5 (Update Lithnet Password Protection Store).
|
||||||
|
Configure the target folder via `LithnetStorePath` in `ElysiumSettings.txt` (the location created with `Open-Store`). The script automatically imports the `khdb.txt` file unless you override/add additional NTLM hash lists in `LithnetHashSources` (comma or semicolon separated). You can also populate plaintext password lists (`LithnetPlaintextSources`) and banned-word files (`LithnetBannedWordSources`), or enable `LithnetSyncHibp=true` to seed the store directly from the Have I Been Pwned API (using `Sync-HashesFromHibp`). Behind the scenes the helper loads the `LithnetPasswordProtection` module, opens the store, runs [`Import-CompromisedPasswordHashes`](https://docs.lithnet.io/password-protection/advanced-help/powershell-reference/import-compromisedpasswordhashes)/`Import-CompromisedPasswords`/`Import-BannedWords` for each configured file, and then closes the store.
|
||||||
|
|
||||||
S3-compatible usage notes:
|
S3-compatible usage notes:
|
||||||
- No AWS Tools required. The scripts can sign requests using native SigV4 via .NET and HttpClient.
|
- No AWS Tools required. The scripts sign requests using native SigV4 via .NET and HttpClient, including non-default endpoint ports.
|
||||||
- To force using AWS Tools instead, set `s3UseAwsTools = true` in `ElysiumSettings.txt` and install `AWS.Tools.S3`.
|
- To force using AWS Tools instead, set `s3UseAwsTools = true` in `ElysiumSettings.txt` and install `AWS.Tools.S3`.
|
||||||
### Uninstallation
|
### Uninstallation
|
||||||
Run script Elysium.ps1 as an administrator and choose option 4 (Uninstall).
|
Run script Elysium.ps1 as an administrator and choose option 4 (Uninstall).
|
||||||
|
|||||||
22
Settings.ps1
Normal file
22
Settings.ps1
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Settings for Elysium Tool
|
||||||
|
|
||||||
|
# General Settings
|
||||||
|
$Global:ToolRepositoryUrl = "https://example.com/git/elysium.git"
|
||||||
|
|
||||||
|
# KHDB Update Settings
|
||||||
|
$Global:KnownHashesBaseUrl = "https://example.com/known-hashes/"
|
||||||
|
$Global:LocalKnownHashesPath = "C:\Elysium\known-hashes"
|
||||||
|
|
||||||
|
# Test Weak AD Passwords Settings
|
||||||
|
$Global:DomainAdminUsernames = @{
|
||||||
|
"Domain1" = "admin1";
|
||||||
|
"Domain2" = "admin2";
|
||||||
|
# Add more domains and usernames as needed
|
||||||
|
}
|
||||||
|
$Global:PdfReportPath = "C:\Elysium\Reports"
|
||||||
|
|
||||||
|
# Extract and Send Hashes Settings
|
||||||
|
$Global:HashesExportPath = "C:\Elysium\Hashes"
|
||||||
|
$Global:ToolProviderUploadUrl = "https://upload.example.com/hashes"
|
||||||
|
|
||||||
|
# Any additional settings...
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Test-WeakADPasswords.ps1 ##
|
## File: Test-WeakADPasswords.ps1 ##
|
||||||
## Version: 1.3.0 ##
|
## Version: 1.4.0 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
@@ -24,7 +24,8 @@ This script will test the passwords of selected domain (defined in ElysiumSettin
|
|||||||
# Enable verbose output
|
# Enable verbose output
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
$VerbosePreference = "Continue"
|
|
||||||
|
$VerbosePreference = "SilentlyContinue"
|
||||||
|
|
||||||
$scriptRoot = $PSScriptRoot
|
$scriptRoot = $PSScriptRoot
|
||||||
|
|
||||||
@@ -52,6 +53,63 @@ function Start-TestTranscript {
|
|||||||
|
|
||||||
function Stop-TestTranscript { try { Stop-Transcript | Out-Null } catch {} }
|
function Stop-TestTranscript { try { Stop-Transcript | Out-Null } catch {} }
|
||||||
|
|
||||||
|
function Invoke-UsageBeacon {
|
||||||
|
param(
|
||||||
|
[string]$Url,
|
||||||
|
[string]$Method = 'GET',
|
||||||
|
[int]$TimeoutSeconds = 5,
|
||||||
|
[string]$InstanceId
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Url)) { return }
|
||||||
|
|
||||||
|
$normalizedMethod = 'GET'
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($Method)) {
|
||||||
|
$normalizedMethod = $Method.ToUpperInvariant()
|
||||||
|
}
|
||||||
|
if ($normalizedMethod -notin @('GET', 'POST', 'PUT')) {
|
||||||
|
$normalizedMethod = 'GET'
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestParams = @{
|
||||||
|
Uri = $Url
|
||||||
|
Method = $normalizedMethod
|
||||||
|
ErrorAction = 'Stop'
|
||||||
|
}
|
||||||
|
|
||||||
|
$invokeWebRequestCmd = $null
|
||||||
|
try { $invokeWebRequestCmd = Get-Command -Name Invoke-WebRequest -ErrorAction Stop } catch { }
|
||||||
|
if ($invokeWebRequestCmd -and $invokeWebRequestCmd.Parameters.ContainsKey('UseBasicParsing')) {
|
||||||
|
$requestParams['UseBasicParsing'] = $true
|
||||||
|
}
|
||||||
|
if ($TimeoutSeconds -gt 0 -and $invokeWebRequestCmd -and $invokeWebRequestCmd.Parameters.ContainsKey('TimeoutSec')) {
|
||||||
|
$requestParams['TimeoutSec'] = $TimeoutSeconds
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($InstanceId)) {
|
||||||
|
$requestParams['Headers'] = @{ 'X-Elysium-Instance' = $InstanceId }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalizedMethod -in @('POST', 'PUT')) {
|
||||||
|
$payload = [ordered]@{
|
||||||
|
script = 'Test-WeakADPasswords'
|
||||||
|
version = '1.3.3'
|
||||||
|
ranAtUtc = (Get-Date).ToUniversalTime().ToString('o')
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($InstanceId)) {
|
||||||
|
$payload['instanceId'] = $InstanceId
|
||||||
|
}
|
||||||
|
$requestParams['ContentType'] = 'application/json'
|
||||||
|
$requestParams['Body'] = ($payload | ConvertTo-Json -Depth 3 -Compress)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest @requestParams | Out-Null
|
||||||
|
Write-Verbose ("Usage beacon sent via {0}." -f $normalizedMethod)
|
||||||
|
} catch {
|
||||||
|
Write-Verbose ("Usage beacon failed: {0}" -f $_.Exception.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Current timestamp for both report generation and header
|
# Current timestamp for both report generation and header
|
||||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||||
|
|
||||||
@@ -93,13 +151,37 @@ try {
|
|||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$usageBeaconUrl = $ElysiumSettings['UsageBeaconUrl']
|
||||||
|
$usageBeaconMethod = $ElysiumSettings['UsageBeaconMethod']
|
||||||
|
$usageBeaconInstanceId = $ElysiumSettings['UsageBeaconInstanceId']
|
||||||
|
$usageBeaconTimeoutSeconds = $null
|
||||||
|
if ($ElysiumSettings.ContainsKey('UsageBeaconTimeoutSeconds')) {
|
||||||
|
$parsedTimeout = 0
|
||||||
|
if ([int]::TryParse($ElysiumSettings['UsageBeaconTimeoutSeconds'], [ref]$parsedTimeout)) {
|
||||||
|
$usageBeaconTimeoutSeconds = $parsedTimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($usageBeaconUrl)) {
|
||||||
|
$beaconParams = @{ Url = $usageBeaconUrl }
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($usageBeaconMethod)) {
|
||||||
|
$beaconParams['Method'] = $usageBeaconMethod
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($usageBeaconInstanceId)) {
|
||||||
|
$beaconParams['InstanceId'] = $usageBeaconInstanceId
|
||||||
|
}
|
||||||
|
if ($null -ne $usageBeaconTimeoutSeconds) {
|
||||||
|
$beaconParams['TimeoutSeconds'] = $usageBeaconTimeoutSeconds
|
||||||
|
}
|
||||||
|
Invoke-UsageBeacon @beaconParams
|
||||||
|
}
|
||||||
|
|
||||||
# Define the function to extract domain details from settings
|
# Define the function to extract domain details from settings
|
||||||
function Get-DomainDetailsFromSettings {
|
function Get-DomainDetailsFromSettings {
|
||||||
param (
|
param (
|
||||||
[hashtable]$Settings
|
[hashtable]$Settings
|
||||||
)
|
)
|
||||||
|
|
||||||
$domainDetails = @{}
|
$domainDetails = [ordered]@{}
|
||||||
$counter = 1
|
$counter = 1
|
||||||
while ($true) {
|
while ($true) {
|
||||||
$nameKey = "Domain${counter}Name"
|
$nameKey = "Domain${counter}Name"
|
||||||
@@ -330,7 +412,7 @@ function Test-WeakADPasswords {
|
|||||||
|
|
||||||
# User selects a domain
|
# User selects a domain
|
||||||
Write-Host "Select a domain to test:"
|
Write-Host "Select a domain to test:"
|
||||||
$DomainDetails.GetEnumerator() | ForEach-Object { Write-Host "$($_.Key): $($_.Value.Name)" }
|
$DomainDetails.GetEnumerator() | Sort-Object { [int]$_.Key } | ForEach-Object { Write-Host "$($_.Key): $($_.Value.Name)" }
|
||||||
$selection = Read-Host "Enter the number of the domain"
|
$selection = Read-Host "Enter the number of the domain"
|
||||||
|
|
||||||
if (-not ($DomainDetails.ContainsKey($selection))) {
|
if (-not ($DomainDetails.ContainsKey($selection))) {
|
||||||
@@ -355,7 +437,7 @@ function Test-WeakADPasswords {
|
|||||||
if ($_.PSObject.Properties.Name -contains 'Enabled') { $_.Enabled } else { $true }
|
if ($_.PSObject.Properties.Name -contains 'Enabled') { $_.Enabled } else { $true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$testResults = $accounts | Test-PasswordQuality -WeakPasswordHashesFile $FilePath
|
$testResults = $accounts | Test-PasswordQuality -WeakPasswordHashesSortedFile $FilePath
|
||||||
Write-Verbose "Password quality test completed."
|
Write-Verbose "Password quality test completed."
|
||||||
} catch {
|
} catch {
|
||||||
Write-Error ("An error occurred while testing passwords: {0}" -f $_.Exception.Message)
|
Write-Error ("An error occurred while testing passwords: {0}" -f $_.Exception.Message)
|
||||||
@@ -365,40 +447,62 @@ function Test-WeakADPasswords {
|
|||||||
# Report generation with dynamic content and UPNs
|
# Report generation with dynamic content and UPNs
|
||||||
$reportPath = Join-Path -Path $reportPathBase -ChildPath "$($selectedDomain.Name)_WeakPasswordReport_$timestamp.txt"
|
$reportPath = Join-Path -Path $reportPathBase -ChildPath "$($selectedDomain.Name)_WeakPasswordReport_$timestamp.txt"
|
||||||
$upnOnlyReportPath = Join-Path -Path $reportPathBase -ChildPath "$($selectedDomain.Name)_DictionaryPasswordUPNs_$timestamp.txt"
|
$upnOnlyReportPath = Join-Path -Path $reportPathBase -ChildPath "$($selectedDomain.Name)_DictionaryPasswordUPNs_$timestamp.txt"
|
||||||
|
|
||||||
Write-Verbose "Generating report at $reportPath"
|
# Build a lookup of SAM account names to UPNs for dictionary hits by leveraging structured results
|
||||||
$reportContent = @($header, ($testResults | Out-String).Trim(), $footer) -join "`r`n"
|
$dictionaryLogonNames = @()
|
||||||
|
foreach ($result in @($testResults)) {
|
||||||
$lines = $reportContent -split "`r`n"
|
if ($null -ne $result -and $null -ne $result.WeakPassword) {
|
||||||
$newReportContent = @()
|
$dictionaryLogonNames += $result.WeakPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$dictionaryLogonNames = $dictionaryLogonNames | Sort-Object -Unique
|
||||||
|
|
||||||
|
$dictionarySamToUpn = @{}
|
||||||
$upnReportContent = @()
|
$upnReportContent = @()
|
||||||
|
|
||||||
|
foreach ($logonName in $dictionaryLogonNames) {
|
||||||
|
$samAccountName = $logonName -replace '^.*\\', ''
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($samAccountName) -and -not $dictionarySamToUpn.ContainsKey($samAccountName)) {
|
||||||
|
Write-Verbose "Looking up UPN for $samAccountName (dictionary hit)"
|
||||||
|
$upn = Get-UserUPN -SamAccountName $samAccountName -Domain $selectedDomain.DC -Credential $credential
|
||||||
|
$dictionarySamToUpn[$samAccountName] = $upn
|
||||||
|
if ($upn -ne "UPN not found") {
|
||||||
|
$upnReportContent += $upn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Generating report at $reportPath"
|
||||||
|
$reportContent = @($header, ($testResults | Out-String).Trim(), $footer) -join "`r`n"
|
||||||
|
|
||||||
|
$lines = $reportContent -split "`r`n"
|
||||||
|
$newReportContent = @()
|
||||||
$collectingUPNs = $false
|
$collectingUPNs = $false
|
||||||
|
|
||||||
foreach ($line in $lines) {
|
foreach ($line in $lines) {
|
||||||
$newReportContent += $line
|
$newReportContent += $line
|
||||||
|
|
||||||
# Start collecting UPNs after detecting the relevant section in the report
|
|
||||||
if ($line -match "Passwords of these accounts have been found in the dictionary:") {
|
if ($line -match "Passwords of these accounts have been found in the dictionary:") {
|
||||||
$collectingUPNs = $true
|
$collectingUPNs = $true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stop collecting UPNs if a new section starts or end of section is detected
|
if ($collectingUPNs) {
|
||||||
if ($collectingUPNs -and $line -match "^\s*$") {
|
if ($line -match '^\s*$') { continue }
|
||||||
$collectingUPNs = $false
|
if ($line -match '^\s*-{2,}') { continue }
|
||||||
}
|
if ($line -match '^\s*(SamAccountName|LogonName)\b') { continue }
|
||||||
|
if ($line -match '^[^\s].*:\s*$' -and $line -notmatch 'dictionary') {
|
||||||
|
$collectingUPNs = $false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
# Regex to match the SAMAccountName from the report line and collect UPNs if in the target section
|
$firstToken = ($line.Trim() -split '\s+')[0]
|
||||||
if ($collectingUPNs -and $line -match "^\s*(\S+)\s*$") {
|
if (-not [string]::IsNullOrWhiteSpace($firstToken)) {
|
||||||
$samAccountName = $matches[1]
|
$samAccountName = $firstToken -replace '^.*\\', ''
|
||||||
Write-Verbose "Looking up UPN for $samAccountName"
|
if ($dictionarySamToUpn.ContainsKey($samAccountName)) {
|
||||||
$upn = Get-UserUPN -SamAccountName $samAccountName -Domain $selectedDomain.DC -Credential $credential
|
$upnValue = $dictionarySamToUpn[$samAccountName]
|
||||||
$newReportContent += " UPN: $upn"
|
$newReportContent += " UPN: $upnValue"
|
||||||
|
}
|
||||||
# Collect UPNs only for accounts found in the dictionary section
|
|
||||||
if ($upn -ne "UPN not found") {
|
|
||||||
$upnReportContent += $upn
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Uninstall.ps1 ##
|
## File: Uninstall.ps1 ##
|
||||||
## Version: 1.1.0 ##
|
## Version: 1.2.0 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
@@ -19,6 +19,13 @@ Uninstall script for the Elysium AD password testing tool.
|
|||||||
This script will remove the Elysium tool and its components (scripts, configurations, and any generated data) from the system, and then delete itself.
|
This script will remove the Elysium tool and its components (scripts, configurations, and any generated data) from the system, and then delete itself.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
[string]$commonHelper = Join-Path -Path $PSScriptRoot -ChildPath 'Elysium.Common.ps1'
|
||||||
|
if (-not (Test-Path -LiteralPath $commonHelper)) { throw "Common helper not found at $commonHelper" }
|
||||||
|
. $commonHelper
|
||||||
|
Restart-WithPwshIfAvailable -BoundParameters $PSBoundParameters -UnboundArguments $MyInvocation.UnboundArguments
|
||||||
|
|
||||||
function Start-UninstallTranscript {
|
function Start-UninstallTranscript {
|
||||||
try {
|
try {
|
||||||
$base = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'Elysium', 'logs')
|
$base = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'Elysium', 'logs')
|
||||||
|
|||||||
847
Update-KHDB.ps1
847
Update-KHDB.ps1
File diff suppressed because it is too large
Load Diff
167
Update-LithnetStore.ps1
Normal file
167
Update-LithnetStore.ps1
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
##################################################
|
||||||
|
## ____ ___ ____ _____ _ _ _____ _____ ##
|
||||||
|
## / ___/ _ \| _ \| ____| | \ | | ____|_ _| ##
|
||||||
|
## | | | | | | |_) | _| | \| | _| | | ##
|
||||||
|
## | |__| |_| | _ <| |___ _| |\ | |___ | | ##
|
||||||
|
## \____\__\_\_| \_\_____(_)_| \_|_____| |_| ##
|
||||||
|
##################################################
|
||||||
|
## Project: Elysium ##
|
||||||
|
## File: Update-LithnetStore.ps1 ##
|
||||||
|
## Version: 1.1.0 ##
|
||||||
|
## Support: support@cqre.net ##
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Populates a Lithnet Password Protection store with compromised passwords and banned words.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Reads configuration from ElysiumSettings.txt (or a provided settings file), opens the Lithnet
|
||||||
|
Password Protection store, optionally synchronizes with Have I Been Pwned, imports local NTLM
|
||||||
|
hash lists, plaintext password lists, and banned-word files.
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$SettingsPath,
|
||||||
|
[string[]]$HashFiles,
|
||||||
|
[string[]]$PlaintextFiles,
|
||||||
|
[string[]]$BannedWordFiles,
|
||||||
|
[switch]$SkipHibpSync
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
[string]$commonHelper = Join-Path -Path $PSScriptRoot -ChildPath 'Elysium.Common.ps1'
|
||||||
|
if (-not (Test-Path -LiteralPath $commonHelper)) { throw "Common helper not found at $commonHelper" }
|
||||||
|
. $commonHelper
|
||||||
|
Restart-WithPwshIfAvailable -BoundParameters $PSBoundParameters -UnboundArguments $MyInvocation.UnboundArguments
|
||||||
|
|
||||||
|
function Read-KeyValueSettings {
|
||||||
|
param([string]$Path)
|
||||||
|
$result = @{}
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) { throw "Settings file not found at $Path" }
|
||||||
|
Get-Content -LiteralPath $Path | ForEach-Object {
|
||||||
|
$line = $_
|
||||||
|
if (-not $line) { return }
|
||||||
|
$trimmed = $line.Trim()
|
||||||
|
if (-not $trimmed) { return }
|
||||||
|
if ($trimmed.StartsWith('#')) { return }
|
||||||
|
$kv = $line -split '=', 2
|
||||||
|
if ($kv.Count -ne 2) { return }
|
||||||
|
$key = $kv[0].Trim()
|
||||||
|
$value = $kv[1].Trim()
|
||||||
|
if (-not $key) { return }
|
||||||
|
if ($value.StartsWith("'") -and $value.EndsWith("'") -and $value.Length -ge 2) {
|
||||||
|
$value = $value.Substring(1, $value.Length - 2)
|
||||||
|
}
|
||||||
|
$result[$key] = $value
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-BooleanSetting {
|
||||||
|
param(
|
||||||
|
[string]$Value,
|
||||||
|
[bool]$Default = $false
|
||||||
|
)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Value)) { return $Default }
|
||||||
|
try { return [System.Convert]::ToBoolean($Value) } catch { return $Default }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ListFromSetting {
|
||||||
|
param([string]$Value)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Value)) { return @() }
|
||||||
|
return ($Value -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExistingPath {
|
||||||
|
param([string]$PathValue, [string]$Description)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($PathValue)) { throw "$Description path was not provided." }
|
||||||
|
if (-not (Test-Path -LiteralPath $PathValue)) {
|
||||||
|
throw "$Description not found at '$PathValue'."
|
||||||
|
}
|
||||||
|
return (Resolve-Path -LiteralPath $PathValue).Path
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SettingsPath) {
|
||||||
|
$SettingsPath = Join-Path -Path $PSScriptRoot -ChildPath 'ElysiumSettings.txt'
|
||||||
|
}
|
||||||
|
$settings = Read-KeyValueSettings -Path $SettingsPath
|
||||||
|
|
||||||
|
$storePathSetting = $settings['LithnetStorePath']
|
||||||
|
$storePath = Resolve-ExistingPath -PathValue $storePathSetting -Description 'Lithnet store'
|
||||||
|
|
||||||
|
$settingsHashSources = Get-ListFromSetting -Value $settings['LithnetHashSources']
|
||||||
|
$settingsPlainSources = Get-ListFromSetting -Value $settings['LithnetPlaintextSources']
|
||||||
|
$settingsBannedSources = Get-ListFromSetting -Value $settings['LithnetBannedWordSources']
|
||||||
|
|
||||||
|
$hashSourcePaths = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
foreach ($path in @($HashFiles) + $settingsHashSources) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($path)) { continue }
|
||||||
|
$resolved = Resolve-ExistingPath -PathValue $path -Description 'Hash list'
|
||||||
|
[void]$hashSourcePaths.Add($resolved)
|
||||||
|
}
|
||||||
|
if ($hashSourcePaths.Count -eq 0) {
|
||||||
|
$defaultKhdb = Join-Path -Path $PSScriptRoot -ChildPath 'khdb.txt'
|
||||||
|
if (Test-Path -LiteralPath $defaultKhdb) {
|
||||||
|
[void]$hashSourcePaths.Add((Resolve-Path -LiteralPath $defaultKhdb).Path)
|
||||||
|
} else {
|
||||||
|
throw 'No hash files were provided via parameters or LithnetHashSources, and khdb.txt was not found.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$plaintextSourcePaths = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
foreach ($path in @($PlaintextFiles) + $settingsPlainSources) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($path)) { continue }
|
||||||
|
$resolved = Resolve-ExistingPath -PathValue $path -Description 'Plaintext password list'
|
||||||
|
[void]$plaintextSourcePaths.Add($resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
$bannedWordSourcePaths = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
foreach ($path in @($BannedWordFiles) + $settingsBannedSources) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($path)) { continue }
|
||||||
|
$resolved = Resolve-ExistingPath -PathValue $path -Description 'Banned word list'
|
||||||
|
[void]$bannedWordSourcePaths.Add($resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
$syncHibp = Get-BooleanSetting -Value $settings['LithnetSyncHibp'] -Default:$false
|
||||||
|
if ($SkipHibpSync) { $syncHibp = $false }
|
||||||
|
|
||||||
|
Write-Host "Importing LithnetPasswordProtection module..."
|
||||||
|
try {
|
||||||
|
Import-Module -Name LithnetPasswordProtection -ErrorAction Stop | Out-Null
|
||||||
|
} catch {
|
||||||
|
throw "LithnetPasswordProtection module not found: $($_.Exception.Message). Install it from https://github.com/lithnet/password-protection."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ("Opening Lithnet store at '{0}'..." -f $storePath)
|
||||||
|
Open-Store -Path $storePath
|
||||||
|
$storeOpened = $true
|
||||||
|
try {
|
||||||
|
if ($syncHibp) {
|
||||||
|
Write-Host 'Synchronizing compromised hashes from Have I Been Pwned (this can take a while)...'
|
||||||
|
Sync-HashesFromHibp
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($hashFile in ($hashSourcePaths.ToArray() | Sort-Object)) {
|
||||||
|
Write-Host ("Importing NTLM hash list '{0}'..." -f $hashFile)
|
||||||
|
Import-CompromisedPasswordHashes -Filename $hashFile
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($plainFile in ($plaintextSourcePaths.ToArray() | Sort-Object)) {
|
||||||
|
Write-Host ("Importing plaintext password list '{0}'..." -f $plainFile)
|
||||||
|
Import-CompromisedPasswords -Filename $plainFile
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($bannedFile in ($bannedWordSourcePaths.ToArray() | Sort-Object)) {
|
||||||
|
Write-Host ("Importing banned word list '{0}'..." -f $bannedFile)
|
||||||
|
Import-BannedWords -Filename $bannedFile
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host 'Lithnet store update completed successfully.'
|
||||||
|
} finally {
|
||||||
|
if ($storeOpened) {
|
||||||
|
try { Close-Store } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user