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 # 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 ## Extract-NTHashes.ps1
### version 1.1.1 ### version 1.1.1

View File

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

View File

@@ -8,7 +8,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: ElysiumSettings.txt ## ## File: ElysiumSettings.txt ##
## Version: 1.0.0 ## ## Version: 1.1.0 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
@@ -21,9 +21,14 @@ sasToken =
# Application Settings # Application Settings
###################### ######################
InstallationPath= InstallationPath=
ReportPathBase=/Reports ReportPathBase=Reports
WeakPasswordsDatabase=khdb.txt 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 # Domain Settings
################# #################

View File

@@ -7,11 +7,12 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Extract-NTLMHashes.ps1 ## ## File: Extract-NTLMHashes.ps1 ##
## Version: 1.0.2 ## ## Version: 1.2.0 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
<# <#
#Requires -Modules DSInternals, Az.Storage
.SYNOPSIS .SYNOPSIS
Script for extracting NTLM hashes from live AD for further analysis. 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. 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..." Write-Host "Loading settings..."
$ElysiumSettings = @{} $ElysiumSettings = @{}
$settingsPath = "ElysiumSettings.txt" $settingsPath = Join-Path -Path $scriptRoot -ChildPath "ElysiumSettings.txt"
if (-not (Test-Path $settingsPath)) { if (-not (Test-Path $settingsPath)) {
Write-Error "Settings file not found at $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 function Normalize-ReportPath([string]$p) {
$requiredModules = @('DSInternals', 'Az.Storage') if ([string]::IsNullOrWhiteSpace($p)) { return (Join-Path -Path $scriptRoot -ChildPath 'Reports') }
foreach ($module in $requiredModules) { if ([System.IO.Path]::IsPathRooted($p)) { return $p }
if (-not (Get-Module -ListAvailable -Name $module)) { return (Join-Path -Path $scriptRoot -ChildPath $p)
Write-Host "Installing $module module..."
Install-Module $module -Scope CurrentUser -Force
}
Import-Module $module
} }
# Script variables
# External settings # External settings
$storageAccountName = $ElysiumSettings['storageAccountName'] $storageAccountName = $ElysiumSettings['storageAccountName']
$containerName = $ElysiumSettings['containerName'] $containerName = $ElysiumSettings['containerName']
@@ -56,14 +73,10 @@ $sasToken = $ElysiumSettings['sasToken']
# Retrieve the passphrase from a user environment variable # Retrieve the passphrase from a user environment variable
$passphrase = [System.Environment]::GetEnvironmentVariable("ELYSIUM_PASSPHRASE", [System.EnvironmentVariableTarget]::User) $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" $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 { function Protect-FileWithAES {
param ( param (
@@ -77,41 +90,49 @@ function Protect-FileWithAES {
[string]$Passphrase [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.KeySize = 256
$aes.BlockSize = 128 $aes.BlockSize = 128
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC $aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
# 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.GenerateIV() $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) $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 { try {
$buffer = New-Object Byte[] 8192 # File header: magic 'ELY1' (4 bytes), salt (16 bytes), IV (16 bytes)
while (($read = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) { $magic = [System.Text.Encoding]::ASCII.GetBytes('ELY1')
$cryptoStream.Write($buffer, 0, $read) $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 {
finally { $outFileStream.Close(); $fileStream.Close(); $aes.Dispose(); $rng.Dispose(); $kdf.Dispose()
$cryptoStream.Close()
$outFileStream.Close()
$fileStream.Close()
$aes.Clear()
} }
Write-Host "File has been encrypted: $OutputFile" Write-Host "File has been encrypted (PBKDF2+AES-256-CBC): $OutputFile"
} }
function Get-FileChecksum { function Get-FileChecksum {
param ( param (
@@ -131,8 +152,8 @@ function Get-FileChecksum {
} }
# Extract NTLM hashes # Extract NTLM hashes
$exportPath = ".\NTLM_Hashes_$timestamp.txt" $reportBase = Normalize-ReportPath -p $ElysiumSettings['ReportPathBase']
$compressedFilePath = ".\NTLM_Hashes_$timestamp.zip" if (-not (Test-Path $reportBase)) { New-Item -Path $reportBase -ItemType Directory -Force | Out-Null }
# Build domain details from settings # Build domain details from settings
$DomainDetails = @{} $DomainDetails = @{}
@@ -159,6 +180,13 @@ if (-not $selectedDomain) {
$domainController = $selectedDomain.DC $domainController = $selectedDomain.DC
$credential = Get-Credential -Message "Enter AD credentials with replication rights for $($selectedDomain.Name)" $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 | $ntlmHashes = Get-ADReplAccount -All -Server $domainController -Credential $credential |
Where-Object { $_.NTHash } | Where-Object { $_.NTHash } |
ForEach-Object { [BitConverter]::ToString($_.NTHash).Replace("-", "") } | ForEach-Object { [BitConverter]::ToString($_.NTHash).Replace("-", "") } |
@@ -179,15 +207,22 @@ Write-Host "File has been encrypted: $encryptedFilePath"
$localFileChecksum = Get-FileChecksum -Path $encryptedFilePath $localFileChecksum = Get-FileChecksum -Path $encryptedFilePath
# Create the context for Azure Blob Storage with SAS token # 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 # 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" Write-Host "Encrypted file uploaded to Azure Blob Storage: $blobName"
# Download the blob to a temporary location to verify # Download the blob to a temporary location to verify
$tempDownloadPath = [System.IO.Path]::GetTempFileName() $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 # Calculate the downloaded file checksum
$downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath $downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
@@ -195,13 +230,16 @@ $downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
# Compare the checksums # Compare the checksums
if ($localFileChecksum -eq $downloadedFileChecksum) { if ($localFileChecksum -eq $downloadedFileChecksum) {
Write-Host "The file was correctly uploaded. Checksum verified." 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 { 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 Write-Host "Script execution completed."
Remove-Item -Path $exportPath, $compressedFilePath, $encryptedFilePath, $tempDownloadPath -Force } finally {
Write-Host "Local and temporary files cleaned up after uploading to Azure Blob Storage." Stop-ExtractTranscript
}
Write-Host "Script execution completed."

View File

@@ -8,11 +8,12 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Test-WeakADPasswords.ps1 ## ## File: Test-WeakADPasswords.ps1 ##
## Version: 1.1.1 ## ## Version: 1.3.0 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
<# <#
#Requires -Modules DSInternals, ActiveDirectory
.SYNOPSIS .SYNOPSIS
Weak AD password finder component of Elysium tool. 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 # Enable verbose output
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$VerbosePreference = "Continue" $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 # Current timestamp for both report generation and header
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $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 ====" $footer = "`r`n==== End of Report ===="
# Import settings Start-TestTranscript -BasePath $scriptRoot
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
try { try {
Get-Content $settingsPath | ForEach-Object { # Import settings
if (-not [string]::IsNullOrWhiteSpace($_) -and -not $_.StartsWith("#")) { Write-Verbose "Loading settings..."
$keyValue = $_ -split '=', 2 $ElysiumSettings = @{}
if ($keyValue.Count -eq 2) { $settingsPath = Join-Path -Path $scriptRoot -ChildPath "ElysiumSettings.txt"
$ElysiumSettings[$keyValue[0].Trim()] = $keyValue[1].Trim()
# 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 # Define the function to extract domain details from settings
function Get-DomainDetailsFromSettings { function Get-DomainDetailsFromSettings {
@@ -91,44 +113,28 @@ function Get-DomainDetailsFromSettings {
$domainDetails = Get-DomainDetailsFromSettings -Settings $ElysiumSettings $domainDetails = Get-DomainDetailsFromSettings -Settings $ElysiumSettings
Write-Verbose ("Domain details extracted: {0}" -f ($domainDetails | ConvertTo-Json)) Write-Verbose ("Domain details extracted: {0}" -f ($domainDetails | ConvertTo-Json))
# Required modules # Modules are required via #Requires; PowerShell will stop early if missing.
$requiredModules = @("DSInternals", "ActiveDirectory")
# Check each required module and import # Resolve KHDB path with fallbacks
foreach ($module in $requiredModules) { $installationPath = $ElysiumSettings["InstallationPath"]
if (-not (Get-Module -ListAvailable -Name $module)) { if ([string]::IsNullOrWhiteSpace($installationPath)) { $installationPath = $scriptRoot }
Write-Verbose "Required module '$module' is not installed." elseif (-not [System.IO.Path]::IsPathRooted($installationPath)) { $installationPath = Join-Path -Path $scriptRoot -ChildPath $installationPath }
$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."
}
# Verify the existence of the Weak Password Hashes file $khdbName = if ([string]::IsNullOrWhiteSpace($ElysiumSettings["WeakPasswordsDatabase"])) { 'khdb.txt' } else { $ElysiumSettings["WeakPasswordsDatabase"] }
$WeakHashesSortedFilePath = Join-Path -Path $ElysiumSettings["InstallationPath"] -ChildPath $ElysiumSettings["WeakPasswordsDatabase"] $WeakHashesSortedFilePath = Join-Path -Path $installationPath -ChildPath $khdbName
if (-not (Test-Path $WeakHashesSortedFilePath)) { if (-not (Test-Path $WeakHashesSortedFilePath)) {
Write-Error "Weak password hashes file not found at '$WeakHashesSortedFilePath'." Write-Error "Weak password hashes file not found at '$WeakHashesSortedFilePath'."
exit exit
} }
Write-Verbose "Weak password hashes file found at '$WeakHashesSortedFilePath'." 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"] $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)) { if (-not (Test-Path -Path $reportPathBase)) {
try { 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'." Write-Verbose "Report directory created at '$reportPathBase'."
} catch { } catch {
Write-Error ("Failed to create report directory: {0}" -f $_.Exception.Message) 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 to get UPN for a given SAM account name
function Get-UserUPN { function Get-UserUPN {
param ( param (
@@ -158,25 +170,14 @@ function Get-UserUPN {
} }
} }
# Inside the foreach loop where accounts are processed: # (removed stray top-level loop; UPN enrichment happens during report generation below)
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"
}
}
# Function to test for weak AD passwords # Function to test for weak AD passwords
function Test-WeakADPasswords { function Test-WeakADPasswords {
param ( param (
[hashtable]$DomainDetails, [hashtable]$DomainDetails,
[string]$FilePath [string]$FilePath,
[bool]$CheckOnlyEnabledUsers = $false
) )
# User selects a domain # User selects a domain
@@ -198,8 +199,15 @@ function Test-WeakADPasswords {
# Performing the test # Performing the test
Write-Verbose "Testing password quality for $($selectedDomain.Name)..." Write-Verbose "Testing password quality for $($selectedDomain.Name)..."
try { try {
$testResults = Get-ADReplAccount -All -Server $selectedDomain["DC"] -Credential $credential | $accounts = Get-ADReplAccount -All -Server $selectedDomain["DC"] -Credential $credential
Test-PasswordQuality -WeakPasswordHashesFile $FilePath 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." 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)
@@ -262,11 +270,12 @@ function Test-WeakADPasswords {
} }
# Main script logic # Main script logic
try {
Write-Verbose "Starting main script execution..." Write-Verbose "Starting main script execution..."
Test-WeakADPasswords -DomainDetails $domainDetails -FilePath $WeakHashesSortedFilePath Test-WeakADPasswords -DomainDetails $domainDetails -FilePath $WeakHashesSortedFilePath -CheckOnlyEnabledUsers:$checkOnlyEnabledUsers
} catch { } catch {
Write-Error ("An error occurred during script execution: {0}" -f $_.Exception.Message) Write-Error ("An error occurred during script execution: {0}" -f $_.Exception.Message)
} finally {
Stop-TestTranscript
} }
Write-Host "Script execution completed." Write-Host "Script execution completed."

View File

@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Update-KHDB.ps1 ## ## File: Update-KHDB.ps1 ##
## Version: 1.0.1 ## ## Version: 1.1.0 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
@@ -16,83 +16,183 @@
Known hashes database update script for the Elysium AD password testing tool. Known hashes database update script for the Elysium AD password testing tool.
.DESCRIPTION .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 # safer defaults
$ElysiumSettings = @{} $ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
# Read the settings file # ensure TLS 1.2
$settingsPath = "ElysiumSettings.txt" [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
Get-Content $settingsPath | ForEach-Object {
if ($_ -notmatch '^#' -and $_.Trim()) { # Resolve paths
$keyValue = $_.Split('=', 2) $scriptRoot = $PSScriptRoot
$key = $keyValue[0].Trim()
$value = $keyValue[1].Trim().Trim("'") function Start-UpdateTranscript {
$ElysiumSettings[$key] = $value 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 function Stop-UpdateTranscript {
if (-not $ElysiumSettings.ContainsKey("storageAccountName") -or try { Stop-Transcript | Out-Null } catch {}
-not $ElysiumSettings.ContainsKey("containerName") -or
-not $ElysiumSettings.ContainsKey("sasToken")) {
Write-Error "Missing required settings. Please check your settings file."
return
} }
# Construct the full URL for accessing the Azure Blob Storage function Read-ElysiumSettings {
$storageAccountName = $ElysiumSettings["storageAccountName"] $settings = @{}
$containerName = $ElysiumSettings["containerName"] $settingsPath = Join-Path -Path $scriptRoot -ChildPath 'ElysiumSettings.txt'
$sasToken = $ElysiumSettings["sasToken"] if (-not (Test-Path $settingsPath)) { throw "Settings file not found at $settingsPath" }
$AzureBlobStorageUrl = "https://$storageAccountName.blob.core.windows.net/$containerName/khdb.txt.zip$sasToken" Get-Content $settingsPath | ForEach-Object {
if ($_ -and -not $_.Trim().StartsWith('#')) {
# Load necessary .NET assembly for HTTP operations $kv = $_ -split '=', 2
Add-Type -AssemblyName System.Net.Http if ($kv.Count -eq 2) { $settings[$kv[0].Trim()] = $kv[1].Trim().Trim("'") }
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)"
} }
} 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 { try {
Expand-Archive -Path "khdb.txt.zip" -DestinationPath . -Force $settings = Read-ElysiumSettings
Remove-Item -Path "khdb.txt.zip" -Force # Delete the zip file after extraction $installPath = Get-InstallationPath $settings
Write-Host "KHDB decompressed and cleaned up successfully." 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 { } catch {
Write-Error "Error decompressing KHDB: $_" Write-Error ("KHDB update failed: {0}" -f $_.Exception.Message)
return 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 ## ## Project: Elysium ##
## File: decrypt.py ## ## File: decrypt.py ##
## Version: 1.0.0 ## ## Version: 1.1.0 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
@@ -15,12 +15,13 @@
# Install PyCryptodome with "pip install pycryptodome". Must be run with python3. # Install PyCryptodome with "pip install pycryptodome". Must be run with python3.
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
import hashlib import hashlib
import os import os
# Ask for the encrypted file's name # Ask for the encrypted file's name
encrypted_file_name = input("Enter the name of the encrypted file (with .enc extension): ") 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') decrypted_file_path = encrypted_file_path.replace('.enc', '.zip')
# Try to retrieve the passphrase from the environment variable # 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: ") 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. # Here, you might save the passphrase to a temporary session or file, but be cautious with security.
# Derive the AES key from the passphrase def decrypt_legacy(data: bytes, key_bytes: bytes) -> bytes:
key = hashlib.sha256(passphrase.encode()).digest() 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: try:
# Read the encrypted file # Read the encrypted file
with open(encrypted_file_path, 'rb') as encrypted_file: with open(encrypted_file_path, 'rb') as encrypted_file:
iv = encrypted_file.read(16) # The first 16 bytes are the IV blob = encrypted_file.read()
encrypted_data = encrypted_file.read()
# Decrypt the data # Try PBKDF2 format first (ELY1 header), then legacy fallback
cipher = AES.new(key, AES.MODE_CBC, iv) if blob.startswith(b"ELY1"):
decrypted_data = cipher.decrypt(encrypted_data) decrypted_data = decrypt_pbkdf2(blob, passphrase)
else:
# Remove potential PKCS#7 padding # Legacy key derivation: SHA-256(passphrase), IV is first 16 bytes
pad_len = decrypted_data[-1] legacy_key = hashlib.sha256(passphrase.encode()).digest()
decrypted_data = decrypted_data[:-pad_len] decrypted_data = decrypt_legacy(blob, legacy_key)
# Write the decrypted data to a file # Write the decrypted data to a file
with open(decrypted_file_path, 'wb') as decrypted_file: with open(decrypted_file_path, 'wb') as decrypted_file: