Lucene search
+L

SSH Public Key Login Scanner

🗓️ 24 Jul 2017 13:26:21Reported by todb <[email protected]>, RageLtManType 
metasploit
 metasploit
🔗 www.rapid7.com👁 64 Views

SSH Public Key Login Scanner module for testing SSH logins using private key files to report successful logins and to track access

Code
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'net/ssh'
require 'metasploit/framework/login_scanner/ssh'
require 'metasploit/framework/credential_collection'
require 'sshkey'
require 'net/ssh/command_stream'

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::AuthBrute
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::CommandShell
  include Msf::Auxiliary::Scanner
  include Msf::Exploit::Remote::SSH::Options
  include Msf::Sessions::CreateSessionOptions
  include Msf::Auxiliary::ReportSummary

  attr_accessor :ssh_socket, :good_key

  def initialize
    super(
      'Name'        => 'SSH Public Key Login Scanner',
      'Description' => %q{
        This module will test ssh logins on a range of machines using
        a defined private key file, and report successful logins.
        If you have loaded a database plugin and connected to a database
        this module will record successful logins and hosts so you can
        track your access.

        Key files may be a single private key, or several private keys in a single
        directory. Only a single passphrase is supported however, so it must either
        be shared between subject keys or only belong to a single one.
      },
      'Author'      => ['todb', 'RageLtMan'],
      'License'     => MSF_LICENSE
    )

    register_options(
      [
        Opt::RPORT(22),
        OptPath.new('KEY_PATH', [false, 'Filename or directory of cleartext private keys. Filenames beginning with a dot, or ending in ".pub" will be skipped. Duplicate private keys will be ignored.']),
        OptString.new('KEY_PASS', [false, 'Passphrase for SSH private key(s)']),
        OptString.new('PRIVATE_KEY', [false, 'The string value of the private key that will be used. If you are using MSFConsole, this value should be set as file:PRIVATE_KEY_PATH. OpenSSH, RSA, DSA, and ECDSA private keys are supported.'])
      ], self.class
    )

    register_advanced_options(
      [
        Opt::Proxies,
        OptBool.new('SSH_DEBUG', [false, 'Enable SSH debugging output (Extreme verbosity!)', 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.', '']),
        OptInt.new('SSH_TIMEOUT', [false, 'Specify the maximum time to negotiate a SSH session', 30]),
        OptBool.new('GatherProof', [true, 'Gather proof of access via pre-session shell commands', true])
      ]
    )

    deregister_options(
      'PASSWORD','PASS_FILE','BLANK_PASSWORDS','USER_AS_PASS','USERPASS_FILE',
      'DB_ALL_CREDS', 'DB_ALL_PASS', 'DB_SKIP_EXISTING'
    )

    @good_key = ''
    @strip_passwords = true

  end

  def rport
    datastore['RPORT']
  end

  def ip
    datastore['RHOST']
  end

  def session_setup(result, scanner, fingerprint, cred_core_private_id)
    return unless scanner.ssh_socket

    # Create a new session
    sess = Msf::Sessions::SshCommandShellBind.new(scanner.ssh_socket)

    # Clean up the stored data - need to stash the keyfile into
    # a datastore for later reuse.
    merge_me = {
      'USERPASS_FILE'        => nil,
      'USER_FILE'            => nil,
      'PASS_FILE'            => nil,
      'USERNAME'             => result.credential.public,
      'CRED_CORE_PRIVATE_ID' => cred_core_private_id,
      'SSH_KEYFILE_B64'      => [result.credential.private].pack("m*").gsub("\n",""),
      'KEY_PATH'             => nil
    }

    s = start_session(self, nil, merge_me, false, sess.rstream, sess)
    self.sockets.delete(scanner.ssh_socket.transport.socket)

    # Set the session platform
    s.platform = scanner.get_platform(result.proof)

    # Create database host information
    host_info = {host: scanner.host}

    unless s.platform == 'unknown'
      host_info[:os_name] = s.platform
    end

    report_host(host_info)

    s
  end

  def run_host(ip)
    print_status("#{ip}:#{rport} SSH - Testing Cleartext Keys")

    if datastore["USER_FILE"].blank? && datastore["USERNAME"].blank?
      validation_reason = "At least one of USER_FILE or USERNAME must be given"
      raise Msf::OptionValidateError.new(
        {
          "USER_FILE" => validation_reason,
          "USERNAME" => validation_reason
        }
      )
    end

    keys = KeyCollection.new(
      key_path: datastore['KEY_PATH'],
      password: datastore['KEY_PASS'],
      user_file: datastore['USER_FILE'],
      username: datastore['USERNAME'],
      private_key: datastore['PRIVATE_KEY']
    )

    unless keys.valid?
      print_error("Files that failed to be read:")
      keys.error_list.each do |err|
        print_line("\t- #{err}")
      end
    end

    keys = prepend_db_keys(keys)

    key_count = keys.key_data.count
    key_sources = []
    unless datastore['KEY_PATH'].blank?
      key_sources.append(datastore['KEY_PATH'])
    end

    unless datastore['PRIVATE_KEY'].blank?
      key_sources.append('PRIVATE_KEY')
    end


    print_brute :level => :vstatus, :ip => ip, :msg => "Testing #{key_count} #{'key'.pluralize(key_count)} from #{key_sources.join(' and ')}"
    scanner = Metasploit::Framework::LoginScanner::SSH.new(
      configure_login_scanner(
        host: ip,
        port: rport,
        cred_details: keys,
        stop_on_success: datastore['STOP_ON_SUCCESS'],
        bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
        proxies: datastore['Proxies'],
        connection_timeout: datastore['SSH_TIMEOUT'],
        framework: framework,
        framework_module: self,
        skip_gather_proof: !datastore['GatherProof']
      )
    )

    scanner.verbosity = :debug if datastore['SSH_DEBUG']

    scanner.scan! do |result|
      credential_data = result.to_h
      credential_data.merge!(
          module_fullname: self.fullname,
          workspace_id: myworkspace_id
      )
      case result.status
        when Metasploit::Model::Login::Status::SUCCESSFUL
          print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}' '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'"
          credential_core = create_credential(credential_data)
          credential_data[:core] = credential_core
          create_credential_login(credential_data)
          tmp_key = result.credential.private
          ssh_key = SSHKey.new tmp_key
          if datastore['CreateSession']
            if credential_core.is_a? Metasploit::Credential::Core
              session_setup(result, scanner, ssh_key.fingerprint, credential_core.private_id)
            else
              session_setup(result, scanner, ssh_key.fingerprint, nil)
            end
          end
          if datastore['GatherProof'] && scanner.get_platform(result.proof) == 'unknown'
            msg = "While a session may have opened, it may be bugged.  If you experience issues with it, re-run this module with"
            msg << " 'set gatherproof false'.  Also consider submitting an issue at github.com/rapid7/metasploit-framework with"
            msg << " device details so it can be handled in the future."
            print_brute :level => :error, :ip => ip, :msg => msg
          end
          :next_user
        when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
          if datastore['VERBOSE']
            print_brute :level => :verror, :ip => ip, :msg => "Could not connect: #{result.proof}"
          end
          scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
          invalidate_login(credential_data)
          :abort
        when Metasploit::Model::Login::Status::INCORRECT
          if datastore['VERBOSE']
            print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
          end
          invalidate_login(credential_data)
          scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
        else
          invalidate_login(credential_data)
          scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
      end
    end
  end

  class KeyCollection < Metasploit::Framework::CredentialCollection
    attr_accessor :key_data
    attr_accessor :key_path
    attr_accessor :private_key
    attr_accessor :error_list

    # Override CredentialCollection#has_privates?
    def has_privates?
      !@key_data.empty?
    end

    def realm
      nil
    end

    def valid?
      @error_list = []
      @key_data = Set.new

      unless @private_key.present? || @key_path.present?
        raise RuntimeError, "No key path or key provided"
      end

      if @key_path.present?
        if File.directory?(@key_path)
          @key_files ||= Dir.entries(@key_path).reject { |f| f =~ /^\x2e|\x2epub$/ }
          @key_files.each do |f|
            begin
              data = read_key(File.join(@key_path, f))
              @key_data << data if valid_key?(data)
            rescue StandardError => e
              @error_list << "#{File.join(@key_path, f)}: #{e}"
            end
          end
        elsif File.file?(@key_path)
          begin
            data = read_key(@key_path)
            @key_data << data if valid_key?(data)
          rescue StandardError => e
            @error_list << "#{@key_path} could not be read, #{e}"
          end
        else
          raise RuntimeError, "Invalid key path"
        end
      end

      if @private_key.present?
        data = Net::SSH::KeyFactory.load_data_private_key(@private_key, @password, false).to_s
        if valid_key?(data)
          @key_data << data
        else
          raise RuntimeError, "Invalid private key"
        end
      end

      !@key_data.empty?
    end

    def valid_key?(key_data)
      !!(key_data.match(/BEGIN [RECD]SA PRIVATE KEY/) && !key_data.match(/Proc-Type:.*ENCRYPTED/))
    end

    def each
      prepended_creds.each { |c| yield c }

      if @user_file.present?
        File.open(@user_file, 'rb') do |user_fd|
          user_fd.each_line do |user_from_file|
            user_from_file.chomp!
            each_key do |key_data|
              yield Metasploit::Framework::Credential.new(public: user_from_file, private: key_data, realm: realm, private_type: :ssh_key)
            end
          end
        end
      end

      if @username.present?
        each_key do |key_data|
          yield Metasploit::Framework::Credential.new(public: @username, private: key_data, realm: realm, private_type: :ssh_key)
        end
      end
    end

    def each_key
      @key_data.each do |data|
        yield data
      end
    end

    def read_key(file_path)
      @cache ||= {}
      @cache[file_path] ||= Net::SSH::KeyFactory.load_data_private_key(File.read(file_path), password, false, key_path).to_s
      @cache[file_path]
    end
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation