class SSHScan::ScanEngine

Handle scanning of targets.

Public Instance Methods

scan(opts) click to toggle source

Utilize multiple threads to scan multiple targets, combine results and check for compliance. @param opts [Hash] options (sockets, threads …) @return [Hash] results

# File lib/ssh_scan/scan_engine.rb, line 170
def scan(opts)
  sockets = opts["sockets"]
  threads = opts["threads"] || 5
  logger = opts["logger"] || Logger.new(STDOUT)

  results = []

  work_queue = Queue.new

  sockets.each {|x| work_queue.push x }
  workers = (0...threads).map do
    Thread.new do
      begin
        while socket = work_queue.pop(true)
          results << scan_target(socket, opts)
        end
      rescue ThreadError => e
        raise e unless e.to_s.match(/queue empty/)
      end
    end
  end
  workers.map(&:join)

  # Add all the fingerprints to our peristent FingerprintDatabase
  fingerprint_db = SSHScan::FingerprintDatabase.new(
    opts['fingerprint_database']
  )
  results.each do |result|
    fingerprint_db.clear_fingerprints(result.ip)

    if result.keys
      result.keys.values.each do |host_key_algo|
        host_key_algo['fingerprints'].values.each do |fingerprint|
          fingerprint_db.add_fingerprint(fingerprint, result.ip)
        end
      end
    end
  end

  # Decorate all the results with duplicate keys
  results.each do |result|
    if result.keys
      ip = result.ip
      result.duplicate_host_key_ips = []
      result.keys.values.each do |host_key_algo|
        host_key_algo["fingerprints"].values.each do |fingerprint|
          fingerprint_db.find_fingerprints(fingerprint).each do |other_ip|
            next if ip == other_ip
            result.duplicate_host_key_ips << other_ip
          end
        end
      end
    end
  end

  # Decorate all the results with SSHFP records
  sshfp = SSHScan::SshFp.new()
  results.each do |result|
    if !result.hostname.empty?
      dns_keys = sshfp.query(result.hostname)
      result.dns_keys = dns_keys
    end
  end

  # Decorate all the results with compliance information
  results.each do |result|
    # Do this only when we have all the information we need
    if opts["policy"] &&
       result.key_algorithms.any? &&
       result.server_host_key_algorithms.any? &&
       result.encryption_algorithms_client_to_server.any? &&
       result.encryption_algorithms_server_to_client.any? &&
       result.mac_algorithms_client_to_server.any? &&
       result.mac_algorithms_server_to_client.any? &&
       result.compression_algorithms_client_to_server.any? &&
       result.compression_algorithms_server_to_client.any?

      policy = SSHScan::Policy.from_file(opts["policy"])
      policy_mgr = SSHScan::PolicyManager.new(result, policy)
      result.set_compliance = policy_mgr.compliance_results

      if result.compliance_policy
        result.grade = SSHScan::Grader.new(result).grade
      end
    end
  end

  return results.map {|r| r.to_hash}
end
scan_target(socket, opts) click to toggle source

Scan a single target. @param socket [String] ip:port specification @param opts [Hash] options (timeout, …) @return [Hash] result

# File lib/ssh_scan/scan_engine.rb, line 19
def scan_target(socket, opts)
  target, port = socket.chomp.split(':')
  if port.nil?
    port = 22
  end

  timeout = opts["timeout"]
  
  result = SSHScan::Result.new()
  result.port = port.to_i

  # Start the scan timer
  result.set_start_time

  if target.fqdn?
    result.hostname = target

    # If doesn't resolve as IPv6, we'll try IPv4
    if target.resolve_fqdn_as_ipv6.nil?
      client = SSHScan::Client.new(
        target.resolve_fqdn_as_ipv4.to_s, port, timeout
      )
      client.connect
      result.set_client_attributes(client)
      kex_result = client.get_kex_result()
      client.close
      result.set_kex_result(kex_result) unless kex_result.nil?
      result.error = client.error if client.error?
    # If it does resolve as IPv6, we're try IPv6
    else
      client = SSHScan::Client.new(
        target.resolve_fqdn_as_ipv6.to_s, port, timeout
      )
      client.connect
      result.set_client_attributes(client)
      kex_result = client.get_kex_result()
      client.close
      result.set_kex_result(kex_result) unless kex_result.nil?
      result.error = client.error if client.error?

      # If resolves as IPv6, but somehow we get an client error, fall-back to IPv4
      if result.error?
        result.unset_error
        client = SSHScan::Client.new(
          target.resolve_fqdn_as_ipv4.to_s, port, timeout
        )
        client.connect()
        result.set_client_attributes(client)
        kex_result = client.get_kex_result()
        client.close
        result.set_kex_result(kex_result) unless kex_result.nil?
        result.error = client.error if client.error?
      end
    end
  else
    client = SSHScan::Client.new(target, port, timeout)
    client.connect()
    result.set_client_attributes(client)
    kex_result = client.get_kex_result()
    client.close

    unless kex_result.nil?
      result.set_kex_result(kex_result)
    end

    # Attempt to suppliment a hostname that wasn't provided
    result.hostname = target.resolve_ptr

    result.error = client.error if client.error?
  end

  if result.error?
    result.set_end_time
    return result
  end

  # Connect and get results (Net-SSH)
  begin
    net_ssh_session = Net::SSH::Transport::Session.new(
                        target,
                        :port => port,
                        :timeout => timeout,
                        :verify_host_key => :never
                      )
    raise SSHScan::Error::ClosedConnection.new if net_ssh_session.closed?
    auth_session = Net::SSH::Authentication::Session.new(
      net_ssh_session, :auth_methods => ["none"]
    )
    auth_session.authenticate("none", "test", "test")
    result.auth_methods = auth_session.allowed_auth_methods
    net_ssh_session.close
  rescue Net::SSH::ConnectionTimeout => e
    result.error = SSHScan::Error::ConnectTimeout.new(e.message)
  rescue Net::SSH::Disconnect, Errno::ECONNRESET => e
    result.error = SSHScan::Error::Disconnected.new(e.message)
  rescue Net::SSH::Exception => e
    if e.to_s.match(/could not settle on/)
      result.error = e
    else
      raise e
    end
  end

  # Figure out what rsa or dsa fingerprints exist
  keys = {}

  output = ""

  cmd = ['ssh-keyscan', '-t', 'rsa,dsa,ecdsa,ed25519', '-p', port.to_s, target].join(" ")

  Utils::Subprocess.new(cmd) do |stdout, stderr, thread|
    if stdout
      output += stdout
    end
  end

  host_keys = output.split
  host_keys_len = host_keys.length - 1

  for i in 0..host_keys_len
    if host_keys[i].eql? "ssh-dss"
      key = SSHScan::Crypto::PublicKey.new([host_keys[i], host_keys[i + 1]].join(" "))
      keys.merge!(key.to_hash)
    end

    if host_keys[i].eql? "ssh-rsa"
      key = SSHScan::Crypto::PublicKey.new([host_keys[i], host_keys[i + 1]].join(" "))
      keys.merge!(key.to_hash)
    end

    if host_keys[i].eql? "ecdsa-sha2-nistp256"
      key = SSHScan::Crypto::PublicKey.new([host_keys[i], host_keys[i + 1]].join(" "))
      keys.merge!(key.to_hash)
    end

    if host_keys[i].eql? "ssh-ed25519"
      key = SSHScan::Crypto::PublicKey.new([host_keys[i], host_keys[i + 1]].join(" "))
      keys.merge!(key.to_hash)
    end
  end

  result.keys = keys
  result.set_end_time

  return result
end