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

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Report
  include Msf::Exploit::Remote::Kerberos::Client

  def initialize(info = {})
    super(update_info(info,
      'Name' => 'Kerberos Domain User Enumeration',
      'Description' => %q(
        This module will enumerate valid Domain Users via Kerberos from an unauthenticated perspective. It utilizes
        the different responses returned by the service for valid and invalid users.
      ),
      'Author' =>
        [
          'Matt Byrne <attackdebris[at]gmail.com>' # Metasploit module
        ],
      'References' =>
        [
          [ 'URL', 'https://nmap.org/nsedoc/scripts/krb5-enum-users.html']
        ],
      'License' => MSF_LICENSE
      )
    )

    register_options(
      [
        OptString.new('DOMAIN', [ true, 'The Domain Eg: demo.local' ]),
        OptPath.new(
          'USER_FILE',
          [true, 'Files containing usernames, one per line', nil]
        )
      ],
      self.class
    )
  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 run
    print_status("Validating options...")

    domain = datastore['DOMAIN'].upcase
    user_file = datastore['USER_FILE']

    print_status("Using domain: #{domain}...")

    pre_auth = []
    pre_auth << build_pa_pac_request
    pre_auth

    user_list.each do |user|
      print_status("#{peer} - Testing User: \"#{user}\"...")
      res = send_request_as(
        client_name: "#{user}",
        server_name: "krbtgt/#{domain}",
        realm: "#{domain}",
        pa_data: pre_auth
      )
      print_status("#{peer} - #{warn_error(res)}") if res.msg_type == Rex::Proto::Kerberos::Model::KRB_ERROR
      test = Rex::Proto::Kerberos::Model::ERROR_CODES[res.error_code]
      if test == ["KDC_ERR_PREAUTH_REQUIRED", "Additional pre-authentication required"]
        print_good("#{peer} - User: \"#{user}\" is present")
        report_cred(
          host: datastore['RHOST'],
          port: rport,
          creds_name: 'Kerberos',
          user: user
        )
      elsif test == ["KDC_ERR_CLIENT_REVOKED", "Clients credentials have been revoked"]
        print_error("#{peer} - User: \"#{user}\" account disabled or locked out")
      else
        print_status("#{peer} - User: \"#{user}\" does not exist")
      end
    end
  end

  def report_cred(opts)
    service_data = {
      address: opts[:host],
      port: opts[:port],
      protocol: 'udp',
      workspace_id: myworkspace.id,
      service_name: opts[:creds_name]
    }

    credential_data = {
      username: opts[:user],
      origin_type: :service,
      module_fullname: self.fullname
    }.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 warn_error(res)
    msg = ''

    if Rex::Proto::Kerberos::Model::ERROR_CODES.key?(res.error_code)
      error_info = Rex::Proto::Kerberos::Model::ERROR_CODES[res.error_code]
      msg = "#{error_info[0]} - #{error_info[1]}"
    else
      msg = 'Wrong DOMAIN Name? Check DOMAIN and retry...'
    end
  end
end
