2 Commits

Author SHA1 Message Date
tomas.kracmar 09c30f97e9 Release v2.2.1: DRY refactoring and housekeeping
Consolidated duplicated helpers into Elysium.Common.ps1:
- Settings parsing (Read-KeyValueSettingsFile, Read-ElysiumSettings, Get-SettingsValue)
- Azure Blob URI builder (Build-BlobUri)
- S3 SigV4 signing helpers and AWS module bootstrap
- AD credential validation and replication permission pre-check
- Parallel execution helper (Get-FunctionDefinitionText)

Test-WeakADPasswords.ps1 and Extract-NTHashes.ps1 now import
Elysium.Common.ps1 for the first time. Update-KHDB.ps1 and
Prepare-KHDBStorage.ps1 removed their local duplicates.

Deleted legacy Settings.ps1 (superseded by ElysiumSettings.txt).
Removed stray placeholder comment in Elysium.ps1.

All versions bumped to unified v2.2.1.
2026-06-09 10:52:19 +02:00
tomas.kracmar 5127c2d096 fix(Test-WeakADPasswords): surface replication permission success to console
Replace Write-Verbose with Write-Host so operators see the
permissions verification result without needing -Verbose.
2026-06-09 09:56:24 +02:00
11 changed files with 616 additions and 845 deletions
+16
View File
@@ -6,6 +6,22 @@ Starting with **v2.2.0**, Elysium uses a **unified project version**. All script
--- ---
## [2.2.1] — 2026-06-09
### Changed
- **DRY refactoring — shared helpers consolidated into `Elysium.Common.ps1`:**
- Moved `Read-KeyValueSettingsFile`, `Read-ElysiumSettings`, and `Get-SettingsValue` from `Prepare-KHDBStorage.ps1` and `Update-KHDB.ps1` into the common helper.
- Moved `Build-BlobUri` and Azure URI helpers from `Update-KHDB.ps1` into the common helper.
- Moved `Get-FunctionDefinitionText` from all scripts that duplicated it into the common helper.
- Moved `Get-ValidatedADCredential` and `Test-ReplicationPermissions` from `Test-WeakADPasswords.ps1` into the common helper.
- Moved all native S3 SigV4 helpers (`Ensure-AWSS3Module`, `New-S3Client`, `HmacSha256`, `GetSignatureKey`, `BuildAuthHeaders`, `BuildS3Uri`, etc.) from `Extract-NTHashes.ps1` into the common helper.
- `Test-WeakADPasswords.ps1` and `Extract-NTHashes.ps1` now import `Elysium.Common.ps1` (they previously did not), reducing duplication and ensuring consistent behavior.
- `Update-KHDB.ps1` and `Prepare-KHDBStorage.ps1` removed their local copies of helpers already available in the common module.
- Removed legacy `Settings.ps1` (superseded by `ElysiumSettings.txt`).
- Minor cleanup: removed stray placeholder comment in `Elysium.ps1`.
---
## [2.2.0] — 2026-06-09 ## [2.2.0] — 2026-06-09
### Changed ### Changed
+316
View File
@@ -68,3 +68,319 @@ function Restart-WithWindowsPowerShellIfAvailable {
Write-Host ("Windows PowerShell detected at '{0}'; relaunching script under powershell.exe..." -f $powershellPath) Write-Host ("Windows PowerShell detected at '{0}'; relaunching script under powershell.exe..." -f $powershellPath)
Invoke-RestartWithExecutable -ExecutablePath $powershellPath -BoundParameters $BoundParameters -UnboundArguments $UnboundArguments Invoke-RestartWithExecutable -ExecutablePath $powershellPath -BoundParameters $BoundParameters -UnboundArguments $UnboundArguments
} }
# ---------------------------------------------------------------------------
# Settings loading
# ---------------------------------------------------------------------------
function Read-KeyValueSettingsFile {
param([Parameter(Mandatory)][string]$Path)
$result = @{}
if (-not (Test-Path -LiteralPath $Path)) { return $result }
foreach ($line in (Get-Content -LiteralPath $Path)) {
if ($null -eq $line) { continue }
$trimmed = $line.Trim()
if (-not $trimmed) { continue }
if ($trimmed.StartsWith('#')) { continue }
$kv = $line -split '=', 2
if ($kv.Count -ne 2) { continue }
$key = $kv[0].Trim()
$value = $kv[1].Trim()
if (-not $key) { continue }
if ($value.StartsWith("'") -and $value.EndsWith("'") -and $value.Length -ge 2) {
$value = $value.Substring(1, $value.Length - 2)
}
$result[$key] = $value
}
return $result
}
function Read-ElysiumSettings {
param([Parameter(Mandatory)][string]$ScriptRoot)
$settingsPath = Join-Path -Path $ScriptRoot -ChildPath 'ElysiumSettings.txt'
if (-not (Test-Path -LiteralPath $settingsPath)) {
throw "Settings file not found at $settingsPath"
}
return Read-KeyValueSettingsFile -Path $settingsPath
}
function Get-SettingsValue {
param(
[hashtable]$Settings,
[string]$Key
)
if (-not $Settings) { return $null }
if ($Settings.ContainsKey($Key)) { return $Settings[$Key] }
return $null
}
# ---------------------------------------------------------------------------
# Parallel execution helpers
# ---------------------------------------------------------------------------
function Get-FunctionDefinitionText {
param([Parameter(Mandatory)][string]$Name)
$cmd = Get-Command -Name $Name -CommandType Function -ErrorAction Stop
return $cmd.ScriptBlock.Ast.Extent.Text
}
# ---------------------------------------------------------------------------
# Azure Blob Storage helpers
# ---------------------------------------------------------------------------
function Build-BlobUri {
param(
[string]$Account,
[string]$Container,
[string]$Sas,
[string]$BlobName
)
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.' }
if ([string]::IsNullOrWhiteSpace($BlobName)) { throw 'BlobName cannot be empty.' }
$sas = $Sas.Trim()
if (-not $sas.StartsWith('?')) { $sas = '?' + $sas }
$normalizedBlob = $BlobName.Replace('\', '/').TrimStart('/')
$builder = [System.UriBuilder]::new("https://$Account.blob.core.windows.net/$Container/$normalizedBlob")
$builder.Query = $sas.TrimStart('?')
return $builder.Uri.AbsoluteUri
}
# ---------------------------------------------------------------------------
# Storage path utilities
# ---------------------------------------------------------------------------
function Combine-StoragePath {
param(
[string]$Prefix,
[string]$Name
)
$cleanName = $Name.Replace('\', '/').TrimStart('/')
if ([string]::IsNullOrWhiteSpace($Prefix)) { return $cleanName }
$normalizedPrefix = $Prefix.Replace('\', '/').Trim('/')
if ([string]::IsNullOrEmpty($normalizedPrefix)) { return $cleanName }
return "$normalizedPrefix/$cleanName"
}
# ---------------------------------------------------------------------------
# AWS SigV4 / S3 helpers
# ---------------------------------------------------------------------------
function Get-Bytes([string]$s) { return [System.Text.Encoding]::UTF8.GetBytes($s) }
function Get-HashHex([byte[]]$bytes) {
if ($null -eq $bytes) { $bytes = [byte[]]@() }
$sha = [System.Security.Cryptography.SHA256]::Create()
try {
$ms = New-Object System.IO.MemoryStream -ArgumentList (,$bytes)
try {
$hash = $sha.ComputeHash([System.IO.Stream]$ms)
} finally { $ms.Dispose() }
return ([BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant()
} finally { $sha.Dispose() }
}
function HmacSha256([byte[]]$key, [string]$data) {
$h = [System.Security.Cryptography.HMACSHA256]::new($key)
try {
$b = [System.Text.Encoding]::UTF8.GetBytes($data)
$ms = New-Object System.IO.MemoryStream -ArgumentList (,$b)
try {
return $h.ComputeHash([System.IO.Stream]$ms)
} finally { $ms.Dispose() }
} finally { $h.Dispose() }
}
function GetSignatureKey([string]$secret, [string]$dateStamp, [string]$regionName, [string]$serviceName) {
$kDate = HmacSha256 (Get-Bytes ('AWS4' + $secret)) $dateStamp
$kRegion = HmacSha256 $kDate $regionName
$kService = HmacSha256 $kRegion $serviceName
HmacSha256 $kService 'aws4_request'
}
function UriEncode([string]$data, [bool]$encodeSlash) {
if ($null -eq $data) { return '' }
$enc = [System.Uri]::EscapeDataString($data)
if (-not $encodeSlash) { $enc = $enc -replace '%2F', '/' }
return $enc
}
function BuildCanonicalPath([System.Uri]$uri) {
$segments = $uri.AbsolutePath.Split('/')
$encoded = @()
foreach ($s in $segments) { $encoded += (UriEncode $s $false) }
$path = ($encoded -join '/')
if (-not $path.StartsWith('/')) { $path = '/' + $path }
return $path
}
function ToHex([byte[]]$b) { ([BitConverter]::ToString($b)).Replace('-', '').ToLowerInvariant() }
function BuildAuthHeaders($method, [System.Uri]$uri, [string]$region, [string]$accessKey, [string]$secretKey, [string]$payloadHash) {
$algorithm = 'AWS4-HMAC-SHA256'
$timestamp = (Get-Date).ToUniversalTime()
$amzDate = $timestamp.ToString('yyyyMMddTHHmmssZ')
$dateStamp = $timestamp.ToString('yyyyMMdd')
$hostHeader = $uri.Host
if (-not $uri.IsDefaultPort) { $hostHeader = "${hostHeader}:$($uri.Port)" }
$canonicalUri = BuildCanonicalPath $uri
$canonicalQueryString = ''
$canonicalHeaders = "host:$hostHeader`n" + "x-amz-content-sha256:$payloadHash`n" + "x-amz-date:$amzDate`n"
$signedHeaders = 'host;x-amz-content-sha256;x-amz-date'
$canonicalRequest = "$method`n$canonicalUri`n$canonicalQueryString`n$canonicalHeaders`n$signedHeaders`n$payloadHash"
$credentialScope = "$dateStamp/$region/s3/aws4_request"
$stringToSign = "$algorithm`n$amzDate`n$credentialScope`n$((Get-HashHex (Get-Bytes $canonicalRequest)))"
$signingKey = GetSignatureKey $secretKey $dateStamp $region 's3'
$signature = ToHex (HmacSha256 $signingKey $stringToSign)
$authHeader = "$algorithm Credential=$accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature"
@{
'x-amz-date' = $amzDate
'x-amz-content-sha256' = $payloadHash
'Authorization' = $authHeader
}
}
function BuildS3Uri([string]$endpointUrl, [string]$bucket, [string]$key, [bool]$forcePathStyle) {
$base = [System.Uri]$endpointUrl
$builder = [System.UriBuilder]::new($base)
$normalizedKey = $key.Replace('\', '/').TrimStart('/')
if ($forcePathStyle) {
$path = $builder.Path.TrimEnd('/')
if ([string]::IsNullOrEmpty($path)) { $path = '/' }
$builder.Path = ($path.TrimEnd('/') + '/' + $bucket + '/' + $normalizedKey)
} else {
$builder.Host = "$bucket." + $builder.Host
$path = $builder.Path.TrimEnd('/')
if ([string]::IsNullOrEmpty($path)) { $path = '/' }
$builder.Path = ($path.TrimEnd('/') + '/' + $normalizedKey)
}
return $builder.Uri
}
function Ensure-AWSS3Module {
try { $null = [Amazon.S3.AmazonS3Client]; return } catch {}
try { Import-Module -Name AWS.Tools.S3 -ErrorAction Stop; return } catch {}
try { Import-Module -Name AWSPowerShell.NetCore -ErrorAction Stop; return } catch {}
throw "AWS Tools for PowerShell not found. Install with: Install-Module AWS.Tools.S3 -Scope CurrentUser"
}
function New-S3Client {
param(
[string]$EndpointUrl,
[string]$Region,
[string]$AccessKeyId,
[string]$SecretAccessKey,
[bool]$ForcePathStyle = $true
)
Ensure-AWSS3Module
$creds = New-Object Amazon.Runtime.BasicAWSCredentials($AccessKeyId, $SecretAccessKey)
$cfg = New-Object Amazon.S3.AmazonS3Config
if ($EndpointUrl) { $cfg.ServiceURL = $EndpointUrl }
if ($Region) { try { $cfg.RegionEndpoint = [Amazon.RegionEndpoint]::GetBySystemName($Region) } catch {} }
$cfg.ForcePathStyle = [bool]$ForcePathStyle
return (New-Object Amazon.S3.AmazonS3Client($creds, $cfg))
}
# ---------------------------------------------------------------------------
# Active Directory credential and permission helpers
# (requires the ActiveDirectory module to be loaded before calling)
# ---------------------------------------------------------------------------
function Get-ValidatedADCredential {
param (
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$Server,
[int]$MaxAttempts = 3
)
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
$credential = Get-Credential -Message "Enter AD credentials with replication rights for $DomainName (attempt $attempt/$MaxAttempts)"
if ($null -eq $credential) {
throw "Credential prompt was cancelled."
}
try {
Get-ADDomain -Server $Server -Credential $credential -ErrorAction Stop | Out-Null
Write-Verbose ("Credential pre-check succeeded for '{0}' against '{1}'." -f $credential.UserName, $Server)
return $credential
} catch {
$message = $_.Exception.Message
if ($message -match 'rejected the client credentials|unknown user name|bad password|logon failure') {
Write-Warning ("Credentials were rejected for '{0}' (attempt {1}/{2})." -f $credential.UserName, $attempt, $MaxAttempts)
if ($attempt -lt $MaxAttempts) { continue }
throw "Credentials were rejected by domain controller '$Server' after $MaxAttempts attempts."
}
throw "Credential pre-check failed against '$Server': $message"
}
}
}
function Test-ReplicationPermissions {
param(
[Parameter(Mandatory)][string]$DomainDN,
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][System.Management.Automation.PSCredential]$Credential
)
$requiredRights = [ordered]@{
'Replicating Directory Changes' = [guid]'1131f6aa-9c07-11d1-f79f-00c04fc2dcd2'
'Replicating Directory Changes All' = [guid]'1131f6ab-9c07-11d1-f79f-00c04fc2dcd2'
'Replicating Directory Changes In Filtered Set' = [guid]'89e95b76-444d-4c62-991a-0facbeda640c'
}
$callerSids = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
try {
$samName = $Credential.UserName -replace '^.*\\', ''
$adUser = Get-ADUser -Identity $samName -Server $Server -Credential $Credential `
-Properties SID, MemberOf -ErrorAction Stop
[void]$callerSids.Add($adUser.SID.Value)
foreach ($groupDN in @($adUser.MemberOf)) {
try {
$g = Get-ADGroup -Identity $groupDN -Server $Server -Credential $Credential `
-Properties SID -ErrorAction Stop
[void]$callerSids.Add($g.SID.Value)
} catch { }
}
} catch {
Write-Warning ("Could not resolve account SIDs for replication permission pre-check: {0}. Skipping." -f $_.Exception.Message)
return
}
$acl = $null
try {
$de = New-Object System.DirectoryServices.DirectoryEntry(
"LDAP://$Server/$DomainDN",
$Credential.UserName,
$Credential.GetNetworkCredential().Password
)
$acl = $de.ObjectSecurity.GetAccessRules(
$true, $true, [System.Security.Principal.SecurityIdentifier])
} catch {
Write-Warning ("Could not read domain object ACL for replication permission pre-check: {0}. Skipping." -f $_.Exception.Message)
return
}
$missing = @()
foreach ($rightName in $requiredRights.Keys) {
$guid = $requiredRights[$rightName]
$granted = $false
foreach ($ace in $acl) {
if ($ace.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { continue }
if (-not ($ace.ActiveDirectoryRights -band [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight)) { continue }
if ($ace.ObjectType -ne $guid) { continue }
if ($callerSids.Contains($ace.IdentityReference.Value)) { $granted = $true; break }
}
if (-not $granted) { $missing += $rightName }
}
if ($missing.Count -gt 0) {
throw ("Account '{0}' is missing the following replication permissions on '{1}':`n - {2}`n`nGrant these extended rights on the domain object to allow DCSync-based hash retrieval." -f `
$Credential.UserName, $DomainDN, ($missing -join "`n - "))
}
Write-Host ("[+] Replication permissions verified for '{0}'." -f $Credential.UserName)
}
+1 -3
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Elysium.ps1 ## ## File: Elysium.ps1 ##
## Version: 2.2.0 ## ## Version: 2.2.1 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
@@ -52,8 +52,6 @@ if ([string]::IsNullOrEmpty($passphrase)) {
Write-Host "Passphrase found in environment variables." Write-Host "Passphrase found in environment variables."
} }
# Continue with the rest of your script...
function Start-OrchestratorTranscript { function Start-OrchestratorTranscript {
param([string]$BasePath) param([string]$BasePath)
try { try {
+1 -1
View File
@@ -8,7 +8,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: ElysiumSettings.txt ## ## File: ElysiumSettings.txt ##
## Version: 2.2.0 ## ## Version: 2.2.1 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
+173 -275
View File
@@ -6,8 +6,8 @@
## \____\__\_\_| \_\_____(_)_| \_|_____| |_| ## ## \____\__\_\_| \_\_____(_)_| \_|_____| |_| ##
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Extract-NTLMHashes.ps1 ## ## File: Extract-NTHashes.ps1 ##
## Version: 2.2.0 ## ## Version: 2.2.1 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
@@ -25,6 +25,11 @@ Set-StrictMode -Version Latest
$scriptRoot = $PSScriptRoot $scriptRoot = $PSScriptRoot
[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-WithWindowsPowerShellIfAvailable -BoundParameters $PSBoundParameters -UnboundArguments $MyInvocation.UnboundArguments
function Start-ExtractTranscript { function Start-ExtractTranscript {
param([string]$BasePath) param([string]$BasePath)
try { try {
@@ -40,167 +45,18 @@ function Start-ExtractTranscript {
function Stop-ExtractTranscript { try { Stop-Transcript | Out-Null } catch {} } function Stop-ExtractTranscript { try { Stop-Transcript | Out-Null } catch {} }
Start-ExtractTranscript -BasePath $scriptRoot
try {
# Import settings
Write-Host "Loading settings..."
$ElysiumSettings = @{}
$settingsPath = Join-Path -Path $scriptRoot -ChildPath "ElysiumSettings.txt"
if (-not (Test-Path $settingsPath)) {
Write-Error "Settings file not found at $settingsPath"
exit
}
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()
}
}
}
function Normalize-ReportPath([string]$p) { function Normalize-ReportPath([string]$p) {
if ([string]::IsNullOrWhiteSpace($p)) { return (Join-Path -Path $scriptRoot -ChildPath 'Reports') } if ([string]::IsNullOrWhiteSpace($p)) { return (Join-Path -Path $scriptRoot -ChildPath 'Reports') }
if ([System.IO.Path]::IsPathRooted($p)) { return $p } if ([System.IO.Path]::IsPathRooted($p)) { return $p }
return (Join-Path -Path $scriptRoot -ChildPath $p) return (Join-Path -Path $scriptRoot -ChildPath $p)
} }
# Storage provider selection (Azure by default)
$storageProvider = $ElysiumSettings['StorageProvider']
if ([string]::IsNullOrWhiteSpace($storageProvider)) { $storageProvider = 'Azure' }
# Azure settings
$storageAccountName = $ElysiumSettings['storageAccountName']
$containerName = $ElysiumSettings['containerName']
$sasToken = $ElysiumSettings['sasToken']
# S3-compatible settings
$s3EndpointUrl = $ElysiumSettings['s3EndpointUrl']
$s3Region = $ElysiumSettings['s3Region']
$s3BucketName = $ElysiumSettings['s3BucketName']
$s3AccessKeyId = $ElysiumSettings['s3AccessKeyId']
$s3SecretAccessKey = $ElysiumSettings['s3SecretAccessKey']
$s3ForcePathStyle = $ElysiumSettings['s3ForcePathStyle']
$s3UseAwsTools = $ElysiumSettings['s3UseAwsTools']
if ([string]::IsNullOrWhiteSpace($s3Region)) { $s3Region = 'us-east-1' }
try { $s3ForcePathStyle = [System.Convert]::ToBoolean($s3ForcePathStyle) } catch { $s3ForcePathStyle = $true }
try { $s3UseAwsTools = [System.Convert]::ToBoolean($s3UseAwsTools) } catch { $s3UseAwsTools = $false }
function Ensure-AWSS3Module {
# Ensure AWS SDK types are available via AWS Tools for PowerShell
try {
$null = [Amazon.S3.AmazonS3Client]
return
} catch {
try { Import-Module -Name AWS.Tools.S3 -ErrorAction Stop; return } catch {}
try { Import-Module -Name AWSPowerShell.NetCore -ErrorAction Stop; return } catch {}
throw "AWS Tools for PowerShell not found. Install with: Install-Module AWS.Tools.S3 -Scope CurrentUser"
}
}
function New-S3Client {
param(
[string]$EndpointUrl,
[string]$Region,
[string]$AccessKeyId,
[string]$SecretAccessKey,
[bool]$ForcePathStyle = $true
)
Ensure-AWSS3Module
$creds = New-Object Amazon.Runtime.BasicAWSCredentials($AccessKeyId, $SecretAccessKey)
$cfg = New-Object Amazon.S3.AmazonS3Config
if ($EndpointUrl) { $cfg.ServiceURL = $EndpointUrl }
if ($Region) {
try { $cfg.RegionEndpoint = [Amazon.RegionEndpoint]::GetBySystemName($Region) } catch {}
}
$cfg.ForcePathStyle = [bool]$ForcePathStyle
return (New-Object Amazon.S3.AmazonS3Client($creds, $cfg))
}
# Native S3 SigV4 (no AWS Tools) helpers
function Get-Bytes([string]$s) { return [System.Text.Encoding]::UTF8.GetBytes($s) }
function Get-HashHex([byte[]]$bytes) {
$sha = [System.Security.Cryptography.SHA256]::Create()
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()
$fs = [System.IO.File]::OpenRead($path) $fs = [System.IO.File]::OpenRead($path)
try { return ([BitConverter]::ToString($sha.ComputeHash($fs))).Replace('-', '').ToLowerInvariant() } finally { $fs.Close(); $sha.Dispose() } try { return ([BitConverter]::ToString($sha.ComputeHash($fs))).Replace('-', '').ToLowerInvariant() } finally { $fs.Close(); $sha.Dispose() }
} }
function HmacSha256([byte[]]$key, [string]$data) {
$h = [System.Security.Cryptography.HMACSHA256]::new($key)
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) {
$kDate = HmacSha256 (Get-Bytes ('AWS4' + $secret)) $dateStamp
$kRegion = HmacSha256 $kDate $regionName
$kService = HmacSha256 $kRegion $serviceName
return (HmacSha256 $kService 'aws4_request')
}
function UriEncode([string]$data, [bool]$encodeSlash) {
if ($null -eq $data) { return '' }
$enc = [System.Uri]::EscapeDataString($data)
if (-not $encodeSlash) { $enc = $enc -replace '%2F','/' }
return $enc
}
function BuildCanonicalPath([System.Uri]$uri) {
$segments = $uri.AbsolutePath.Split('/')
$encoded = @()
foreach ($seg in $segments) { $encoded += (UriEncode $seg $false) }
$path = ($encoded -join '/')
if (-not $path.StartsWith('/')) { $path = '/' + $path }
return $path
}
function ToHex([byte[]]$bytes) { return ([BitConverter]::ToString($bytes)).Replace('-', '').ToLowerInvariant() }
function BuildAuthHeaders($method, [System.Uri]$uri, [string]$region, [string]$accessKey, [string]$secretKey, [string]$payloadHash) {
$algorithm = 'AWS4-HMAC-SHA256'
$amzdate = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')
$datestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd')
$hostHeader = $uri.Host
if (-not $uri.IsDefaultPort) { $hostHeader = "{0}:{1}" -f $hostHeader, $uri.Port }
$canonicalUri = BuildCanonicalPath $uri
$canonicalQueryString = ''
$canonicalHeaders = "host:$hostHeader`n" + "x-amz-content-sha256:$payloadHash`n" + "x-amz-date:$amzdate`n"
$signedHeaders = 'host;x-amz-content-sha256;x-amz-date'
$canonicalRequest = "$method`n$canonicalUri`n$canonicalQueryString`n$canonicalHeaders`n$signedHeaders`n$payloadHash"
$credentialScope = "$datestamp/$region/s3/aws4_request"
$stringToSign = "$algorithm`n$amzdate`n$credentialScope`n$((Get-HashHex (Get-Bytes $canonicalRequest)))"
$signingKey = GetSignatureKey $secretKey $datestamp $region 's3'
$signature = ToHex (HmacSha256 $signingKey $stringToSign)
$authHeader = "$algorithm Credential=$accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature"
return @{ 'x-amz-date' = $amzdate; 'x-amz-content-sha256' = $payloadHash; 'Authorization' = $authHeader }
}
function BuildS3Uri([string]$endpointUrl, [string]$bucket, [string]$key, [bool]$forcePathStyle) {
$base = [System.Uri]$endpointUrl
$ub = [System.UriBuilder]::new($base)
if ($forcePathStyle) {
$p = ($ub.Path.TrimEnd('/'))
if ([string]::IsNullOrEmpty($p)) { $p = '/' }
$ub.Path = ($p.TrimEnd('/') + '/' + $bucket + '/' + $key)
} else {
$ub.Host = "$bucket." + $ub.Host
$p = $ub.Path.TrimEnd('/')
if ([string]::IsNullOrEmpty($p)) { $p = '/' }
$ub.Path = ($p.TrimEnd('/') + '/' + $key)
}
return $ub.Uri
}
function Invoke-S3PutFile([string]$endpointUrl, [string]$bucket, [string]$key, [string]$filePath, [string]$region, [string]$ak, [string]$sk, [bool]$forcePathStyle) { function Invoke-S3PutFile([string]$endpointUrl, [string]$bucket, [string]$key, [string]$filePath, [string]$region, [string]$ak, [string]$sk, [bool]$forcePathStyle) {
$uri = BuildS3Uri -endpointUrl $endpointUrl -bucket $bucket -key $key -forcePathStyle $forcePathStyle $uri = BuildS3Uri -endpointUrl $endpointUrl -bucket $bucket -key $key -forcePathStyle $forcePathStyle
$payloadHash = Get-FileSha256Hex -path $filePath $payloadHash = Get-FileSha256Hex -path $filePath
@@ -218,6 +74,7 @@ function Invoke-S3PutFile([string]$endpointUrl, [string]$bucket, [string]$key, [
if (-not $resp.IsSuccessStatusCode) { throw "S3 PUT failed: $([int]$resp.StatusCode) $($resp.ReasonPhrase)" } if (-not $resp.IsSuccessStatusCode) { throw "S3 PUT failed: $([int]$resp.StatusCode) $($resp.ReasonPhrase)" }
} finally { if ($req) { $req.Dispose() }; if ($stream) { $stream.Close(); $stream.Dispose() }; $client.Dispose() } } finally { if ($req) { $req.Dispose() }; if ($stream) { $stream.Close(); $stream.Dispose() }; $client.Dispose() }
} }
function Invoke-S3GetToFile([string]$endpointUrl, [string]$bucket, [string]$key, [string]$targetPath, [string]$region, [string]$ak, [string]$sk, [bool]$forcePathStyle) { function Invoke-S3GetToFile([string]$endpointUrl, [string]$bucket, [string]$key, [string]$targetPath, [string]$region, [string]$ak, [string]$sk, [bool]$forcePathStyle) {
$uri = BuildS3Uri -endpointUrl $endpointUrl -bucket $bucket -key $key -forcePathStyle $forcePathStyle $uri = BuildS3Uri -endpointUrl $endpointUrl -bucket $bucket -key $key -forcePathStyle $forcePathStyle
$payloadHash = (Get-HashHex (Get-Bytes '')) $payloadHash = (Get-HashHex (Get-Bytes ''))
@@ -236,13 +93,6 @@ function Invoke-S3GetToFile([string]$endpointUrl, [string]$bucket, [string]$key,
} finally { if ($req) { $req.Dispose() }; $client.Dispose() } } finally { if ($req) { $req.Dispose() }; $client.Dispose() }
} }
# 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 }
# Timestamp
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
function Protect-FileWithAES { function Protect-FileWithAES {
param ( param (
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@@ -255,7 +105,6 @@ function Protect-FileWithAES {
[string]$Passphrase [string]$Passphrase
) )
# Derive key with PBKDF2 (HMACSHA256) + random salt
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$salt = New-Object byte[] 16 $salt = New-Object byte[] 16
$rng.GetBytes($salt) $rng.GetBytes($salt)
@@ -277,7 +126,6 @@ function Protect-FileWithAES {
$outFileStream = [System.IO.File]::Create($OutputFile) $outFileStream = [System.IO.File]::Create($OutputFile)
try { try {
# File header: magic 'ELY1' (4 bytes), salt (16 bytes), IV (16 bytes)
$magic = [System.Text.Encoding]::ASCII.GetBytes('ELY1') $magic = [System.Text.Encoding]::ASCII.GetBytes('ELY1')
$outFileStream.Write($magic, 0, $magic.Length) $outFileStream.Write($magic, 0, $magic.Length)
$outFileStream.Write($salt, 0, $salt.Length) $outFileStream.Write($salt, 0, $salt.Length)
@@ -299,6 +147,7 @@ function Protect-FileWithAES {
Write-Host "File has been encrypted (PBKDF2+AES-256-CBC): $OutputFile" Write-Host "File has been encrypted (PBKDF2+AES-256-CBC): $OutputFile"
} }
function Get-FileChecksum { function Get-FileChecksum {
param ( param (
[string]$Path, [string]$Path,
@@ -309,142 +158,191 @@ function Get-FileChecksum {
try { try {
$hashBytes = $hasher.ComputeHash($stream) $hashBytes = $hasher.ComputeHash($stream)
return [BitConverter]::ToString($hashBytes) -replace '-', '' return [BitConverter]::ToString($hashBytes) -replace '-', ''
} } finally {
finally {
$stream.Close() $stream.Close()
$hasher.Dispose() $hasher.Dispose()
} }
} }
# Extract NTLM hashes Start-ExtractTranscript -BasePath $scriptRoot
$reportBase = Normalize-ReportPath -p $ElysiumSettings['ReportPathBase'] try {
if (-not (Test-Path $reportBase)) { New-Item -Path $reportBase -ItemType Directory -Force | Out-Null } Write-Host "Loading settings..."
$ElysiumSettings = Read-ElysiumSettings -ScriptRoot $scriptRoot
# Build domain details from settings (ordered to keep numeric index order) # Storage provider selection (Azure by default)
$DomainDetails = [ordered]@{} $storageProvider = $ElysiumSettings['StorageProvider']
for ($i = 1; $ElysiumSettings.ContainsKey("Domain${i}Name"); $i++) { if ([string]::IsNullOrWhiteSpace($storageProvider)) { $storageProvider = 'Azure' }
$DomainDetails["$i"] = @{
Name = $ElysiumSettings["Domain${i}Name"]
DC = $ElysiumSettings["Domain${i}DC"]
DA = $ElysiumSettings["Domain${i}DA"]
}
}
# User selects a domain # Azure settings
Write-Host "Select a domain to extract NTLM hashes:" $storageAccountName = $ElysiumSettings['storageAccountName']
$DomainDetails.GetEnumerator() | Sort-Object { [int]$_.Key } | ForEach-Object { Write-Host "$($_.Key): $($_.Value.Name)" } $containerName = $ElysiumSettings['containerName']
$selection = Read-Host "Enter the number of the domain" $sasToken = $ElysiumSettings['sasToken']
$selectedDomain = $DomainDetails[$selection]
if (-not $selectedDomain) { # S3-compatible settings
Write-Error "Invalid selection." $s3EndpointUrl = $ElysiumSettings['s3EndpointUrl']
exit $s3Region = $ElysiumSettings['s3Region']
} $s3BucketName = $ElysiumSettings['s3BucketName']
$s3AccessKeyId = $ElysiumSettings['s3AccessKeyId']
$s3SecretAccessKey = $ElysiumSettings['s3SecretAccessKey']
$s3ForcePathStyle = $ElysiumSettings['s3ForcePathStyle']
$s3UseAwsTools = $ElysiumSettings['s3UseAwsTools']
if ([string]::IsNullOrWhiteSpace($s3Region)) { $s3Region = 'us-east-1' }
try { $s3ForcePathStyle = [System.Convert]::ToBoolean($s3ForcePathStyle) } catch { $s3ForcePathStyle = $true }
try { $s3UseAwsTools = [System.Convert]::ToBoolean($s3UseAwsTools) } catch { $s3UseAwsTools = $false }
# Update script variables based on selected domain # Retrieve the passphrase from a user environment variable
$domainController = $selectedDomain.DC $passphrase = [System.Environment]::GetEnvironmentVariable("ELYSIUM_PASSPHRASE", [System.EnvironmentVariableTarget]::User)
$credential = Get-Credential -Message "Enter AD credentials with replication rights for $($selectedDomain.Name)" if ([string]::IsNullOrWhiteSpace($passphrase)) { throw 'Passphrase not found in ELYSIUM_PASSPHRASE environment variable.' }
$domainPrefix = ($selectedDomain.Name -replace "\W", "_") $timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$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 | $reportBase = Normalize-ReportPath -p $ElysiumSettings['ReportPathBase']
Where-Object { $_.NTHash } | if (-not (Test-Path $reportBase)) { New-Item -Path $reportBase -ItemType Directory -Force | Out-Null }
ForEach-Object { [BitConverter]::ToString($_.NTHash).Replace("-", "") } |
Sort-Object -Unique
$ntlmHashes | Out-File -FilePath $exportPath # Build domain details from settings (ordered to keep numeric index order)
Write-Host "NTLM hashes have been extracted to: $exportPath" $DomainDetails = [ordered]@{}
for ($i = 1; $ElysiumSettings.ContainsKey("Domain${i}Name"); $i++) {
# Compress extracted NTLM hashes $DomainDetails["$i"] = @{
Compress-Archive -Path $exportPath -DestinationPath $compressedFilePath Name = $ElysiumSettings["Domain${i}Name"]
Write-Host "File has been compressed: $compressedFilePath" DC = $ElysiumSettings["Domain${i}DC"]
DA = $ElysiumSettings["Domain${i}DA"]
# Encrypt the compressed file
Protect-FileWithAES -InputFile $compressedFilePath -OutputFile $encryptedFilePath -Passphrase $passphrase
Write-Host "File has been encrypted: $encryptedFilePath"
# Calculate the local file checksum
$localFileChecksum = Get-FileChecksum -Path $encryptedFilePath
if ($storageProvider -ieq 'S3') {
# S3-compatible path (e.g., IDrive e2) without requiring AWS Tools
if ([string]::IsNullOrWhiteSpace($s3BucketName)) { Write-Error 's3BucketName is missing in settings.'; exit }
if ([string]::IsNullOrWhiteSpace($s3AccessKeyId) -or [string]::IsNullOrWhiteSpace($s3SecretAccessKey)) { Write-Error 's3AccessKeyId / s3SecretAccessKey missing in settings.'; exit }
if ([string]::IsNullOrWhiteSpace($s3EndpointUrl)) { Write-Error 's3EndpointUrl is required for S3-compatible storage.'; exit }
$usedAwsTools = $false
if ($s3UseAwsTools) {
try {
$s3Client = New-S3Client -EndpointUrl $s3EndpointUrl -Region $s3Region -AccessKeyId $s3AccessKeyId -SecretAccessKey $s3SecretAccessKey -ForcePathStyle:$s3ForcePathStyle
# Upload
$putReq = New-Object Amazon.S3.Model.PutObjectRequest -Property @{ BucketName = $s3BucketName; Key = $blobName; FilePath = $encryptedFilePath }
$null = $s3Client.PutObject($putReq)
Write-Host "Encrypted file uploaded to S3-compatible bucket (AWS Tools): $blobName"
$tempDownloadPath = [System.IO.Path]::GetTempFileName()
$getReq = New-Object Amazon.S3.Model.GetObjectRequest -Property @{ BucketName = $s3BucketName; Key = $blobName }
$getResp = $s3Client.GetObject($getReq)
$getResp.WriteResponseStreamToFile($tempDownloadPath, $true)
$getResp.Dispose()
$downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
$usedAwsTools = $true
} catch {
Write-Warning "AWS Tools path failed or not available. Falling back to native HTTP (SigV4). Details: $($_.Exception.Message)"
$usedAwsTools = $false
} }
} }
if (-not $usedAwsTools) { # User selects a domain
Invoke-S3PutFile -endpointUrl $s3EndpointUrl -bucket $s3BucketName -key $blobName -filePath $encryptedFilePath -region $s3Region -ak $s3AccessKeyId -sk $s3SecretAccessKey -forcePathStyle:$s3ForcePathStyle Write-Host "Select a domain to extract NTLM hashes:"
Write-Host "Encrypted file uploaded to S3-compatible bucket: $blobName" $DomainDetails.GetEnumerator() | Sort-Object { [int]$_.Key } | ForEach-Object { Write-Host "$($_.Key): $($_.Value.Name)" }
$tempDownloadPath = [System.IO.Path]::GetTempFileName() $selection = Read-Host "Enter the number of the domain"
Invoke-S3GetToFile -endpointUrl $s3EndpointUrl -bucket $s3BucketName -key $blobName -targetPath $tempDownloadPath -region $s3Region -ak $s3AccessKeyId -sk $s3SecretAccessKey -forcePathStyle:$s3ForcePathStyle $selectedDomain = $DomainDetails[$selection]
$downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
if (-not $selectedDomain) {
throw "Invalid selection."
} }
}
else {
# Azure Blob Storage path (default)
$sas = $sasToken
if ([string]::IsNullOrWhiteSpace($sas)) { Write-Error 'sasToken is missing in settings.'; exit }
$sas = $sas.Trim(); if (-not $sas.StartsWith('?')) { $sas = '?' + $sas }
try { Import-Module Az.Storage -ErrorAction Stop } catch {}
$storageContext = New-AzStorageContext -StorageAccountName $storageAccountName -SasToken $sas
# Ensure container exists $domainController = $selectedDomain.DC
$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 # Validate credentials and replication permissions before attempting DCSync
Set-AzStorageBlobContent -File $encryptedFilePath -Container $containerName -Blob $blobName -Context $storageContext | Out-Null $hasADModule = $null -ne (Get-Module -Name ActiveDirectory -ErrorAction SilentlyContinue)
Write-Host "Encrypted file uploaded to Azure Blob Storage: $blobName" if (-not $hasADModule) {
try { Import-Module ActiveDirectory -ErrorAction Stop; $hasADModule = $true } catch {}
}
# Download the blob to a temporary location to verify if ($hasADModule) {
$tempDownloadPath = [System.IO.Path]::GetTempFileName() $credential = Get-ValidatedADCredential -DomainName $selectedDomain.Name -Server $domainController
Get-AzStorageBlobContent -Blob $blobName -Container $containerName -Context $storageContext -Destination $tempDownloadPath -Force | Out-Null try {
$domainInfo = Get-ADDomain -Server $domainController -Credential $credential -ErrorAction Stop
# Calculate the downloaded file checksum Test-ReplicationPermissions -DomainDN $domainInfo.DistinguishedName `
$downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath -Server $domainController -Credential $credential
} } catch {
throw $_.Exception.Message
# 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
if ($storageProvider -ieq 'S3') {
Write-Host "Local and temporary files cleaned up after uploading to S3-compatible storage."
} else { } else {
Write-Host "Local and temporary files cleaned up after uploading to Azure Blob Storage." Write-Warning "ActiveDirectory module not available; skipping credential pre-check and replication permission verification."
$credential = Get-Credential -Message "Enter AD credentials with replication rights for $($selectedDomain.Name)"
if ($null -eq $credential) { throw "Credential prompt was cancelled." }
}
$domainPrefix = ($selectedDomain.Name -replace "\W", "_")
$baseName = "${domainPrefix}_NTLM_Hashes_$timestamp"
$blobName = "$baseName.enc"
# Use a temp directory for all sensitive intermediate files so they are
# never written to the installation directory and are always cleaned up.
$tmpDir = New-Item -ItemType Directory -Path ([System.IO.Path]::Combine(
[System.IO.Path]::GetTempPath(), "elysium-extract-" + [System.Guid]::NewGuid())) -Force
$exportPath = Join-Path -Path $tmpDir.FullName -ChildPath "$baseName.txt"
$compressedFilePath = Join-Path -Path $tmpDir.FullName -ChildPath "$baseName.zip"
$encryptedFilePath = Join-Path -Path $tmpDir.FullName -ChildPath "$baseName.enc"
$tempDownloadPath = $null
try {
$ntlmHashes = Get-ADReplAccount -All -Server $domainController -Credential $credential |
Where-Object { $_.NTHash } |
ForEach-Object { [BitConverter]::ToString($_.NTHash).Replace("-", "") } |
Sort-Object -Unique
$ntlmHashes | Out-File -FilePath $exportPath
Write-Host "NTLM hashes have been extracted to temporary file."
Compress-Archive -Path $exportPath -DestinationPath $compressedFilePath
Write-Host "File has been compressed."
Protect-FileWithAES -InputFile $compressedFilePath -OutputFile $encryptedFilePath -Passphrase $passphrase
$localFileChecksum = Get-FileChecksum -Path $encryptedFilePath
if ($storageProvider -ieq 'S3') {
if ([string]::IsNullOrWhiteSpace($s3BucketName)) { throw 's3BucketName is missing in settings.' }
if ([string]::IsNullOrWhiteSpace($s3AccessKeyId) -or [string]::IsNullOrWhiteSpace($s3SecretAccessKey)) { throw 's3AccessKeyId / s3SecretAccessKey missing in settings.' }
if ([string]::IsNullOrWhiteSpace($s3EndpointUrl)) { throw 's3EndpointUrl is required for S3-compatible storage.' }
$usedAwsTools = $false
if ($s3UseAwsTools) {
try {
$s3Client = New-S3Client -EndpointUrl $s3EndpointUrl -Region $s3Region -AccessKeyId $s3AccessKeyId -SecretAccessKey $s3SecretAccessKey -ForcePathStyle:$s3ForcePathStyle
$putReq = New-Object Amazon.S3.Model.PutObjectRequest -Property @{ BucketName = $s3BucketName; Key = $blobName; FilePath = $encryptedFilePath }
$null = $s3Client.PutObject($putReq)
Write-Host "Encrypted file uploaded to S3-compatible bucket (AWS Tools): $blobName"
$tempDownloadPath = [System.IO.Path]::GetTempFileName()
$getReq = New-Object Amazon.S3.Model.GetObjectRequest -Property @{ BucketName = $s3BucketName; Key = $blobName }
$getResp = $s3Client.GetObject($getReq)
$getResp.WriteResponseStreamToFile($tempDownloadPath, $true)
$getResp.Dispose()
$usedAwsTools = $true
} catch {
Write-Warning "AWS Tools path failed or not available. Falling back to native HTTP (SigV4). Details: $($_.Exception.Message)"
$usedAwsTools = $false
}
}
if (-not $usedAwsTools) {
Invoke-S3PutFile -endpointUrl $s3EndpointUrl -bucket $s3BucketName -key $blobName -filePath $encryptedFilePath -region $s3Region -ak $s3AccessKeyId -sk $s3SecretAccessKey -forcePathStyle:$s3ForcePathStyle
Write-Host "Encrypted file uploaded to S3-compatible bucket: $blobName"
$tempDownloadPath = [System.IO.Path]::GetTempFileName()
Invoke-S3GetToFile -endpointUrl $s3EndpointUrl -bucket $s3BucketName -key $blobName -targetPath $tempDownloadPath -region $s3Region -ak $s3AccessKeyId -sk $s3SecretAccessKey -forcePathStyle:$s3ForcePathStyle
}
} else {
$sas = $sasToken
if ([string]::IsNullOrWhiteSpace($sas)) { throw 'sasToken is missing in settings.' }
$sas = $sas.Trim(); if (-not $sas.StartsWith('?')) { $sas = '?' + $sas }
try { Import-Module Az.Storage -ErrorAction Stop } catch {}
$storageContext = New-AzStorageContext -StorageAccountName $storageAccountName -SasToken $sas
$container = Get-AzStorageContainer -Name $containerName -Context $storageContext -ErrorAction SilentlyContinue
if (-not $container) { throw "Azure container '$containerName' not found or access denied." }
Set-AzStorageBlobContent -File $encryptedFilePath -Container $containerName -Blob $blobName -Context $storageContext | Out-Null
Write-Host "Encrypted file uploaded to Azure Blob Storage: $blobName"
$tempDownloadPath = [System.IO.Path]::GetTempFileName()
Get-AzStorageBlobContent -Blob $blobName -Container $containerName -Context $storageContext -Destination $tempDownloadPath -Force | Out-Null
}
$downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
if ($localFileChecksum -eq $downloadedFileChecksum) {
Write-Host "The file was correctly uploaded. Checksum verified."
Remove-Item -Path $encryptedFilePath -Force
Remove-Item -Path $tempDownloadPath -Force
if ($storageProvider -ieq 'S3') {
Write-Host "Upload to S3-compatible storage completed and verified."
} else {
Write-Host "Upload to Azure Blob Storage completed and verified."
}
} else {
Write-Warning "Checksum verification failed. Encrypted file preserved for investigation: $encryptedFilePath"
if ($tempDownloadPath -and (Test-Path $tempDownloadPath)) {
Remove-Item -Path $tempDownloadPath -Force -ErrorAction SilentlyContinue
}
}
} finally {
# Always delete plaintext hashes and compressed archive regardless of outcome.
foreach ($f in @($exportPath, $compressedFilePath)) {
if ($f -and (Test-Path $f)) {
Remove-Item -Path $f -Force -ErrorAction SilentlyContinue
}
}
} }
}
else {
Write-Warning "Checksum verification failed. Keeping local artifacts for investigation: $exportPath, $compressedFilePath, $encryptedFilePath"
if (Test-Path $tempDownloadPath) { Remove-Item -Path $tempDownloadPath -Force }
}
Write-Host "Script execution completed." Write-Host "Script execution completed."
} finally { } finally {
+1 -155
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Prepare-KHDBStorage.ps1 ## ## File: Prepare-KHDBStorage.ps1 ##
## Version: 2.2.0 ## ## Version: 2.2.1 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
@@ -100,44 +100,6 @@ function Remove-DirectoryContents {
} }
} }
function Read-KeyValueSettingsFile {
param([string]$Path)
$result = @{}
if (-not (Test-Path -LiteralPath $Path)) { return $result }
foreach ($line in (Get-Content -LiteralPath $Path)) {
if ($null -eq $line) { continue }
$trimmed = $line.Trim()
if (-not $trimmed) { continue }
if ($trimmed.StartsWith('#')) { continue }
$kv = $line -split '=', 2
if ($kv.Count -ne 2) { continue }
$key = $kv[0].Trim()
$value = $kv[1].Trim()
if (-not $key) { continue }
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-SettingsValue {
param(
[hashtable]$Settings,
[string]$Key
)
if (-not $Settings) { return $null }
if ($Settings.ContainsKey($Key)) { return $Settings[$Key] }
return $null
}
function Get-FunctionDefinitionText {
param([Parameter(Mandatory = $true)][string]$Name)
$cmd = Get-Command -Name $Name -CommandType Function -ErrorAction Stop
return $cmd.ScriptBlock.Ast.Extent.Text
}
function Merge-ShardsToFile { function Merge-ShardsToFile {
param( param(
[psobject]$Manifest, [psobject]$Manifest,
@@ -176,27 +138,6 @@ function Get-NormalizedForwardPath {
return $PathValue.Replace('\', '/').Trim('/') return $PathValue.Replace('\', '/').Trim('/')
} }
function Build-BlobUri {
param(
[string]$Account,
[string]$Container,
[string]$Sas,
[string]$BlobName
)
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.' }
if ([string]::IsNullOrWhiteSpace($BlobName)) { throw 'BlobName cannot be empty.' }
$sas = $Sas.Trim()
if (-not $sas.StartsWith('?')) { $sas = '?' + $sas }
$normalizedBlob = $BlobName.Replace('\', '/').TrimStart('/')
$builder = [System.UriBuilder]::new("https://$Account.blob.core.windows.net/$Container/$normalizedBlob")
$builder.Query = $sas.TrimStart('?')
return $builder.Uri.AbsoluteUri
}
function Upload-AzureBlob { function Upload-AzureBlob {
param( param(
[string]$Account, [string]$Account,
@@ -232,88 +173,6 @@ function Upload-AzureBlob {
} }
} }
function Get-Bytes([string]$s) { return [System.Text.Encoding]::UTF8.GetBytes($s) }
function Get-HashHex([byte[]]$bytes) {
if ($null -eq $bytes) { $bytes = [byte[]]@() }
$sha = [System.Security.Cryptography.SHA256]::Create()
try {
$ms = New-Object System.IO.MemoryStream -ArgumentList (,$bytes)
try {
$hash = $sha.ComputeHash([System.IO.Stream]$ms)
} finally { $ms.Dispose() }
return ([BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant()
} finally { $sha.Dispose() }
}
function HmacSha256([byte[]]$key, [string]$data) {
$h = [System.Security.Cryptography.HMACSHA256]::new($key)
try {
$b = [System.Text.Encoding]::UTF8.GetBytes($data)
$ms = New-Object System.IO.MemoryStream -ArgumentList (,$b)
try {
return $h.ComputeHash([System.IO.Stream]$ms)
} finally { $ms.Dispose() }
} finally { $h.Dispose() }
}
function GetSignatureKey([string]$secret, [string]$dateStamp, [string]$regionName, [string]$serviceName) {
$kDate = HmacSha256 (Get-Bytes ('AWS4' + $secret)) $dateStamp
$kRegion = HmacSha256 $kDate $regionName
$kService = HmacSha256 $kRegion $serviceName
HmacSha256 $kService 'aws4_request'
}
function UriEncode([string]$data, [bool]$encodeSlash) {
$enc = [System.Uri]::EscapeDataString($data)
if (-not $encodeSlash) { $enc = $enc -replace '%2F', '/' }
return $enc
}
function BuildCanonicalPath([System.Uri]$uri) {
$segments = $uri.AbsolutePath.Split('/')
$encoded = @()
foreach ($s in $segments) { $encoded += (UriEncode $s $false) }
$path = ($encoded -join '/')
if (-not $path.StartsWith('/')) { $path = '/' + $path }
return $path
}
function ToHex([byte[]]$b) { ([BitConverter]::ToString($b)).Replace('-', '').ToLowerInvariant() }
function BuildAuthHeaders($method, [System.Uri]$uri, [string]$region, [string]$accessKey, [string]$secretKey, [string]$payloadHash) {
$algorithm = 'AWS4-HMAC-SHA256'
$timestamp = (Get-Date).ToUniversalTime()
$amzDate = $timestamp.ToString('yyyyMMddTHHmmssZ')
$dateStamp = $timestamp.ToString('yyyyMMdd')
$hostHeader = $uri.Host
if (-not $uri.IsDefaultPort) { $hostHeader = "${hostHeader}:$($uri.Port)" }
$canonicalUri = BuildCanonicalPath $uri
$canonicalQueryString = ''
$canonicalHeaders = "host:$hostHeader`n" + "x-amz-content-sha256:$payloadHash`n" + "x-amz-date:$amzDate`n"
$signedHeaders = 'host;x-amz-content-sha256;x-amz-date'
$canonicalRequest = "$method`n$canonicalUri`n$canonicalQueryString`n$canonicalHeaders`n$signedHeaders`n$payloadHash"
$credentialScope = "$dateStamp/$region/s3/aws4_request"
$stringToSign = "$algorithm`n$amzDate`n$credentialScope`n$((Get-HashHex (Get-Bytes $canonicalRequest)))"
$signingKey = GetSignatureKey $secretKey $dateStamp $region 's3'
$signature = ToHex (HmacSha256 $signingKey $stringToSign)
$authHeader = "$algorithm Credential=$accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature"
@{
'x-amz-date' = $amzDate
'x-amz-content-sha256' = $payloadHash
'Authorization' = $authHeader
}
}
function BuildS3Uri([string]$endpointUrl, [string]$bucket, [string]$key, [bool]$forcePathStyle) {
$base = [System.Uri]$endpointUrl
$builder = [System.UriBuilder]::new($base)
$normalizedKey = $key.Replace('\', '/').TrimStart('/')
if ($forcePathStyle) {
$path = $builder.Path.TrimEnd('/')
if ([string]::IsNullOrEmpty($path)) { $path = '/' }
$builder.Path = ($path.TrimEnd('/') + '/' + $bucket + '/' + $normalizedKey)
} else {
$builder.Host = "$bucket." + $builder.Host
$path = $builder.Path.TrimEnd('/')
if ([string]::IsNullOrEmpty($path)) { $path = '/' }
$builder.Path = ($path.TrimEnd('/') + '/' + $normalizedKey)
}
return $builder.Uri
}
function Invoke-S3HttpUpload { function Invoke-S3HttpUpload {
param( param(
[string]$EndpointUrl, [string]$EndpointUrl,
@@ -356,19 +215,6 @@ function Invoke-S3HttpUpload {
} }
} }
function Combine-StoragePath {
param(
[string]$Prefix,
[string]$Name
)
$cleanName = $Name.Replace('\', '/').TrimStart('/')
if ([string]::IsNullOrWhiteSpace($Prefix)) { return $cleanName }
$normalizedPrefix = $Prefix.Replace('\', '/').Trim('/')
if ([string]::IsNullOrEmpty($normalizedPrefix)) { return $cleanName }
return "$normalizedPrefix/$cleanName"
}
function Split-KhdbIntoShards { function Split-KhdbIntoShards {
param( param(
[string]$Source, [string]$Source,
-22
View File
@@ -1,22 +0,0 @@
# 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 -126
View File
@@ -8,7 +8,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Test-WeakADPasswords.ps1 ## ## File: Test-WeakADPasswords.ps1 ##
## Version: 2.2.0 ## ## Version: 2.2.1 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
@@ -21,10 +21,13 @@ Weak AD password finder component of Elysium tool.
This script will test the passwords of selected domain (defined in ElysiumSettings.txt) using DSInternals' Test-PasswordQuality cmdlet. It writes its output to a report file which is meant to be shared with the internal security team. The report now includes UPNs for each account mentioned. This script will test the passwords of selected domain (defined in ElysiumSettings.txt) using DSInternals' Test-PasswordQuality cmdlet. It writes its output to a report file which is meant to be shared with the internal security team. The report now includes UPNs for each account mentioned.
#> #>
# Enable verbose output
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest 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
$VerbosePreference = "SilentlyContinue" $VerbosePreference = "SilentlyContinue"
$scriptRoot = $PSScriptRoot $scriptRoot = $PSScriptRoot
@@ -92,7 +95,7 @@ function Invoke-UsageBeacon {
if ($normalizedMethod -in @('POST', 'PUT')) { if ($normalizedMethod -in @('POST', 'PUT')) {
$payload = [ordered]@{ $payload = [ordered]@{
script = 'Test-WeakADPasswords' script = 'Test-WeakADPasswords'
version = '1.4.5' version = '2.2.1'
ranAtUtc = (Get-Date).ToUniversalTime().ToString('o') ranAtUtc = (Get-Date).ToUniversalTime().ToString('o')
} }
if (-not [string]::IsNullOrWhiteSpace($InstanceId)) { if (-not [string]::IsNullOrWhiteSpace($InstanceId)) {
@@ -124,32 +127,9 @@ $footer = "`r`n==== End of Report ===="
Start-TestTranscript -BasePath $scriptRoot Start-TestTranscript -BasePath $scriptRoot
try { try {
# Import settings
Write-Verbose "Loading settings..." Write-Verbose "Loading settings..."
$ElysiumSettings = @{} $ElysiumSettings = Read-ElysiumSettings -ScriptRoot $scriptRoot
$settingsPath = Join-Path -Path $scriptRoot -ChildPath "ElysiumSettings.txt" Write-Verbose "Settings loaded successfully."
# 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
}
$usageBeaconUrl = $ElysiumSettings['UsageBeaconUrl'] $usageBeaconUrl = $ElysiumSettings['UsageBeaconUrl']
$usageBeaconMethod = $ElysiumSettings['UsageBeaconMethod'] $usageBeaconMethod = $ElysiumSettings['UsageBeaconMethod']
@@ -561,104 +541,6 @@ function Resolve-DSInternalsWeakHashFile {
} }
} }
function Get-ValidatedADCredential {
param (
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$Server,
[int]$MaxAttempts = 3
)
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
$credential = Get-Credential -Message "Enter AD credentials with replication rights for $DomainName (attempt $attempt/$MaxAttempts)"
if ($null -eq $credential) {
throw "Credential prompt was cancelled."
}
try {
Get-ADDomain -Server $Server -Credential $credential -ErrorAction Stop | Out-Null
Write-Verbose ("Credential pre-check succeeded for '{0}' against '{1}'." -f $credential.UserName, $Server)
return $credential
} catch {
$message = $_.Exception.Message
if ($message -match 'rejected the client credentials|unknown user name|bad password|logon failure') {
Write-Warning ("Credentials were rejected for '{0}' (attempt {1}/{2})." -f $credential.UserName, $attempt, $MaxAttempts)
if ($attempt -lt $MaxAttempts) { continue }
throw "Credentials were rejected by domain controller '$Server' after $MaxAttempts attempts."
}
throw "Credential pre-check failed against '$Server': $message"
}
}
}
function Test-ReplicationPermissions {
param(
[Parameter(Mandatory)][string]$DomainDN,
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][System.Management.Automation.PSCredential]$Credential
)
$requiredRights = [ordered]@{
'Replicating Directory Changes' = [guid]'1131f6aa-9c07-11d1-f79f-00c04fc2dcd2'
'Replicating Directory Changes All' = [guid]'1131f6ab-9c07-11d1-f79f-00c04fc2dcd2'
'Replicating Directory Changes In Filtered Set' = [guid]'89e95b76-444d-4c62-991a-0facbeda640c'
}
# Collect caller SID + direct group SIDs so we can match ACEs below
$callerSids = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
try {
$samName = $Credential.UserName -replace '^.*\\', ''
$adUser = Get-ADUser -Identity $samName -Server $Server -Credential $Credential `
-Properties SID, MemberOf -ErrorAction Stop
[void]$callerSids.Add($adUser.SID.Value)
foreach ($groupDN in @($adUser.MemberOf)) {
try {
$g = Get-ADGroup -Identity $groupDN -Server $Server -Credential $Credential `
-Properties SID -ErrorAction Stop
[void]$callerSids.Add($g.SID.Value)
} catch { }
}
} catch {
Write-Warning ("Could not resolve account SIDs for replication permission pre-check: {0}. Skipping." -f $_.Exception.Message)
return
}
# Read the domain object's DACL via ADSI so we can use the provided credential
$acl = $null
try {
$de = New-Object System.DirectoryServices.DirectoryEntry(
"LDAP://$Server/$DomainDN",
$Credential.UserName,
$Credential.GetNetworkCredential().Password
)
# Translate all trustees to SID form for consistent comparison
$acl = $de.ObjectSecurity.GetAccessRules(
$true, $true, [System.Security.Principal.SecurityIdentifier])
} catch {
Write-Warning ("Could not read domain object ACL for replication permission pre-check: {0}. Skipping." -f $_.Exception.Message)
return
}
$missing = @()
foreach ($rightName in $requiredRights.Keys) {
$guid = $requiredRights[$rightName]
$granted = $false
foreach ($ace in $acl) {
if ($ace.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { continue }
if (-not ($ace.ActiveDirectoryRights -band [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight)) { continue }
if ($ace.ObjectType -ne $guid) { continue }
if ($callerSids.Contains($ace.IdentityReference.Value)) { $granted = $true; break }
}
if (-not $granted) { $missing += $rightName }
}
if ($missing.Count -gt 0) {
throw ("Account '{0}' is missing the following replication permissions on '{1}':`n - {2}`n`nGrant these extended rights on the domain object to allow DCSync-based hash retrieval." -f `
$Credential.UserName, $DomainDN, ($missing -join "`n - "))
}
Write-Verbose ("Replication permission pre-check passed for '{0}'." -f $Credential.UserName)
}
# Function to test for weak AD passwords # Function to test for weak AD passwords
function Test-WeakADPasswords { function Test-WeakADPasswords {
param ( param (
+1 -1
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Uninstall.ps1 ## ## File: Uninstall.ps1 ##
## Version: 2.2.0 ## ## Version: 2.2.1 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
+98 -261
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Update-KHDB.ps1 ## ## File: Update-KHDB.ps1 ##
## Version: 2.2.0 ## ## Version: 2.2.1 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################
@@ -50,21 +50,6 @@ function Stop-UpdateTranscript {
try { Stop-Transcript | Out-Null } catch {} try { Stop-Transcript | Out-Null } catch {}
} }
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("'")
}
}
}
return $settings
}
function Get-InstallationPath([hashtable]$settings) { function Get-InstallationPath([hashtable]$settings) {
$p = $settings['InstallationPath'] $p = $settings['InstallationPath']
if ([string]::IsNullOrWhiteSpace($p)) { return $scriptRoot } if ([string]::IsNullOrWhiteSpace($p)) { return $scriptRoot }
@@ -76,218 +61,10 @@ function New-HttpClient {
Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue
$client = [System.Net.Http.HttpClient]::new() $client = [System.Net.Http.HttpClient]::new()
$client.Timeout = [TimeSpan]::FromSeconds(600) $client.Timeout = [TimeSpan]::FromSeconds(600)
$client.DefaultRequestHeaders.UserAgent.ParseAdd('Elysium/2.1.1 (+Update-KHDB)') $client.DefaultRequestHeaders.UserAgent.ParseAdd('Elysium/2.2.1 (+Update-KHDB)')
return $client return $client
} }
function Build-BlobUri {
param(
[string]$Account,
[string]$Container,
[string]$Sas,
[string]$BlobName
)
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.' }
if ([string]::IsNullOrWhiteSpace($BlobName)) { throw 'BlobName cannot be empty.' }
$sas = $Sas.Trim()
if (-not $sas.StartsWith('?')) { $sas = '?' + $sas }
$normalizedBlob = $BlobName.Replace('\', '/').TrimStart('/')
$uriBuilder = [System.UriBuilder]::new("https://$Account.blob.core.windows.net/$Container/$normalizedBlob")
$uriBuilder.Query = $sas.TrimStart('?')
return $uriBuilder.Uri.AbsoluteUri
}
function Ensure-AWSS3Module {
try { $null = [Amazon.S3.AmazonS3Client]; return } catch {}
try { Import-Module -Name AWS.Tools.S3 -ErrorAction Stop; return } catch {}
try { Import-Module -Name AWSPowerShell.NetCore -ErrorAction Stop; return } catch {}
throw "AWS Tools for PowerShell not found. Install with: Install-Module AWS.Tools.S3 -Scope CurrentUser"
}
function Get-FunctionDefinitionText {
param([Parameter(Mandatory = $true)][string]$Name)
$cmd = Get-Command -Name $Name -CommandType Function -ErrorAction Stop
return $cmd.ScriptBlock.Ast.Extent.Text
}
function New-S3Client {
param(
[string]$EndpointUrl,
[string]$Region,
[string]$AccessKeyId,
[string]$SecretAccessKey,
[bool]$ForcePathStyle = $true
)
Ensure-AWSS3Module
$creds = New-Object Amazon.Runtime.BasicAWSCredentials($AccessKeyId, $SecretAccessKey)
$cfg = New-Object Amazon.S3.AmazonS3Config
if ($EndpointUrl) { $cfg.ServiceURL = $EndpointUrl }
if ($Region) { try { $cfg.RegionEndpoint = [Amazon.RegionEndpoint]::GetBySystemName($Region) } catch {} }
$cfg.ForcePathStyle = [bool]$ForcePathStyle
return (New-Object Amazon.S3.AmazonS3Client($creds, $cfg))
}
function Get-Bytes([string]$s) { return [System.Text.Encoding]::UTF8.GetBytes($s) }
function Get-HashHex([byte[]]$bytes) {
if ($null -eq $bytes) { $bytes = [byte[]]@() }
$sha = [System.Security.Cryptography.SHA256]::Create()
try {
$ms = New-Object System.IO.MemoryStream -ArgumentList (,$bytes)
try {
$hash = $sha.ComputeHash([System.IO.Stream]$ms)
} finally { $ms.Dispose() }
return ([BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant()
} finally { $sha.Dispose() }
}
function HmacSha256([byte[]]$key, [string]$data) {
$h = [System.Security.Cryptography.HMACSHA256]::new($key)
try {
$b = [System.Text.Encoding]::UTF8.GetBytes($data)
$ms = New-Object System.IO.MemoryStream -ArgumentList (,$b)
try {
return $h.ComputeHash([System.IO.Stream]$ms)
} finally { $ms.Dispose() }
} finally { $h.Dispose() }
}
function GetSignatureKey([string]$secret, [string]$dateStamp, [string]$regionName, [string]$serviceName) {
$kDate = HmacSha256 (Get-Bytes ('AWS4' + $secret)) $dateStamp
$kRegion = HmacSha256 $kDate $regionName
$kService = HmacSha256 $kRegion $serviceName
HmacSha256 $kService 'aws4_request'
}
function UriEncode([string]$data, [bool]$encodeSlash) {
$enc = [System.Uri]::EscapeDataString($data)
if (-not $encodeSlash) { $enc = $enc -replace '%2F', '/' }
return $enc
}
function BuildCanonicalPath([System.Uri]$uri) {
$segments = $uri.AbsolutePath.Split('/')
$encoded = @()
foreach ($s in $segments) { $encoded += (UriEncode $s $false) }
$path = ($encoded -join '/')
if (-not $path.StartsWith('/')) { $path = '/' + $path }
return $path
}
function ToHex([byte[]]$b) { ([BitConverter]::ToString($b)).Replace('-', '').ToLowerInvariant() }
function BuildAuthHeaders($method, [System.Uri]$uri, [string]$region, [string]$accessKey, [string]$secretKey, [string]$payloadHash) {
$algorithm = 'AWS4-HMAC-SHA256'
$timestamp = (Get-Date).ToUniversalTime()
$amzDate = $timestamp.ToString('yyyyMMddTHHmmssZ')
$dateStamp = $timestamp.ToString('yyyyMMdd')
$hostHeader = $uri.Host
if (-not $uri.IsDefaultPort) { $hostHeader = "${hostHeader}:$($uri.Port)" }
$canonicalUri = BuildCanonicalPath $uri
$canonicalQueryString = ''
$canonicalHeaders = "host:$hostHeader`n" + "x-amz-content-sha256:$payloadHash`n" + "x-amz-date:$amzDate`n"
$signedHeaders = 'host;x-amz-content-sha256;x-amz-date'
$canonicalRequest = "$method`n$canonicalUri`n$canonicalQueryString`n$canonicalHeaders`n$signedHeaders`n$payloadHash"
$credentialScope = "$dateStamp/$region/s3/aws4_request"
$stringToSign = "$algorithm`n$amzDate`n$credentialScope`n$((Get-HashHex (Get-Bytes $canonicalRequest)))"
$signingKey = GetSignatureKey $secretKey $dateStamp $region 's3'
$signature = ToHex (HmacSha256 $signingKey $stringToSign)
$authHeader = "$algorithm Credential=$accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature"
@{
'x-amz-date' = $amzDate
'x-amz-content-sha256' = $payloadHash
'Authorization' = $authHeader
}
}
function BuildS3Uri([string]$endpointUrl, [string]$bucket, [string]$key, [bool]$forcePathStyle) {
$base = [System.Uri]$endpointUrl
$builder = [System.UriBuilder]::new($base)
$normalizedKey = $key.Replace('\', '/').TrimStart('/')
if ($forcePathStyle) {
$path = $builder.Path.TrimEnd('/')
if ([string]::IsNullOrEmpty($path)) { $path = '/' }
$builder.Path = ($path.TrimEnd('/') + '/' + $bucket + '/' + $normalizedKey)
} else {
$builder.Host = "$bucket." + $builder.Host
$path = $builder.Path.TrimEnd('/')
if ([string]::IsNullOrEmpty($path)) { $path = '/' }
$builder.Path = ($path.TrimEnd('/') + '/' + $normalizedKey)
}
return $builder.Uri
}
function Invoke-S3HttpDownloadWithRetry {
param(
[string]$EndpointUrl,
[string]$Bucket,
[string]$Key,
[string]$TargetPath,
[string]$Region,
[string]$AccessKeyId,
[string]$SecretAccessKey,
[bool]$ForcePathStyle,
[string]$Activity
)
Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue
[System.Net.Http.HttpClient]$client = [System.Net.Http.HttpClient]::new()
$retries = 5
$delay = 2
try {
for ($attempt = 0; $attempt -lt $retries; $attempt++) {
$request = $null
try {
$uri = BuildS3Uri -endpointUrl $EndpointUrl -bucket $Bucket -key $Key -forcePathStyle $ForcePathStyle
$payloadHash = (Get-HashHex (Get-Bytes ''))
$headers = BuildAuthHeaders -method 'GET' -uri $uri -region $Region -accessKey $AccessKeyId -secretKey $SecretAccessKey -payloadHash $payloadHash
$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $uri)
foreach ($kvp in $headers.GetEnumerator()) {
$request.Headers.TryAddWithoutValidation($kvp.Key, $kvp.Value) | Out-Null
}
$response = $client.SendAsync($request, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
$null = $response.EnsureSuccessStatusCode()
$totalBytes = $response.Content.Headers.ContentLength
$stream = $response.Content.ReadAsStreamAsync().Result
$tmpPath = $TargetPath
$fs = [System.IO.File]::Create($tmpPath)
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 $Activity -Status ("{0:N2}% Complete" -f $pct) -PercentComplete $pct
} else {
Write-Progress -Activity $Activity -Status ("Downloaded {0:N0} bytes" -f $totalRead) -PercentComplete 0
}
}
} finally {
$fs.Close()
$stream.Close()
}
if ($response) { $response.Dispose() }
Write-Progress -Activity $Activity -Completed -Status 'Completed'
return
} catch {
if ($attempt -lt ($retries - 1)) {
Write-Warning "Download of '$Key' failed (attempt $($attempt + 1)/$retries): $($_.Exception.Message). Retrying in ${delay}s..."
Start-Sleep -Seconds $delay
$delay = [Math]::Min($delay * 2, 30)
} else {
throw
}
} finally {
if ($request) { $request.Dispose() }
}
}
} finally {
$client.Dispose()
}
}
function Invoke-DownloadWithRetry { function Invoke-DownloadWithRetry {
param( param(
[System.Net.Http.HttpClient]$Client, [System.Net.Http.HttpClient]$Client,
@@ -342,6 +119,79 @@ function Invoke-DownloadWithRetry {
} }
} }
function Invoke-S3HttpDownloadWithRetry {
param(
[string]$EndpointUrl,
[string]$Bucket,
[string]$Key,
[string]$TargetPath,
[string]$Region,
[string]$AccessKeyId,
[string]$SecretAccessKey,
[bool]$ForcePathStyle,
[string]$Activity
)
Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue
[System.Net.Http.HttpClient]$client = [System.Net.Http.HttpClient]::new()
$retries = 5
$delay = 2
try {
for ($attempt = 0; $attempt -lt $retries; $attempt++) {
$request = $null
try {
$uri = BuildS3Uri -endpointUrl $EndpointUrl -bucket $Bucket -key $Key -forcePathStyle $ForcePathStyle
$payloadHash = (Get-HashHex (Get-Bytes ''))
$headers = BuildAuthHeaders -method 'GET' -uri $uri -region $Region -accessKey $AccessKeyId -secretKey $SecretAccessKey -payloadHash $payloadHash
$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $uri)
foreach ($kvp in $headers.GetEnumerator()) {
$request.Headers.TryAddWithoutValidation($kvp.Key, $kvp.Value) | Out-Null
}
$response = $client.SendAsync($request, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
$null = $response.EnsureSuccessStatusCode()
$totalBytes = $response.Content.Headers.ContentLength
$stream = $response.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 $Activity -Status ("{0:N2}% Complete" -f $pct) -PercentComplete $pct
} else {
Write-Progress -Activity $Activity -Status ("Downloaded {0:N0} bytes" -f $totalRead) -PercentComplete 0
}
}
} finally {
$fs.Close()
$stream.Close()
}
if ($response) { $response.Dispose() }
Write-Progress -Activity $Activity -Completed -Status 'Completed'
return
} catch {
if ($attempt -lt ($retries - 1)) {
Write-Warning "Download of '$Key' failed (attempt $($attempt + 1)/$retries): $($_.Exception.Message). Retrying in ${delay}s..."
Start-Sleep -Seconds $delay
$delay = [Math]::Min($delay * 2, 30)
} else {
throw
}
} finally {
if ($request) { $request.Dispose() }
}
}
} finally {
$client.Dispose()
}
}
function Get-FileSha256Lower { function Get-FileSha256Lower {
param([string]$Path) param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) { throw "File not found: $Path" } if (-not (Test-Path -LiteralPath $Path)) { throw "File not found: $Path" }
@@ -387,19 +237,6 @@ function Get-RelativePath {
return $relativePath.Replace('/', [System.IO.Path]::DirectorySeparatorChar) return $relativePath.Replace('/', [System.IO.Path]::DirectorySeparatorChar)
} }
function Combine-StoragePath {
param(
[string]$Prefix,
[string]$Name
)
$cleanName = $Name.Replace('\', '/').TrimStart('/')
if ([string]::IsNullOrWhiteSpace($Prefix)) { return $cleanName }
$normalizedPrefix = $Prefix.Replace('\', '/').Trim('/')
if ([string]::IsNullOrEmpty($normalizedPrefix)) { return $cleanName }
return "$normalizedPrefix/$cleanName"
}
function Load-Manifest { function Load-Manifest {
param([string]$Path) param([string]$Path)
$raw = Get-Content -LiteralPath $Path -Encoding UTF8 -Raw $raw = Get-Content -LiteralPath $Path -Encoding UTF8 -Raw
@@ -542,7 +379,7 @@ function Update-KHDB {
) )
Start-UpdateTranscript -BasePath $scriptRoot Start-UpdateTranscript -BasePath $scriptRoot
try { try {
$settings = Read-ElysiumSettings $settings = Read-ElysiumSettings -ScriptRoot $scriptRoot
$installPath = Get-InstallationPath $settings $installPath = Get-InstallationPath $settings
Ensure-Directory $installPath Ensure-Directory $installPath
@@ -558,10 +395,10 @@ function Update-KHDB {
$parallelS3DownloadHelperList = @() $parallelS3DownloadHelperList = @()
if ($parallelDownloadsEnabled) { if ($parallelDownloadsEnabled) {
$parallelAzureDownloadHelpers = @{ $parallelAzureDownloadHelpers = @{
'Build-BlobUri' = Get-FunctionDefinitionText 'Build-BlobUri' 'Build-BlobUri' = Get-FunctionDefinitionText 'Build-BlobUri'
'Invoke-DownloadWithRetry' = Get-FunctionDefinitionText 'Invoke-DownloadWithRetry' 'Invoke-DownloadWithRetry' = Get-FunctionDefinitionText 'Invoke-DownloadWithRetry'
'New-HttpClient' = Get-FunctionDefinitionText 'New-HttpClient' 'New-HttpClient' = Get-FunctionDefinitionText 'New-HttpClient'
'Get-FileSha256Lower' = Get-FunctionDefinitionText 'Get-FileSha256Lower' 'Get-FileSha256Lower' = Get-FunctionDefinitionText 'Get-FileSha256Lower'
} }
$parallelAzureDownloadHelperList = $parallelAzureDownloadHelpers.GetEnumerator() | ForEach-Object { $parallelAzureDownloadHelperList = $parallelAzureDownloadHelpers.GetEnumerator() | ForEach-Object {
[pscustomobject]@{ Name = $_.Key; Definition = $_.Value } [pscustomobject]@{ Name = $_.Key; Definition = $_.Value }
@@ -727,14 +564,14 @@ function Update-KHDB {
} }
} }
if ($needsDownload) { if ($needsDownload) {
[void]$downloadQueue.Add([pscustomobject]@{ [void]$downloadQueue.Add([pscustomobject]@{
Name = $name Name = $name
Sha256 = $expectedHash Sha256 = $expectedHash
Size = $expectedSize Size = $expectedSize
}) })
}
} }
}
if ($downloadQueue.Count -gt 0) { if ($downloadQueue.Count -gt 0) {
Write-Host ("{0} shard(s) require download or refresh." -f $downloadQueue.Count) Write-Host ("{0} shard(s) require download or refresh." -f $downloadQueue.Count)
@@ -763,12 +600,12 @@ function Update-KHDB {
$storageClient = New-S3Client -EndpointUrl $s3EndpointUrl -Region $s3Region -AccessKeyId $s3AK -SecretAccessKey $s3SK -ForcePathStyle:$forcePathStyle $storageClient = New-S3Client -EndpointUrl $s3EndpointUrl -Region $s3Region -AccessKeyId $s3AK -SecretAccessKey $s3SK -ForcePathStyle:$forcePathStyle
} }
$storageHttpClient = @{ $storageHttpClient = @{
Endpoint = $s3EndpointUrl Endpoint = $s3EndpointUrl
Bucket = $s3Bucket Bucket = $s3Bucket
Region = $s3Region Region = $s3Region
AccessKey = $s3AK AccessKey = $s3AK
SecretKey = $s3SK SecretKey = $s3SK
ForcePath = $forcePathStyle ForcePath = $forcePathStyle
} }
} }
} else { } else {
@@ -846,11 +683,11 @@ function Update-KHDB {
$downloadIndex = 0 $downloadIndex = 0
foreach ($entry in $downloadQueue.ToArray()) { foreach ($entry in $downloadQueue.ToArray()) {
$downloadIndex++ $downloadIndex++
if ($null -eq $entry) { continue } if ($null -eq $entry) { continue }
$name = [string]$entry.Name $name = [string]$entry.Name
if ([string]::IsNullOrWhiteSpace($name)) { if ([string]::IsNullOrWhiteSpace($name)) {
throw "Shard entry missing name: $(ConvertTo-Json $entry -Compress)" throw "Shard entry missing name: $(ConvertTo-Json $entry -Compress)"
} }
$expectedHash = ([string]$entry.Sha256).ToLowerInvariant() $expectedHash = ([string]$entry.Sha256).ToLowerInvariant()
$expectedSize = [long]$entry.Size $expectedSize = [long]$entry.Size
@@ -862,7 +699,7 @@ function Update-KHDB {
if ($isS3) { if ($isS3) {
if ($storageClient) { if ($storageClient) {
try { try {
$request = New-Object Amazon.S3.Model.GetObjectRequest -Property @{ BucketName = $s3Bucket; Key = $remoteKey } $request = New-Object Amazon.S3.Model.GetObjectRequest -Property @{ BucketName = $s3Bucket; Key = $remoteKey }
$response = $storageClient.GetObject($request) $response = $storageClient.GetObject($request)
try { $response.WriteResponseStreamToFile($stagingPath, $true) } finally { $response.Dispose() } try { $response.WriteResponseStreamToFile($stagingPath, $true) } finally { $response.Dispose() }
} catch { } catch {
+1 -1
View File
@@ -7,7 +7,7 @@
################################################## ##################################################
## Project: Elysium ## ## Project: Elysium ##
## File: Update-LithnetStore.ps1 ## ## File: Update-LithnetStore.ps1 ##
## Version: 2.2.0 ## ## Version: 2.2.1 ##
## Support: support@cqre.net ## ## Support: support@cqre.net ##
################################################## ##################################################