4 Commits

Author SHA1 Message Date
tomas.kracmar 27a682a968 Release v2.2.2: fix replication permission check for nested groups
Test-ReplicationPermissions now uses the tokenGroups constructed
attribute to resolve all effective SIDs in the caller's Kerberos
token, including nested group memberships. This replaces the
previous MemberOf walk which missed indirect entitlement and
could produce false-positive missing-permission errors.

All versions bumped to unified v2.2.2.
2026-06-09 11:41:14 +02:00
tomas.kracmar 255cfe0a17 chore: centralize version and add Bump-Version.ps1
- Add  to Elysium.Common.ps1 as the single
  runtime source of truth for version strings.
- Update Update-KHDB.ps1 User-Agent to reference .
- Update Test-WeakADPasswords.ps1 usage beacon payload to reference
  .
- Add Bump-Version.ps1 release helper that updates the centralized
  variable, ASCII headers across .ps1/.py files, runtime references,
  and prints a CHANGELOG stub.
2026-06-09 11:14:20 +02:00
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
12 changed files with 792 additions and 845 deletions
+161
View File
@@ -0,0 +1,161 @@
##################################################
## ____ ___ ____ _____ _ _ _____ _____ ##
## / ___/ _ \| _ \| ____| | \ | | ____|_ _| ##
## | | | | | | |_) | _| | \| | _| | | ##
## | |__| |_| | _ <| |___ _| |\ | |___ | | ##
## \____\__\_\_| \_\_____(_)_| \_|_____| |_| ##
## Move fast and fix things. ##
##################################################
## Project: Elysium ##
## File: Bump-Version.ps1 ##
## Version: 2.2.2 ##
## Support: support@cqre.net ##
##################################################
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
<#
.SYNOPSIS
Bumps the unified Elysium version across all project files.
.DESCRIPTION
Updates the centralized $ElysiumVersion variable, ASCII headers in all
operational scripts, the settings template, and runtime references
(User-Agent, usage beacon, etc.). Optionally stubs a new CHANGELOG entry.
.PARAMETER NewVersion
The new version string to apply (e.g. 2.2.2).
.PARAMETER SkipChangelog
Do not print a CHANGELOG entry stub.
.EXAMPLE
.\Bump-Version.ps1 -NewVersion 2.2.2
#>
param(
[Parameter(Mandatory = $true)]
[string]$NewVersion,
[switch]$SkipChangelog
)
$scriptRoot = $PSScriptRoot
if (-not $scriptRoot) { $scriptRoot = (Get-Location).Path }
# ---------------------------------------------------------------------------
# Validate input
# ---------------------------------------------------------------------------
if ($NewVersion -notmatch '^\d+\.\d+\.\d+$') {
throw "Version must be in semantic format X.Y.Z (e.g. 2.2.2). Got: '$NewVersion'"
}
# ---------------------------------------------------------------------------
# Determine current version from Elysium.Common.ps1
# ---------------------------------------------------------------------------
$commonPath = Join-Path -Path $scriptRoot -ChildPath 'Elysium.Common.ps1'
if (-not (Test-Path -LiteralPath $commonPath)) {
throw "Elysium.Common.ps1 not found at $commonPath"
}
$commonContent = Get-Content -LiteralPath $commonPath -Raw
$currentVersionMatch = [regex]::Match($commonContent, "\`$script:ElysiumVersion\s*=\s*'([^']+)'")
if (-not $currentVersionMatch.Success) {
throw "Could not determine current version from Elysium.Common.ps1"
}
$oldVersion = $currentVersionMatch.Groups[1].Value
if ($oldVersion -eq $NewVersion) {
Write-Warning "Current version is already $NewVersion. Nothing to do."
return
}
Write-Host "Bumping Elysium from $oldVersion -> $NewVersion"
# ---------------------------------------------------------------------------
# Helper: replace in file
# ---------------------------------------------------------------------------
function Edit-FileVersion {
param(
[string]$Path,
[string]$Old,
[string]$New
)
$content = Get-Content -LiteralPath $Path -Raw
$newContent = $content.Replace($Old, $New)
if ($newContent -eq $content) {
Write-Verbose " No changes in $(Split-Path -Leaf $Path)"
} else {
Set-Content -LiteralPath $Path -Value $newContent -NoNewline -Encoding UTF8
Write-Host " Updated $(Split-Path -Leaf $Path)"
}
}
# ---------------------------------------------------------------------------
# 1. Central version variable
# ---------------------------------------------------------------------------
Write-Host "`n[1/4] Updating centralized version variable..."
Edit-FileVersion -Path $commonPath -Old "`$script:ElysiumVersion = '$oldVersion'" -New "`$script:ElysiumVersion = '$NewVersion'"
# ---------------------------------------------------------------------------
# 2. ASCII headers in scripts and templates
# ---------------------------------------------------------------------------
Write-Host "`n[2/4] Updating script headers..."
$headerTargets = Get-ChildItem -Path $scriptRoot -File | Where-Object {
$_.Extension -in @('.ps1', '.py') -or $_.Name -eq 'ElysiumSettings.txt.sample'
}
foreach ($file in $headerTargets) {
$content = Get-Content -LiteralPath $file.FullName -Raw
# The header pattern: ## Version: X.Y.Z ##
$pattern = "## Version:\s+$([regex]::Escape($oldVersion))\s+##"
$replacement = "## Version: $NewVersion ##"
$newContent = [regex]::Replace($content, $pattern, $replacement)
if ($newContent -ne $content) {
Set-Content -LiteralPath $file.FullName -Value $newContent -NoNewline -Encoding UTF8
Write-Host " Updated $($file.Name)"
}
}
# ---------------------------------------------------------------------------
# 3. Runtime references (safety net)
# ---------------------------------------------------------------------------
Write-Host "`n[3/4] Updating runtime version references..."
$runtimeTargets = Get-ChildItem -Path $scriptRoot -Filter '*.ps1' -File
foreach ($file in $runtimeTargets) {
$content = Get-Content -LiteralPath $file.FullName -Raw
$newContent = $content
# User-Agent patterns: 'Elysium/2.2.1 (+...)' or "Elysium/2.2.1 (+...)"
$newContent = [regex]::Replace($newContent,
"Elysium/$([regex]::Escape($oldVersion))",
"Elysium/$NewVersion")
# Literal string assignments: version = '2.2.1'
$newContent = [regex]::Replace($newContent,
"version\s*=\s*'$([regex]::Escape($oldVersion))'",
"version = '$NewVersion'")
if ($newContent -ne $content) {
Set-Content -LiteralPath $file.FullName -Value $newContent -NoNewline -Encoding UTF8
Write-Host " Updated runtime refs in $($file.Name)"
}
}
# ---------------------------------------------------------------------------
# 4. CHANGELOG stub
# ---------------------------------------------------------------------------
if (-not $SkipChangelog) {
Write-Host "`n[4/4] CHANGELOG entry stub (copy-paste ready):"
$today = (Get-Date).ToString('yyyy-MM-dd')
$stub = @"
---
## [$NewVersion] $today
### Changed
- (describe your change here)
"@
Write-Host $stub
Write-Host "`nAppend the above to CHANGELOG.md, then commit and tag."
}
Write-Host "`nDone. Review the changes with: git diff --stat"
+23
View File
@@ -6,6 +6,29 @@ Starting with **v2.2.0**, Elysium uses a **unified project version**. All script
---
## [2.2.2] — 2026-06-09
### Fixed
- `Test-ReplicationPermissions` (in `Elysium.Common.ps1`) now resolves the caller's **effective token SIDs** via the `tokenGroups` constructed attribute instead of walking `MemberOf` directly. This correctly accounts for nested group memberships and avoids false-positive "missing permissions" errors when the account is entitled through nested groups.
---
## [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
### Changed
+324
View File
@@ -1,3 +1,5 @@
$script:ElysiumVersion = '2.2.2'
function Invoke-RestartWithExecutable {
param(
[string]$ExecutablePath,
@@ -68,3 +70,325 @@ function Restart-WithWindowsPowerShellIfAvailable {
Write-Host ("Windows PowerShell detected at '{0}'; relaunching script under powershell.exe..." -f $powershellPath)
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, DistinguishedName -ErrorAction Stop
[void]$callerSids.Add($adUser.SID.Value)
# tokenGroups is a constructed attribute containing all SIDs in the user's token,
# including nested group memberships — more reliable than walking MemberOf recursively
$userDe = New-Object System.DirectoryServices.DirectoryEntry(
"LDAP://$Server/$($adUser.DistinguishedName)",
$Credential.UserName,
$Credential.GetNetworkCredential().Password
)
$userDe.RefreshCache(@('tokenGroups'))
foreach ($sidBytes in $userDe.Properties['tokenGroups']) {
$sid = New-Object System.Security.Principal.SecurityIdentifier($sidBytes, 0)
[void]$callerSids.Add($sid.Value)
}
} 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 ##
## File: Elysium.ps1 ##
## Version: 2.2.0 ##
## Version: 2.2.2 ##
## Support: support@cqre.net ##
##################################################
@@ -52,8 +52,6 @@ if ([string]::IsNullOrEmpty($passphrase)) {
Write-Host "Passphrase found in environment variables."
}
# Continue with the rest of your script...
function Start-OrchestratorTranscript {
param([string]$BasePath)
try {
+1 -1
View File
@@ -8,7 +8,7 @@
##################################################
## Project: Elysium ##
## File: ElysiumSettings.txt ##
## Version: 2.2.0 ##
## Version: 2.2.2 ##
## Support: support@cqre.net ##
##################################################
+183 -285
View File
@@ -6,8 +6,8 @@
## \____\__\_\_| \_\_____(_)_| \_|_____| |_| ##
##################################################
## Project: Elysium ##
## File: Extract-NTLMHashes.ps1 ##
## Version: 2.2.0 ##
## File: Extract-NTHashes.ps1 ##
## Version: 2.2.2 ##
## Support: support@cqre.net ##
##################################################
@@ -25,6 +25,11 @@ Set-StrictMode -Version Latest
$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 {
param([string]$BasePath)
try {
@@ -40,33 +45,130 @@ function Start-ExtractTranscript {
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) {
if ([string]::IsNullOrWhiteSpace($p)) { return (Join-Path -Path $scriptRoot -ChildPath 'Reports') }
if ([System.IO.Path]::IsPathRooted($p)) { return $p }
return (Join-Path -Path $scriptRoot -ChildPath $p)
}
function Get-FileSha256Hex([string]$path) {
$sha = [System.Security.Cryptography.SHA256]::Create()
$fs = [System.IO.File]::OpenRead($path)
try { return ([BitConverter]::ToString($sha.ComputeHash($fs))).Replace('-', '').ToLowerInvariant() } finally { $fs.Close(); $sha.Dispose() }
}
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
$payloadHash = Get-FileSha256Hex -path $filePath
Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue
$client = [System.Net.Http.HttpClient]::new()
try {
$req = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Put, $uri)
$stream = [System.IO.File]::OpenRead($filePath)
$req.Content = [System.Net.Http.StreamContent]::new($stream)
$hdrs = BuildAuthHeaders -method 'PUT' -uri $uri -region $region -accessKey $ak -secretKey $sk -payloadHash $payloadHash
$req.Headers.TryAddWithoutValidation('x-amz-date', $hdrs['x-amz-date']) | Out-Null
$req.Headers.TryAddWithoutValidation('Authorization', $hdrs['Authorization']) | Out-Null
$req.Headers.TryAddWithoutValidation('x-amz-content-sha256', $hdrs['x-amz-content-sha256']) | Out-Null
$resp = $client.SendAsync($req).Result
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() }
}
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
$payloadHash = (Get-HashHex (Get-Bytes ''))
Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue
$client = [System.Net.Http.HttpClient]::new()
try {
$req = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $uri)
$hdrs = BuildAuthHeaders -method 'GET' -uri $uri -region $region -accessKey $ak -secretKey $sk -payloadHash $payloadHash
$req.Headers.TryAddWithoutValidation('x-amz-date', $hdrs['x-amz-date']) | Out-Null
$req.Headers.TryAddWithoutValidation('Authorization', $hdrs['Authorization']) | Out-Null
$req.Headers.TryAddWithoutValidation('x-amz-content-sha256', $hdrs['x-amz-content-sha256']) | Out-Null
$resp = $client.SendAsync($req).Result
if (-not $resp.IsSuccessStatusCode) { throw "S3 GET failed: $([int]$resp.StatusCode) $($resp.ReasonPhrase)" }
$bytes = $resp.Content.ReadAsByteArrayAsync().Result
[System.IO.File]::WriteAllBytes($targetPath, $bytes)
} finally { if ($req) { $req.Dispose() }; $client.Dispose() }
}
function Protect-FileWithAES {
param (
[Parameter(Mandatory = $true)]
[string]$InputFile,
[Parameter(Mandatory = $true)]
[string]$OutputFile,
[Parameter(Mandatory = $true)]
[string]$Passphrase
)
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$salt = New-Object byte[] 16
$rng.GetBytes($salt)
$kdf = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Passphrase, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA256)
$key = $kdf.GetBytes(32)
$aes = [System.Security.Cryptography.Aes]::Create()
$aes.KeySize = 256
$aes.BlockSize = 128
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$aes.GenerateIV()
$iv = $aes.IV
$encryptor = $aes.CreateEncryptor($key, $iv)
$fileStream = [System.IO.File]::Open($InputFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
$outFileStream = [System.IO.File]::Create($OutputFile)
try {
$magic = [System.Text.Encoding]::ASCII.GetBytes('ELY1')
$outFileStream.Write($magic, 0, $magic.Length)
$outFileStream.Write($salt, 0, $salt.Length)
$outFileStream.Write($iv, 0, $iv.Length)
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($outFileStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
try {
$buffer = New-Object Byte[] 8192
while (($read = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$cryptoStream.Write($buffer, 0, $read)
}
} finally {
$cryptoStream.FlushFinalBlock()
$cryptoStream.Close()
}
} finally {
$outFileStream.Close(); $fileStream.Close(); $aes.Dispose(); $rng.Dispose(); $kdf.Dispose()
}
Write-Host "File has been encrypted (PBKDF2+AES-256-CBC): $OutputFile"
}
function Get-FileChecksum {
param (
[string]$Path,
[string]$Algorithm = "SHA256"
)
$hasher = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm)
$stream = [System.IO.File]::OpenRead($Path)
try {
$hashBytes = $hasher.ComputeHash($stream)
return [BitConverter]::ToString($hashBytes) -replace '-', ''
} finally {
$stream.Close()
$hasher.Dispose()
}
}
Start-ExtractTranscript -BasePath $scriptRoot
try {
Write-Host "Loading settings..."
$ElysiumSettings = Read-ElysiumSettings -ScriptRoot $scriptRoot
# Storage provider selection (Azure by default)
$storageProvider = $ElysiumSettings['StorageProvider']
if ([string]::IsNullOrWhiteSpace($storageProvider)) { $storageProvider = 'Azure' }
@@ -88,235 +190,12 @@ 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) {
$sha = [System.Security.Cryptography.SHA256]::Create()
$fs = [System.IO.File]::OpenRead($path)
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) {
$uri = BuildS3Uri -endpointUrl $endpointUrl -bucket $bucket -key $key -forcePathStyle $forcePathStyle
$payloadHash = Get-FileSha256Hex -path $filePath
Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue
$client = [System.Net.Http.HttpClient]::new()
try {
$req = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Put, $uri)
$stream = [System.IO.File]::OpenRead($filePath)
$req.Content = [System.Net.Http.StreamContent]::new($stream)
$hdrs = BuildAuthHeaders -method 'PUT' -uri $uri -region $region -accessKey $ak -secretKey $sk -payloadHash $payloadHash
$req.Headers.TryAddWithoutValidation('x-amz-date', $hdrs['x-amz-date']) | Out-Null
$req.Headers.TryAddWithoutValidation('Authorization', $hdrs['Authorization']) | Out-Null
$req.Headers.TryAddWithoutValidation('x-amz-content-sha256', $hdrs['x-amz-content-sha256']) | Out-Null
$resp = $client.SendAsync($req).Result
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() }
}
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
$payloadHash = (Get-HashHex (Get-Bytes ''))
Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue
$client = [System.Net.Http.HttpClient]::new()
try {
$req = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $uri)
$hdrs = BuildAuthHeaders -method 'GET' -uri $uri -region $region -accessKey $ak -secretKey $sk -payloadHash $payloadHash
$req.Headers.TryAddWithoutValidation('x-amz-date', $hdrs['x-amz-date']) | Out-Null
$req.Headers.TryAddWithoutValidation('Authorization', $hdrs['Authorization']) | Out-Null
$req.Headers.TryAddWithoutValidation('x-amz-content-sha256', $hdrs['x-amz-content-sha256']) | Out-Null
$resp = $client.SendAsync($req).Result
if (-not $resp.IsSuccessStatusCode) { throw "S3 GET failed: $([int]$resp.StatusCode) $($resp.ReasonPhrase)" }
$bytes = $resp.Content.ReadAsByteArrayAsync().Result
[System.IO.File]::WriteAllBytes($targetPath, $bytes)
} 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 }
if ([string]::IsNullOrWhiteSpace($passphrase)) { throw 'Passphrase not found in ELYSIUM_PASSPHRASE environment variable.' }
# Timestamp
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
function Protect-FileWithAES {
param (
[Parameter(Mandatory = $true)]
[string]$InputFile,
[Parameter(Mandatory = $true)]
[string]$OutputFile,
[Parameter(Mandatory = $true)]
[string]$Passphrase
)
# Derive key with PBKDF2 (HMACSHA256) + random salt
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$salt = New-Object byte[] 16
$rng.GetBytes($salt)
$kdf = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Passphrase, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA256)
$key = $kdf.GetBytes(32)
$aes = [System.Security.Cryptography.Aes]::Create()
$aes.KeySize = 256
$aes.BlockSize = 128
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$aes.GenerateIV()
$iv = $aes.IV
$encryptor = $aes.CreateEncryptor($key, $iv)
$fileStream = [System.IO.File]::Open($InputFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
$outFileStream = [System.IO.File]::Create($OutputFile)
try {
# File header: magic 'ELY1' (4 bytes), salt (16 bytes), IV (16 bytes)
$magic = [System.Text.Encoding]::ASCII.GetBytes('ELY1')
$outFileStream.Write($magic, 0, $magic.Length)
$outFileStream.Write($salt, 0, $salt.Length)
$outFileStream.Write($iv, 0, $iv.Length)
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($outFileStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
try {
$buffer = New-Object Byte[] 8192
while (($read = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$cryptoStream.Write($buffer, 0, $read)
}
} finally {
$cryptoStream.FlushFinalBlock()
$cryptoStream.Close()
}
} finally {
$outFileStream.Close(); $fileStream.Close(); $aes.Dispose(); $rng.Dispose(); $kdf.Dispose()
}
Write-Host "File has been encrypted (PBKDF2+AES-256-CBC): $OutputFile"
}
function Get-FileChecksum {
param (
[string]$Path,
[string]$Algorithm = "SHA256"
)
$hasher = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm)
$stream = [System.IO.File]::OpenRead($Path)
try {
$hashBytes = $hasher.ComputeHash($stream)
return [BitConverter]::ToString($hashBytes) -replace '-', ''
}
finally {
$stream.Close()
$hasher.Dispose()
}
}
# Extract NTLM hashes
$reportBase = Normalize-ReportPath -p $ElysiumSettings['ReportPathBase']
if (-not (Test-Path $reportBase)) { New-Item -Path $reportBase -ItemType Directory -Force | Out-Null }
@@ -337,51 +216,70 @@ $selection = Read-Host "Enter the number of the domain"
$selectedDomain = $DomainDetails[$selection]
if (-not $selectedDomain) {
Write-Error "Invalid selection."
exit
throw "Invalid selection."
}
# Update script variables based on selected domain
$domainController = $selectedDomain.DC
# Validate credentials and replication permissions before attempting DCSync
$hasADModule = $null -ne (Get-Module -Name ActiveDirectory -ErrorAction SilentlyContinue)
if (-not $hasADModule) {
try { Import-Module ActiveDirectory -ErrorAction Stop; $hasADModule = $true } catch {}
}
if ($hasADModule) {
$credential = Get-ValidatedADCredential -DomainName $selectedDomain.Name -Server $domainController
try {
$domainInfo = Get-ADDomain -Server $domainController -Credential $credential -ErrorAction Stop
Test-ReplicationPermissions -DomainDN $domainInfo.DistinguishedName `
-Server $domainController -Credential $credential
} catch {
throw $_.Exception.Message
}
} else {
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"
$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"
# 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: $exportPath"
Write-Host "NTLM hashes have been extracted to temporary file."
# Compress extracted NTLM hashes
Compress-Archive -Path $exportPath -DestinationPath $compressedFilePath
Write-Host "File has been compressed: $compressedFilePath"
Write-Host "File has been compressed."
# 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 }
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
# 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"
@@ -390,7 +288,6 @@ if ($storageProvider -ieq 'S3') {
$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)"
@@ -403,47 +300,48 @@ if ($storageProvider -ieq 'S3') {
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
$downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
}
}
else {
# Azure Blob Storage path (default)
} else {
$sas = $sasToken
if ([string]::IsNullOrWhiteSpace($sas)) { Write-Error 'sasToken is missing in settings.'; exit }
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
# 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 }
if (-not $container) { throw "Azure container '$containerName' not found or access denied." }
# Upload the encrypted file to Azure Blob Storage
Set-AzStorageBlobContent -File $encryptedFilePath -Container $containerName -Blob $blobName -Context $storageContext | Out-Null
Write-Host "Encrypted file uploaded to Azure Blob Storage: $blobName"
# Download the blob to a temporary location to verify
$tempDownloadPath = [System.IO.Path]::GetTempFileName()
Get-AzStorageBlobContent -Blob $blobName -Container $containerName -Context $storageContext -Destination $tempDownloadPath -Force | Out-Null
# Calculate the downloaded file checksum
$downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
}
# Compare the checksums
$downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
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
Remove-Item -Path $encryptedFilePath -Force
Remove-Item -Path $tempDownloadPath -Force
if ($storageProvider -ieq 'S3') {
Write-Host "Local and temporary files cleaned up after uploading to S3-compatible storage."
Write-Host "Upload to S3-compatible storage completed and verified."
} else {
Write-Host "Local and temporary files cleaned up after uploading to Azure Blob Storage."
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."
+1 -155
View File
@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Prepare-KHDBStorage.ps1 ##
## Version: 2.2.0 ##
## Version: 2.2.2 ##
## 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 {
param(
[psobject]$Manifest,
@@ -176,27 +138,6 @@ function Get-NormalizedForwardPath {
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 {
param(
[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 {
param(
[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 {
param(
[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...
+7 -125
View File
@@ -8,7 +8,7 @@
##################################################
## Project: Elysium ##
## File: Test-WeakADPasswords.ps1 ##
## Version: 2.2.0 ##
## Version: 2.2.2 ##
## 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.
#>
# Enable verbose output
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
[string]$commonHelper = Join-Path -Path $PSScriptRoot -ChildPath 'Elysium.Common.ps1'
if (-not (Test-Path -LiteralPath $commonHelper)) { throw "Common helper not found at $commonHelper" }
. $commonHelper
$VerbosePreference = "SilentlyContinue"
$scriptRoot = $PSScriptRoot
@@ -92,7 +95,7 @@ function Invoke-UsageBeacon {
if ($normalizedMethod -in @('POST', 'PUT')) {
$payload = [ordered]@{
script = 'Test-WeakADPasswords'
version = '1.4.5'
version = $ElysiumVersion
ranAtUtc = (Get-Date).ToUniversalTime().ToString('o')
}
if (-not [string]::IsNullOrWhiteSpace($InstanceId)) {
@@ -124,32 +127,9 @@ $footer = "`r`n==== End of Report ===="
Start-TestTranscript -BasePath $scriptRoot
try {
# Import settings
Write-Verbose "Loading settings..."
$ElysiumSettings = @{}
$settingsPath = Join-Path -Path $scriptRoot -ChildPath "ElysiumSettings.txt"
# Ensure the settings file exists
if (-not (Test-Path $settingsPath)) {
Write-Error "Settings file not found at $settingsPath"
exit
}
# Load settings from file
try {
Get-Content $settingsPath | ForEach-Object {
if (-not [string]::IsNullOrWhiteSpace($_) -and -not $_.StartsWith("#")) {
$keyValue = $_ -split '=', 2
if ($keyValue.Count -eq 2) {
$ElysiumSettings[$keyValue[0].Trim()] = $keyValue[1].Trim()
}
}
}
$ElysiumSettings = Read-ElysiumSettings -ScriptRoot $scriptRoot
Write-Verbose "Settings loaded successfully."
} catch {
Write-Error ("An error occurred while loading settings: {0}" -f $_.Exception.Message)
exit
}
$usageBeaconUrl = $ElysiumSettings['UsageBeaconUrl']
$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 Test-WeakADPasswords {
param (
+1 -1
View File
@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Uninstall.ps1 ##
## Version: 2.2.0 ##
## Version: 2.2.2 ##
## Support: support@cqre.net ##
##################################################
+76 -239
View File
@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Update-KHDB.ps1 ##
## Version: 2.2.0 ##
## Version: 2.2.2 ##
## Support: support@cqre.net ##
##################################################
@@ -50,21 +50,6 @@ function Stop-UpdateTranscript {
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) {
$p = $settings['InstallationPath']
if ([string]::IsNullOrWhiteSpace($p)) { return $scriptRoot }
@@ -76,218 +61,10 @@ function New-HttpClient {
Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue
$client = [System.Net.Http.HttpClient]::new()
$client.Timeout = [TimeSpan]::FromSeconds(600)
$client.DefaultRequestHeaders.UserAgent.ParseAdd('Elysium/2.1.1 (+Update-KHDB)')
$client.DefaultRequestHeaders.UserAgent.ParseAdd("Elysium/$ElysiumVersion (+Update-KHDB)")
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 {
param(
[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 {
param([string]$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)
}
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 {
param([string]$Path)
$raw = Get-Content -LiteralPath $Path -Encoding UTF8 -Raw
@@ -542,7 +379,7 @@ function Update-KHDB {
)
Start-UpdateTranscript -BasePath $scriptRoot
try {
$settings = Read-ElysiumSettings
$settings = Read-ElysiumSettings -ScriptRoot $scriptRoot
$installPath = Get-InstallationPath $settings
Ensure-Directory $installPath
+1 -1
View File
@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Update-LithnetStore.ps1 ##
## Version: 2.2.0 ##
## Version: 2.2.2 ##
## Support: support@cqre.net ##
##################################################