New updates

This commit is contained in:
2025-10-10 15:09:33 +02:00
parent 76c9fcfb61
commit aa54c751c3
7 changed files with 436 additions and 207 deletions

View File

@@ -1,5 +1,52 @@
# Changelog
## 2025-10-10
### Test-WeakADPasswords.ps1 v1.3.0
Added:
- `CheckOnlyEnabledUsers` flag wired from settings to filter accounts prior to `Test-PasswordQuality`.
- Transcript logging to `Reports/logs/test-weakad-<timestamp>.log`.
### Extract-NTHashes.ps1 v1.2.0
Added:
- Transcript logging to `Reports/logs/extract-hashes-<timestamp>.log`.
### Elysium.ps1 v1.1.0
Updated:
- Added strict error handling (`$ErrorActionPreference='Stop'`) and `Set-StrictMode`.
- Resolved script invocations via `$PSScriptRoot` to avoid CWD issues.
### Update-KHDB.ps1 v1.1.0
Added/Updated:
- Robust settings validation and SAS token normalization.
- Safe URL construction with `UriBuilder` and custom User-Agent.
- TLS 1.2 enforced; `HttpClient` timeout and retry with backoff for transient errors.
- Download progress for both known and unknown content length.
- Atomic-ish update: download to temp, extract, validate, backup existing `khdb.txt`, then replace.
- KHDB validation: format check (32-hex), deduplication and normalization.
- Transcript logging to `Reports/logs/update-khdb-<timestamp>.log`.
### Test-WeakADPasswords.ps1 v1.2.0
Updated:
- Enforced modules via `#Requires`; removed runtime installs.
- Added strict mode and error preference.
- Resolved paths relative to `$PSScriptRoot` (settings, KHDB, reports).
- Ensured report directory creation and sane defaults (`Reports`).
- Removed stray top-level loop; UPN enrichment occurs during report generation only.
### Extract-NTHashes.ps1 v1.1.0
Updated:
- Enforced modules via `#Requires`; added strict mode.
- Fixed variable ordering bug and unified filename scheme with domain prefix.
- Implemented PBKDF2 (HMAC-SHA256, 100k iterations) + random salt for AES-256-CBC encryption; header `ELY1|salt|iv`.
- Normalized SAS token and verified container existence; checksum verified before cleanup; artifacts retained on failure.
- Paths resolved relative to `$PSScriptRoot`; ensured report base directory exists.
### ElysiumSettings.txt.sample v1.1.0
Updated:
- `ReportPathBase` default changed to `Reports` (relative) and added guidance on required modules and replication rights.
- Added optional `CheckOnlyEnabledUsers=true` example flag.
## Extract-NTHashes.ps1
### version 1.1.1

View File

@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Elysium.ps1 ##
## Version: 1.0 ##
## Version: 1.1.0 ##
## Support: support@cqre.net ##
##################################################
@@ -24,8 +24,12 @@ Elysium.ps1 offers a menu to perform various actions:
5. Exit
#>
# Safer defaults
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
# Define the path to the settings file
$settingsFilePath = "ElysiumSettings.txt"
$settingsFilePath = Join-Path -Path $PSScriptRoot -ChildPath "ElysiumSettings.txt"
# Check if the settings file exists
if (-Not (Test-Path $settingsFilePath)) {
@@ -69,19 +73,19 @@ do {
switch ($userSelection) {
'1' {
Write-Host "Downloading KHDB..."
.\Update-KHDB.ps1
& (Join-Path -Path $PSScriptRoot -ChildPath 'Update-KHDB.ps1')
}
'2' {
Write-Host "Testing Weak AD Passwords..."
.\Test-WeakADPasswords.ps1
& (Join-Path -Path $PSScriptRoot -ChildPath 'Test-WeakADPasswords.ps1')
}
'3' {
Write-Host "Extracting and Sending Current Hashes..."
.\Extract-NTHashes.ps1
& (Join-Path -Path $PSScriptRoot -ChildPath 'Extract-NTHashes.ps1')
}
'4' {
Write-Host "Uninstalling..."
.\Uninstall.ps1
& (Join-Path -Path $PSScriptRoot -ChildPath 'Uninstall.ps1')
}
'5' {
Write-Host "Exiting..."

View File

@@ -8,7 +8,7 @@
##################################################
## Project: Elysium ##
## File: ElysiumSettings.txt ##
## Version: 1.0.0 ##
## Version: 1.1.0 ##
## Support: support@cqre.net ##
##################################################
@@ -21,9 +21,14 @@ sasToken =
# Application Settings
######################
InstallationPath=
ReportPathBase=/Reports
ReportPathBase=Reports
WeakPasswordsDatabase=khdb.txt
# TODO CheckOnlyEnabledUsers=true
# CheckOnlyEnabledUsers=true
# Notes:
# - Required PowerShell modules: DSInternals, ActiveDirectory, Az.Storage (for upload).
# - AD account permissions: Replication Directory Changes and Replication Directory Changes All
# on the domain (DCSync-equivalent) are sufficient; full Domain Admin not required.
# Domain Settings
#################

View File

@@ -7,11 +7,12 @@
##################################################
## Project: Elysium ##
## File: Extract-NTLMHashes.ps1 ##
## Version: 1.0.2 ##
## Version: 1.2.0 ##
## Support: support@cqre.net ##
##################################################
<#
#Requires -Modules DSInternals, Az.Storage
.SYNOPSIS
Script for extracting NTLM hashes from live AD for further analysis.
@@ -19,10 +20,31 @@ Script for extracting NTLM hashes from live AD for further analysis.
This script will connect to selected domain (defined in ElysiumSettings.txt) using account with AD replication privileges and extract NTLM hashes from all active accounts. It will then compress and encrypt the resulting file, uploads it to designated Azure Storage account, checks for validity and then deletes everything. The hashes are extracted without usernames to minimise the sensitivity of the operation. Encryption is done with AES and passphrase that was defined in environment variable during first run.
#>
# Import settings
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$scriptRoot = $PSScriptRoot
function Start-ExtractTranscript {
param([string]$BasePath)
try {
$logsDir = Join-Path -Path $BasePath -ChildPath 'Reports/logs'
if (-not (Test-Path $logsDir)) { New-Item -Path $logsDir -ItemType Directory -Force | Out-Null }
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
$logPath = Join-Path -Path $logsDir -ChildPath "extract-hashes-$ts.log"
Start-Transcript -Path $logPath -Force | Out-Null
} catch {
Write-Warning "Could not start transcript: $($_.Exception.Message)"
}
}
function Stop-ExtractTranscript { try { Stop-Transcript | Out-Null } catch {} }
Start-ExtractTranscript -BasePath $scriptRoot
try {
# Import settings
Write-Host "Loading settings..."
$ElysiumSettings = @{}
$settingsPath = "ElysiumSettings.txt"
$settingsPath = Join-Path -Path $scriptRoot -ChildPath "ElysiumSettings.txt"
if (-not (Test-Path $settingsPath)) {
Write-Error "Settings file not found at $settingsPath"
@@ -38,17 +60,12 @@ Get-Content $settingsPath | ForEach-Object {
}
}
# Ensure DSInternals and Az.Storage are installed
$requiredModules = @('DSInternals', 'Az.Storage')
foreach ($module in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $module)) {
Write-Host "Installing $module module..."
Install-Module $module -Scope CurrentUser -Force
}
Import-Module $module
function Normalize-ReportPath([string]$p) {
if ([string]::IsNullOrWhiteSpace($p)) { return (Join-Path -Path $scriptRoot -ChildPath 'Reports') }
if ([System.IO.Path]::IsPathRooted($p)) { return $p }
return (Join-Path -Path $scriptRoot -ChildPath $p)
}
# Script variables
# External settings
$storageAccountName = $ElysiumSettings['storageAccountName']
$containerName = $ElysiumSettings['containerName']
@@ -56,14 +73,10 @@ $sasToken = $ElysiumSettings['sasToken']
# Retrieve the passphrase from a user environment variable
$passphrase = [System.Environment]::GetEnvironmentVariable("ELYSIUM_PASSPHRASE", [System.EnvironmentVariableTarget]::User)
if ([string]::IsNullOrWhiteSpace($passphrase)) { Write-Error 'Passphrase not found in ELYSIUM_PASSPHRASE environment variable.'; exit }
# Dynamic variables
# Timestamp
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$domainPrefix = $selectedDomain.Name -replace "\W", "_" # Replace non-alphanumeric characters to ensure a valid file name
$exportPath = ".\${domainPrefix}_NTLM_Hashes_$timestamp.txt"
$compressedFilePath = ".\${domainPrefix}_NTLM_Hashes_$timestamp.zip"
$encryptedFilePath = ".\${domainPrefix}_NTLM_Hashes_$timestamp.enc"
$blobName = "${domainPrefix}_NTLM_Hashes_$timestamp.enc"
function Protect-FileWithAES {
param (
@@ -77,41 +90,49 @@ function Protect-FileWithAES {
[string]$Passphrase
)
$aes = New-Object System.Security.Cryptography.AesManaged
# Derive key with PBKDF2 (HMACSHA256) + random salt
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$salt = New-Object byte[] 16
$rng.GetBytes($salt)
$kdf = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Passphrase, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA256)
$key = $kdf.GetBytes(32)
$aes = [System.Security.Cryptography.Aes]::Create()
$aes.KeySize = 256
$aes.BlockSize = 128
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
# Generate key from passphrase
$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($Passphrase)
$aes.Key = [System.Security.Cryptography.SHA256]::Create().ComputeHash($passwordBytes)
# Generate a random IV
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$aes.GenerateIV()
$encryptor = $aes.CreateEncryptor($aes.Key, $aes.IV)
$iv = $aes.IV
$encryptor = $aes.CreateEncryptor($key, $iv)
$fileStream = [System.IO.File]::Open($InputFile, [System.IO.FileMode]::Open)
$fileStream = [System.IO.File]::Open($InputFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
$outFileStream = [System.IO.File]::Create($OutputFile)
# Write the IV at the beginning of the output file
$outFileStream.Write($aes.IV, 0, $aes.IV.Length)
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($outFileStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
try {
$buffer = New-Object Byte[] 8192
while (($read = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$cryptoStream.Write($buffer, 0, $read)
# File header: magic 'ELY1' (4 bytes), salt (16 bytes), IV (16 bytes)
$magic = [System.Text.Encoding]::ASCII.GetBytes('ELY1')
$outFileStream.Write($magic, 0, $magic.Length)
$outFileStream.Write($salt, 0, $salt.Length)
$outFileStream.Write($iv, 0, $iv.Length)
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($outFileStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
try {
$buffer = New-Object Byte[] 8192
while (($read = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$cryptoStream.Write($buffer, 0, $read)
}
} finally {
$cryptoStream.FlushFinalBlock()
$cryptoStream.Close()
}
}
finally {
$cryptoStream.Close()
$outFileStream.Close()
$fileStream.Close()
$aes.Clear()
} finally {
$outFileStream.Close(); $fileStream.Close(); $aes.Dispose(); $rng.Dispose(); $kdf.Dispose()
}
Write-Host "File has been encrypted: $OutputFile"
Write-Host "File has been encrypted (PBKDF2+AES-256-CBC): $OutputFile"
}
function Get-FileChecksum {
param (
@@ -131,8 +152,8 @@ function Get-FileChecksum {
}
# Extract NTLM hashes
$exportPath = ".\NTLM_Hashes_$timestamp.txt"
$compressedFilePath = ".\NTLM_Hashes_$timestamp.zip"
$reportBase = Normalize-ReportPath -p $ElysiumSettings['ReportPathBase']
if (-not (Test-Path $reportBase)) { New-Item -Path $reportBase -ItemType Directory -Force | Out-Null }
# Build domain details from settings
$DomainDetails = @{}
@@ -159,6 +180,13 @@ if (-not $selectedDomain) {
$domainController = $selectedDomain.DC
$credential = Get-Credential -Message "Enter AD credentials with replication rights for $($selectedDomain.Name)"
$domainPrefix = ($selectedDomain.Name -replace "\W", "_")
$baseName = "${domainPrefix}_NTLM_Hashes_$timestamp"
$exportPath = Join-Path -Path $scriptRoot -ChildPath "$baseName.txt"
$compressedFilePath = Join-Path -Path $scriptRoot -ChildPath "$baseName.zip"
$encryptedFilePath = Join-Path -Path $scriptRoot -ChildPath "$baseName.enc"
$blobName = "$baseName.enc"
$ntlmHashes = Get-ADReplAccount -All -Server $domainController -Credential $credential |
Where-Object { $_.NTHash } |
ForEach-Object { [BitConverter]::ToString($_.NTHash).Replace("-", "") } |
@@ -179,15 +207,22 @@ Write-Host "File has been encrypted: $encryptedFilePath"
$localFileChecksum = Get-FileChecksum -Path $encryptedFilePath
# Create the context for Azure Blob Storage with SAS token
$storageContext = New-AzStorageContext -StorageAccountName $storageAccountName -SasToken "$sasToken"
$sas = $sasToken
if ([string]::IsNullOrWhiteSpace($sas)) { Write-Error 'sasToken is missing in settings.'; exit }
$sas = $sas.Trim(); if (-not $sas.StartsWith('?')) { $sas = '?' + $sas }
$storageContext = New-AzStorageContext -StorageAccountName $storageAccountName -SasToken $sas
# Ensure container exists
$container = Get-AzStorageContainer -Name $containerName -Context $storageContext -ErrorAction SilentlyContinue
if (-not $container) { Write-Error "Azure container '$containerName' not found or access denied."; exit }
# Upload the encrypted file to Azure Blob Storage
Set-AzStorageBlobContent -File $encryptedFilePath -Container $containerName -Blob $blobName -Context $storageContext
Set-AzStorageBlobContent -File $encryptedFilePath -Container $containerName -Blob $blobName -Context $storageContext | Out-Null
Write-Host "Encrypted file uploaded to Azure Blob Storage: $blobName"
# Download the blob to a temporary location to verify
$tempDownloadPath = [System.IO.Path]::GetTempFileName()
Get-AzStorageBlobContent -Blob $blobName -Container $containerName -Context $storageContext -Destination $tempDownloadPath -Force
Get-AzStorageBlobContent -Blob $blobName -Container $containerName -Context $storageContext -Destination $tempDownloadPath -Force | Out-Null
# Calculate the downloaded file checksum
$downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
@@ -195,13 +230,16 @@ $downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
# Compare the checksums
if ($localFileChecksum -eq $downloadedFileChecksum) {
Write-Host "The file was correctly uploaded. Checksum verified."
# Clean up local and temporary files only on success
Remove-Item -Path $exportPath, $compressedFilePath, $encryptedFilePath, $tempDownloadPath -Force
Write-Host "Local and temporary files cleaned up after uploading to Azure Blob Storage."
}
else {
Write-Host "Checksum verification failed. The uploaded file may be corrupted."
Write-Warning "Checksum verification failed. Keeping local artifacts for investigation: $exportPath, $compressedFilePath, $encryptedFilePath"
if (Test-Path $tempDownloadPath) { Remove-Item -Path $tempDownloadPath -Force }
}
# Clean up local and temporary files
Remove-Item -Path $exportPath, $compressedFilePath, $encryptedFilePath, $tempDownloadPath -Force
Write-Host "Local and temporary files cleaned up after uploading to Azure Blob Storage."
Write-Host "Script execution completed."
Write-Host "Script execution completed."
} finally {
Stop-ExtractTranscript
}

View File

@@ -8,11 +8,12 @@
##################################################
## Project: Elysium ##
## File: Test-WeakADPasswords.ps1 ##
## Version: 1.1.1 ##
## Version: 1.3.0 ##
## Support: support@cqre.net ##
##################################################
<#
#Requires -Modules DSInternals, ActiveDirectory
.SYNOPSIS
Weak AD password finder component of Elysium tool.
@@ -21,8 +22,27 @@ This script will test the passwords of selected domain (defined in ElysiumSettin
#>
# Enable verbose output
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$VerbosePreference = "Continue"
$scriptRoot = $PSScriptRoot
function Start-TestTranscript {
param([string]$BasePath)
try {
$logsDir = Join-Path -Path $BasePath -ChildPath 'Reports/logs'
if (-not (Test-Path $logsDir)) { New-Item -Path $logsDir -ItemType Directory -Force | Out-Null }
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
$logPath = Join-Path -Path $logsDir -ChildPath "test-weakad-$ts.log"
Start-Transcript -Path $logPath -Force | Out-Null
} catch {
Write-Warning "Could not start transcript: $($_.Exception.Message)"
}
}
function Stop-TestTranscript { try { Stop-Transcript | Out-Null } catch {} }
# Current timestamp for both report generation and header
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
@@ -35,32 +55,34 @@ Report Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
"@
$footer = "`r`n==== End of Report ===="
# Import settings
Write-Verbose "Loading settings..."
$ElysiumSettings = @{}
$settingsPath = "ElysiumSettings.txt"
# Ensure the settings file exists
if (-not (Test-Path $settingsPath)) {
Write-Error "Settings file not found at $settingsPath"
exit
}
# Load settings from file
Start-TestTranscript -BasePath $scriptRoot
try {
Get-Content $settingsPath | ForEach-Object {
if (-not [string]::IsNullOrWhiteSpace($_) -and -not $_.StartsWith("#")) {
$keyValue = $_ -split '=', 2
if ($keyValue.Count -eq 2) {
$ElysiumSettings[$keyValue[0].Trim()] = $keyValue[1].Trim()
# Import settings
Write-Verbose "Loading settings..."
$ElysiumSettings = @{}
$settingsPath = Join-Path -Path $scriptRoot -ChildPath "ElysiumSettings.txt"
# Ensure the settings file exists
if (-not (Test-Path $settingsPath)) {
Write-Error "Settings file not found at $settingsPath"
exit
}
# Load settings from file
try {
Get-Content $settingsPath | ForEach-Object {
if (-not [string]::IsNullOrWhiteSpace($_) -and -not $_.StartsWith("#")) {
$keyValue = $_ -split '=', 2
if ($keyValue.Count -eq 2) {
$ElysiumSettings[$keyValue[0].Trim()] = $keyValue[1].Trim()
}
}
}
Write-Verbose "Settings loaded successfully."
} catch {
Write-Error ("An error occurred while loading settings: {0}" -f $_.Exception.Message)
exit
}
Write-Verbose "Settings loaded successfully."
} catch {
Write-Error ("An error occurred while loading settings: {0}" -f $_.Exception.Message)
exit
}
# Define the function to extract domain details from settings
function Get-DomainDetailsFromSettings {
@@ -91,44 +113,28 @@ function Get-DomainDetailsFromSettings {
$domainDetails = Get-DomainDetailsFromSettings -Settings $ElysiumSettings
Write-Verbose ("Domain details extracted: {0}" -f ($domainDetails | ConvertTo-Json))
# Required modules
$requiredModules = @("DSInternals", "ActiveDirectory")
# Modules are required via #Requires; PowerShell will stop early if missing.
# Check each required module and import
foreach ($module in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $module)) {
Write-Verbose "Required module '$module' is not installed."
$response = Read-Host "Would you like to install it? (Y/N)"
if ($response -eq 'Y') {
try {
Install-Module -Name $module -Force -ErrorAction Stop
Write-Verbose "Module '$module' installed successfully."
} catch {
Write-Error ("Failed to install module '{0}': {1}" -f $module, $_.Exception.Message)
exit
}
} else {
Write-Error "Required module '$module' is not installed. Please install it to proceed."
exit
}
}
Import-Module $module
Write-Verbose "Module '$module' imported."
}
# Resolve KHDB path with fallbacks
$installationPath = $ElysiumSettings["InstallationPath"]
if ([string]::IsNullOrWhiteSpace($installationPath)) { $installationPath = $scriptRoot }
elseif (-not [System.IO.Path]::IsPathRooted($installationPath)) { $installationPath = Join-Path -Path $scriptRoot -ChildPath $installationPath }
# Verify the existence of the Weak Password Hashes file
$WeakHashesSortedFilePath = Join-Path -Path $ElysiumSettings["InstallationPath"] -ChildPath $ElysiumSettings["WeakPasswordsDatabase"]
$khdbName = if ([string]::IsNullOrWhiteSpace($ElysiumSettings["WeakPasswordsDatabase"])) { 'khdb.txt' } else { $ElysiumSettings["WeakPasswordsDatabase"] }
$WeakHashesSortedFilePath = Join-Path -Path $installationPath -ChildPath $khdbName
if (-not (Test-Path $WeakHashesSortedFilePath)) {
Write-Error "Weak password hashes file not found at '$WeakHashesSortedFilePath'."
exit
}
Write-Verbose "Weak password hashes file found at '$WeakHashesSortedFilePath'."
# Ensure the report directory exists
# Ensure the report directory exists (relative paths resolved against script root)
$reportPathBase = $ElysiumSettings["ReportPathBase"]
if ([string]::IsNullOrWhiteSpace($reportPathBase)) { $reportPathBase = 'Reports' }
if (-not [System.IO.Path]::IsPathRooted($reportPathBase)) { $reportPathBase = Join-Path -Path $scriptRoot -ChildPath $reportPathBase }
if (-not (Test-Path -Path $reportPathBase)) {
try {
New-Item -Path $reportPathBase -ItemType Directory -ErrorAction Stop
New-Item -Path $reportPathBase -ItemType Directory -ErrorAction Stop | Out-Null
Write-Verbose "Report directory created at '$reportPathBase'."
} catch {
Write-Error ("Failed to create report directory: {0}" -f $_.Exception.Message)
@@ -136,6 +142,12 @@ if (-not (Test-Path -Path $reportPathBase)) {
}
}
# Read filtering flag (defaults to false)
$checkOnlyEnabledUsers = $false
if ($ElysiumSettings.ContainsKey('CheckOnlyEnabledUsers')) {
try { $checkOnlyEnabledUsers = [System.Convert]::ToBoolean($ElysiumSettings['CheckOnlyEnabledUsers']) } catch { $checkOnlyEnabledUsers = $false }
}
# Function to get UPN for a given SAM account name
function Get-UserUPN {
param (
@@ -158,25 +170,14 @@ function Get-UserUPN {
}
}
# Inside the foreach loop where accounts are processed:
foreach ($line in $lines) {
$newReportContent += $line
# Regex to match the SAMAccountName from the report line
if ($line -match "^\s*(\S+)\s*$") {
$samAccountName = $matches[1]
Write-Verbose "Looking up UPN for $samAccountName"
$upn = Get-UserUPN -SamAccountName $samAccountName -Domain $selectedDomain.DC -Credential $credential
$newReportContent += " UPN: $upn"
}
}
# (removed stray top-level loop; UPN enrichment happens during report generation below)
# Function to test for weak AD passwords
function Test-WeakADPasswords {
param (
[hashtable]$DomainDetails,
[string]$FilePath
[string]$FilePath,
[bool]$CheckOnlyEnabledUsers = $false
)
# User selects a domain
@@ -198,8 +199,15 @@ function Test-WeakADPasswords {
# Performing the test
Write-Verbose "Testing password quality for $($selectedDomain.Name)..."
try {
$testResults = Get-ADReplAccount -All -Server $selectedDomain["DC"] -Credential $credential |
Test-PasswordQuality -WeakPasswordHashesFile $FilePath
$accounts = Get-ADReplAccount -All -Server $selectedDomain["DC"] -Credential $credential
if ($CheckOnlyEnabledUsers) {
Write-Verbose "Filtering to only enabled users per settings."
# Prefer property 'Enabled' if present, fall back gracefully if not
$accounts = $accounts | Where-Object {
if ($_.PSObject.Properties.Name -contains 'Enabled') { $_.Enabled } else { $true }
}
}
$testResults = $accounts | Test-PasswordQuality -WeakPasswordHashesFile $FilePath
Write-Verbose "Password quality test completed."
} catch {
Write-Error ("An error occurred while testing passwords: {0}" -f $_.Exception.Message)
@@ -262,11 +270,12 @@ function Test-WeakADPasswords {
}
# Main script logic
try {
Write-Verbose "Starting main script execution..."
Test-WeakADPasswords -DomainDetails $domainDetails -FilePath $WeakHashesSortedFilePath
Test-WeakADPasswords -DomainDetails $domainDetails -FilePath $WeakHashesSortedFilePath -CheckOnlyEnabledUsers:$checkOnlyEnabledUsers
} catch {
Write-Error ("An error occurred during script execution: {0}" -f $_.Exception.Message)
} finally {
Stop-TestTranscript
}
Write-Host "Script execution completed."

View File

@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Update-KHDB.ps1 ##
## Version: 1.0.1 ##
## Version: 1.1.0 ##
## Support: support@cqre.net ##
##################################################
@@ -16,83 +16,183 @@
Known hashes database update script for the Elysium AD password testing tool.
.DESCRIPTION
This script downloads khdb.txt.zip from the designated Azure Storage account, decompresses it, and overwrites the current version.
This script downloads khdb.txt.zip from the designated Azure Storage account, validates and decompresses it, and atomically updates the current version with backup and logging.
#>
# Initialize an empty hashtable to store settings
$ElysiumSettings = @{}
# safer defaults
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
# Read the settings file
$settingsPath = "ElysiumSettings.txt"
Get-Content $settingsPath | ForEach-Object {
if ($_ -notmatch '^#' -and $_.Trim()) {
$keyValue = $_.Split('=', 2)
$key = $keyValue[0].Trim()
$value = $keyValue[1].Trim().Trim("'")
$ElysiumSettings[$key] = $value
# ensure TLS 1.2
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
# Resolve paths
$scriptRoot = $PSScriptRoot
function Start-UpdateTranscript {
param(
[string]$BasePath
)
try {
$logsDir = Join-Path -Path $BasePath -ChildPath 'Reports/logs'
if (-not (Test-Path $logsDir)) { New-Item -Path $logsDir -ItemType Directory -Force | Out-Null }
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
$logPath = Join-Path -Path $logsDir -ChildPath "update-khdb-$ts.log"
Start-Transcript -Path $logPath -Force | Out-Null
} catch {
Write-Warning "Could not start transcript: $($_.Exception.Message)"
}
}
# Verify that all required settings have been loaded
if (-not $ElysiumSettings.ContainsKey("storageAccountName") -or
-not $ElysiumSettings.ContainsKey("containerName") -or
-not $ElysiumSettings.ContainsKey("sasToken")) {
Write-Error "Missing required settings. Please check your settings file."
return
function Stop-UpdateTranscript {
try { Stop-Transcript | Out-Null } catch {}
}
# Construct the full URL for accessing the Azure Blob Storage
$storageAccountName = $ElysiumSettings["storageAccountName"]
$containerName = $ElysiumSettings["containerName"]
$sasToken = $ElysiumSettings["sasToken"]
$AzureBlobStorageUrl = "https://$storageAccountName.blob.core.windows.net/$containerName/khdb.txt.zip$sasToken"
# Load necessary .NET assembly for HTTP operations
Add-Type -AssemblyName System.Net.Http
function Update-KHDB {
Write-Host "Downloading KHDB..."
# Initialize the client for downloading the file
$httpClient = New-Object System.Net.Http.HttpClient
try {
# Start the asynchronous request to download the file
$response = $httpClient.GetAsync($AzureBlobStorageUrl, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result
if ($response.IsSuccessStatusCode) {
$totalBytes = $response.Content.Headers.ContentLength
$totalRead = 0
$read = 0
$buffer = New-Object byte[] 8192
$stream = $response.Content.ReadAsStreamAsync().Result
$fileStream = [System.IO.File]::Create("khdb.txt.zip")
# Read the stream in chunks and update the progress bar
while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$fileStream.Write($buffer, 0, $read)
$totalRead += $read
$percentage = ($totalRead * 100) / $totalBytes
Write-Progress -Activity "Downloading khdb.txt.zip" -Status "$([Math]::Round($percentage, 2))% Complete:" -PercentComplete $percentage
}
$fileStream.Close()
Write-Host "KHDB.zip downloaded successfully."
} else {
Write-Error "Failed to download khdb.txt.zip: $($response.StatusCode)"
function Read-ElysiumSettings {
$settings = @{}
$settingsPath = Join-Path -Path $scriptRoot -ChildPath 'ElysiumSettings.txt'
if (-not (Test-Path $settingsPath)) { throw "Settings file not found at $settingsPath" }
Get-Content $settingsPath | ForEach-Object {
if ($_ -and -not $_.Trim().StartsWith('#')) {
$kv = $_ -split '=', 2
if ($kv.Count -eq 2) { $settings[$kv[0].Trim()] = $kv[1].Trim().Trim("'") }
}
} catch {
Write-Error "Error during download: $_"
return
}
return $settings
}
# Decompressing KHDB.zip
function Get-InstallationPath([hashtable]$settings) {
$p = $settings['InstallationPath']
if ([string]::IsNullOrWhiteSpace($p)) { return $scriptRoot }
if ([System.IO.Path]::IsPathRooted($p)) { return $p }
return (Join-Path -Path $scriptRoot -ChildPath $p)
}
function New-HttpClient {
Add-Type -AssemblyName System.Net.Http
$client = [System.Net.Http.HttpClient]::new()
$client.Timeout = [TimeSpan]::FromSeconds(600)
$client.DefaultRequestHeaders.UserAgent.ParseAdd('Elysium/1.1 (+Update-KHDB)')
return $client
}
function Build-BlobUri([string]$account, [string]$container, [string]$sas) {
if ([string]::IsNullOrWhiteSpace($account)) { throw 'storageAccountName is missing or empty.' }
if ([string]::IsNullOrWhiteSpace($container)) { throw 'containerName is missing or empty.' }
if ([string]::IsNullOrWhiteSpace($sas)) { throw 'sasToken is missing or empty.' }
$sas = $sas.Trim()
if (-not $sas.StartsWith('?')) { $sas = '?' + $sas }
$ub = [System.UriBuilder]::new("https://$account.blob.core.windows.net/$container/khdb.txt.zip")
$ub.Query = $sas.TrimStart('?')
return $ub.Uri.AbsoluteUri
}
function Invoke-DownloadWithRetry([System.Net.Http.HttpClient]$client, [string]$uri, [string]$targetPath) {
$retries = 5
$delay = 2
for ($i = 0; $i -lt $retries; $i++) {
try {
$resp = $client.GetAsync($uri, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result
if (-not $resp.IsSuccessStatusCode) {
$code = [int]$resp.StatusCode
if (($code -ge 500 -and $code -lt 600) -or $code -eq 429 -or $code -eq 408) { throw "Transient HTTP error $code" }
throw "HTTP error $code"
}
$totalBytes = $resp.Content.Headers.ContentLength
$stream = $resp.Content.ReadAsStreamAsync().Result
$fs = [System.IO.File]::Create($targetPath)
try {
$buffer = New-Object byte[] 8192
$totalRead = 0
while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$fs.Write($buffer, 0, $read)
$totalRead += $read
if ($totalBytes) {
$pct = ($totalRead * 100.0) / $totalBytes
Write-Progress -Activity "Downloading khdb.txt.zip" -Status ("{0:N2}% Complete" -f $pct) -PercentComplete $pct
} else {
Write-Progress -Activity "Downloading khdb.txt.zip" -Status ("Downloaded {0:N0} bytes" -f $totalRead) -PercentComplete 0
}
}
} finally {
$fs.Close(); $stream.Close()
}
return
} catch {
if ($i -lt ($retries - 1)) {
Write-Warning "Download failed (attempt $($i+1)/$retries): $($_.Exception.Message). Retrying in ${delay}s..."
Start-Sleep -Seconds $delay
$delay = [Math]::Min($delay * 2, 30)
} else {
throw
}
}
}
}
function Validate-KHDBFile([string]$path) {
if (-not (Test-Path $path)) { throw "Validation failed: $path not found" }
$lines = Get-Content -Path $path -Encoding UTF8
if (-not $lines -or $lines.Count -eq 0) { throw 'Validation failed: file is empty.' }
$regex = '^[0-9A-Fa-f]{32}$'
$invalid = $lines | Where-Object { $_ -notmatch $regex }
if ($invalid.Count -gt 0) {
throw ("Validation failed: {0} invalid lines detected." -f $invalid.Count)
}
# Deduplicate and normalize line endings
$unique = $lines | ForEach-Object { $_.Trim() } | Where-Object { $_ } | Sort-Object -Unique
Set-Content -Path $path -Value $unique -Encoding ASCII
}
function Update-KHDB {
Start-UpdateTranscript -BasePath $scriptRoot
try {
Expand-Archive -Path "khdb.txt.zip" -DestinationPath . -Force
Remove-Item -Path "khdb.txt.zip" -Force # Delete the zip file after extraction
Write-Host "KHDB decompressed and cleaned up successfully."
$settings = Read-ElysiumSettings
$installPath = Get-InstallationPath $settings
if (-not (Test-Path $installPath)) { New-Item -Path $installPath -ItemType Directory -Force | Out-Null }
$storageAccountName = $settings['storageAccountName']
$containerName = $settings['containerName']
$sasToken = $settings['sasToken']
$uri = Build-BlobUri -account $storageAccountName -container $containerName -sas $sasToken
$client = New-HttpClient
$tmpDir = New-Item -ItemType Directory -Path ([System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "elysium-khdb-" + [System.Guid]::NewGuid())) -Force
$zipPath = Join-Path -Path $tmpDir.FullName -ChildPath 'khdb.txt.zip'
$extractDir = Join-Path -Path $tmpDir.FullName -ChildPath 'extract'
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
Write-Host "Downloading KHDB from Azure Blob Storage..."
Invoke-DownloadWithRetry -client $client -uri $uri -targetPath $zipPath
Write-Host "Download completed. Extracting archive..."
Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force
$extractedKHDB = Get-ChildItem -Path $extractDir -Recurse -Filter 'khdb.txt' | Select-Object -First 1
if (-not $extractedKHDB) { throw 'Extracted archive does not contain khdb.txt.' }
# Validate content
Validate-KHDBFile -path $extractedKHDB.FullName
# Compute target path and backup
$targetKHDB = Join-Path -Path $installPath -ChildPath 'khdb.txt'
if (Test-Path $targetKHDB) {
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
$backupPath = Join-Path -Path $installPath -ChildPath ("khdb.txt.bak-$ts")
Copy-Item -Path $targetKHDB -Destination $backupPath -Force
Write-Host "Existing KHDB backed up to $backupPath"
}
# Atomic-ish replace: move validated file into place
Move-Item -Path $extractedKHDB.FullName -Destination $targetKHDB -Force
Write-Host "KHDB updated at $targetKHDB"
Write-Host "KHDB update completed successfully."
} catch {
Write-Error "Error decompressing KHDB: $_"
return
Write-Error ("KHDB update failed: {0}" -f $_.Exception.Message)
throw
} finally {
try { if ($tmpDir -and (Test-Path $tmpDir.FullName)) { Remove-Item -Path $tmpDir.FullName -Recurse -Force } } catch {}
Stop-UpdateTranscript
}
}

View File

@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: decrypt.py ##
## Version: 1.0.0 ##
## Version: 1.1.0 ##
## Support: support@cqre.net ##
##################################################
@@ -15,12 +15,13 @@
# Install PyCryptodome with "pip install pycryptodome". Must be run with python3.
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
import hashlib
import os
# Ask for the encrypted file's name
encrypted_file_name = input("Enter the name of the encrypted file (with .enc extension): ")
encrypted_file_path = f'path/to/your/encrypted/{encrypted_file_name}'
encrypted_file_path = encrypted_file_name if os.path.isabs(encrypted_file_name) else os.path.join(os.getcwd(), encrypted_file_name)
decrypted_file_path = encrypted_file_path.replace('.enc', '.zip')
# Try to retrieve the passphrase from the environment variable
@@ -30,22 +31,47 @@ if passphrase is None:
passphrase = input("Passphrase not found in environment. Please enter the passphrase: ")
# Here, you might save the passphrase to a temporary session or file, but be cautious with security.
# Derive the AES key from the passphrase
key = hashlib.sha256(passphrase.encode()).digest()
def decrypt_legacy(data: bytes, key_bytes: bytes) -> bytes:
if len(data) < 16:
raise ValueError("Encrypted data too short for legacy format")
iv = data[:16]
encrypted = data[16:]
cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(encrypted)
pad_len = plaintext[-1]
if not (1 <= pad_len <= 16):
raise ValueError("Invalid padding length in legacy data")
return plaintext[:-pad_len]
def decrypt_pbkdf2(data: bytes, passphrase: str) -> bytes:
if len(data) < 4 + 16 + 16:
raise ValueError("Encrypted data too short for PBKDF2 format")
magic = data[:4]
if magic != b"ELY1":
raise ValueError("Invalid magic header for PBKDF2 format")
salt = data[4:20]
iv = data[20:36]
encrypted = data[36:]
key = PBKDF2(passphrase, salt, dkLen=32, count=100000)
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(encrypted)
pad_len = plaintext[-1]
if not (1 <= pad_len <= 16):
raise ValueError("Invalid padding length in PBKDF2 data")
return plaintext[:-pad_len]
try:
# Read the encrypted file
with open(encrypted_file_path, 'rb') as encrypted_file:
iv = encrypted_file.read(16) # The first 16 bytes are the IV
encrypted_data = encrypted_file.read()
blob = encrypted_file.read()
# Decrypt the data
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_data = cipher.decrypt(encrypted_data)
# Remove potential PKCS#7 padding
pad_len = decrypted_data[-1]
decrypted_data = decrypted_data[:-pad_len]
# Try PBKDF2 format first (ELY1 header), then legacy fallback
if blob.startswith(b"ELY1"):
decrypted_data = decrypt_pbkdf2(blob, passphrase)
else:
# Legacy key derivation: SHA-256(passphrase), IV is first 16 bytes
legacy_key = hashlib.sha256(passphrase.encode()).digest()
decrypted_data = decrypt_legacy(blob, legacy_key)
# Write the decrypted data to a file
with open(decrypted_file_path, 'wb') as decrypted_file: