From 8050a19d4c47bdf563b910e9a05f123cea03af83 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Mon, 6 Oct 2025 10:39:20 -0700 Subject: [PATCH 01/15] Initial commit --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index e9f88a7b6204f..952a0ca0abc20 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -509,6 +509,7 @@ def find_esc9_vuln_cert_templates if @registry_values[:strong_certificate_binding_enforcement].present? note += " Registry value: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}." end + @certificate_details[certificate_symbol][:target_users] = users @certificate_details[certificate_symbol][:certificate_name_flags] = template['mspki-certificate-name-flag'] @certificate_details[certificate_symbol][:techniques] << 'ESC9' @certificate_details[certificate_symbol][:notes] << note @@ -549,6 +550,7 @@ def find_esc10_vuln_cert_templates if @registry_values[:strong_certificate_binding_enforcement].present? && @registry_values[:certificate_mapping_methods].present? note += " Registry values: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}, CertificateMappingMethods=#{@registry_values[:certificate_mapping_methods]}." end + @certificate_details[certificate_symbol][:target_users] = users @certificate_details[certificate_symbol][:certificate_name_flags] = template['mspki-certificate-name-flag'] @certificate_details[certificate_symbol][:techniques] << 'ESC10' @certificate_details[certificate_symbol][:notes] << note @@ -723,9 +725,24 @@ def find_esc16_vuln_cert_templates next if (@registry_values[ca_name][:disable_extension_list] && !@registry_values[ca_name][:disable_extension_list].include?('1.3.6.1.4.1.311.25.2')) if @registry_values[:strong_certificate_binding_enforcement] && (@registry_values[:strong_certificate_binding_enforcement] == 0 || @registry_values[:strong_certificate_binding_enforcement] == 1) + enroll_sids = @certificate_details[certificate_symbol][:enroll_sids] + users = find_users_with_write_and_enroll_rights(enroll_sids) + next if users.empty? + + user_plural = users.size > 1 ? 'accounts' : 'account' + has_plural = users.size > 1 ? 'have' : 'has' + + current_user = adds_get_current_user(@ldap)[:samaccountname].first + + note = "ESC16: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." + note += " Registry values: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}, CertificateMappingMethods=#{@registry_values[:certificate_mapping_methods]}." + note += " The Certificate Authority: #{ca_name} has 1.3.6.1.4.1.311.25.2 defined in it's disabled extension list" + # Scenario 1 - StrongCertificateBindingEnforcement = 1 or 0 then it's the same as ESC9 - mark them all as vulnerable + @certificate_details[certificate_symbol][:target_users] = users + @certificate_details[certificate_symbol][:certificate_name_flags] = template['mspki-certificate-name-flag'] @certificate_details[certificate_symbol][:techniques] << 'ESC16' - @certificate_details[certificate_symbol][:notes] << "ESC16: Template is vulnerable due StrongCertificateBindingEnforcement = #{@registry_values[:strong_certificate_binding_enforcement]} and the Certificate Authority: #{ca_name} having 1.3.6.1.4.1.311.25.2 defined in it's disabled extension list" + @certificate_details[certificate_symbol][:notes] << note elsif @registry_values[ca_name][:edit_flags] & EDITF_ATTRIBUTESUBJECTALTNAME2 != 0 # Scenario 2 - StrongCertificateBindingEnforcement = 2 but the edit_flags contain EDITF_ATTRIBUTESUBJECTALTNAME2 which re-enables the ability to exploit the certificate in the same way as ESC6 @certificate_details[certificate_symbol][:techniques] << 'ESC16' @@ -1057,7 +1074,7 @@ def run registry_values = enum_registry_values if datastore['RUN_REGISTRY_CHECKS'] - if registry_values.any? + if registry_values.present? registry_values.each do |key, value| vprint_good("#{key}: #{value.inspect}") end From bd318b111892f015b8f73bd37792fc3e42b6a74c Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Wed, 8 Oct 2025 10:51:19 -0700 Subject: [PATCH 02/15] Fix broken assignment --- modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index 952a0ca0abc20..46ae245a04058 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -740,7 +740,7 @@ def find_esc16_vuln_cert_templates # Scenario 1 - StrongCertificateBindingEnforcement = 1 or 0 then it's the same as ESC9 - mark them all as vulnerable @certificate_details[certificate_symbol][:target_users] = users - @certificate_details[certificate_symbol][:certificate_name_flags] = template['mspki-certificate-name-flag'] + @certificate_details[certificate_symbol][:certificate_name_flags] = entry['mspki-certificate-name-flag'] @certificate_details[certificate_symbol][:techniques] << 'ESC16' @certificate_details[certificate_symbol][:notes] << note elsif @registry_values[ca_name][:edit_flags] & EDITF_ATTRIBUTESUBJECTALTNAME2 != 0 From 2ff148f9fccd808f9826441926ec1cbdedbc10ce Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 24 Oct 2025 16:48:36 -0700 Subject: [PATCH 03/15] Cert finder DC version check --- .../Attacking-AD-CS-ESC-Vulnerabilities.md | 2 +- lib/msf/core/exploit/remote/ms_icpr.rb | 37 ++++++++++-- .../admin/dcerpc/esc_update_ldap_object.rb | 3 - .../gather/ldap_esc_vulnerable_cert_finder.rb | 57 +++++++++++++++++++ 4 files changed, 89 insertions(+), 10 deletions(-) diff --git a/docs/metasploit-framework.wiki/ad-certificates/Attacking-AD-CS-ESC-Vulnerabilities.md b/docs/metasploit-framework.wiki/ad-certificates/Attacking-AD-CS-ESC-Vulnerabilities.md index 3865e6cf057d3..e9bb19e43f0d3 100644 --- a/docs/metasploit-framework.wiki/ad-certificates/Attacking-AD-CS-ESC-Vulnerabilities.md +++ b/docs/metasploit-framework.wiki/ad-certificates/Attacking-AD-CS-ESC-Vulnerabilities.md @@ -1580,7 +1580,7 @@ msf6 auxiliary(admin/kerberos/get_ticket) > get_hash rhost=172.16.199.200 cert_f [*] Auxiliary module execution completed ``` -#### ESC16 Scenario 2 +## ESC16 Scenario 2 If domain controllers are in Full Enforcement mode (`StrongCertificateBindingEnforcement` == 2), ESC16 alone would normally prevent authentication using certificates that lack the required SID extension. However, if the CA is also vulnerable to ESC6, which is defined as: `EDITF_ATTRIBUTESUBJECTALTNAME2` flag is set under it's `EditFlags` registry key, located here: diff --git a/lib/msf/core/exploit/remote/ms_icpr.rb b/lib/msf/core/exploit/remote/ms_icpr.rb index ecf40aa9f3916..fd22f581f9cc5 100644 --- a/lib/msf/core/exploit/remote/ms_icpr.rb +++ b/lib/msf/core/exploit/remote/ms_icpr.rb @@ -420,14 +420,39 @@ def build_on_behalf_of(csr:, on_behalf_of:, cert:, key:, algorithm: 'SHA256') # @param [OpenSSL::X509::Certificate] cert # @return [Array] The policy OIDs if any were found. def get_cert_policy_oids(cert) - ext = cert.extensions.find { |e| e.oid == 'ms-app-policies' } - return [] unless ext + all_oids = [] - cert_policies = Rex::Proto::CryptoAsn1::X509::CertificatePolicies.parse(ext.value_der) - cert_policies.value.map do |policy_info| - oid_string = policy_info[:policyIdentifier].value - Rex::Proto::CryptoAsn1::OIDs.value(oid_string) || Rex::Proto::CryptoAsn1::ObjectId.new(oid_string) + # ms-app-policies (CertificatePolicies) - existing handling + if (ext = cert.extensions.find { |e| e.oid == 'ms-app-policies' }) + begin + cert_policies = Rex::Proto::CryptoAsn1::X509::CertificatePolicies.parse(ext.value_der) + cert_policies.value.each do |policy_info| + oid_string = policy_info[:policyIdentifier].value + all_oids << (Rex::Proto::CryptoAsn1::OIDs.value(oid_string) || Rex::Proto::CryptoAsn1::ObjectId.new(oid_string)) + end + rescue StandardError => e + vprint_error("Failed to parse ms-app-policies: #{e.class}: #{e.message}") + end end + + # extendedKeyUsage - SEQUENCE OF OBJECT IDENTIFIER + if (eku_ext = cert.extensions.find { |e| e.oid == 'extendedKeyUsage' }) + begin + asn1 = OpenSSL::ASN1.decode(eku_ext.value_der) + # asn1 should be a Sequence whose children are OBJECT IDENTIFIER nodes + if asn1.is_a?(OpenSSL::ASN1::Sequence) + asn1.value.each do |node| + next unless node.is_a?(OpenSSL::ASN1::ObjectId) + oid_string = node.value + all_oids << (Rex::Proto::CryptoAsn1::OIDs.value(oid_string) || Rex::Proto::CryptoAsn1::ObjectId.new(oid_string)) + end + end + rescue StandardError => e + vprint_error("Failed to parse extendedKeyUsage: #{e.class}: #{e.message}") + end + end + + all_oids end diff --git a/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb b/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb index 5696dcf8638fa..1e25b407e8240 100644 --- a/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb +++ b/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb @@ -27,9 +27,6 @@ def initialize(info = {}) not provided. It then uses the admin/kerberos/get_ticket module to retrieve the NTLM hash of the target user and requests a certificate via MS-ICPR. The resulting certificate can be used for various operations, such as authentication. - - The module ensures that any changes made by the ldap_object_attribute or shadow_credentials module are - reverted after execution to maintain system integrity. }, 'License' => MSF_LICENSE, 'Author' => [ diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index 46ae245a04058..45daff9c4f5b7 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -1035,6 +1035,58 @@ def get_ip_addresses_by_fqdn(host_fqdn) ip_addresses end + def domain_controller_version_check + domain = adds_get_domain_info(@ldap)[:dns_name] + user = adds_get_current_user(@ldap)[:sAMAccountName].first.to_s + print_status("user: #{user}, domain: #{domain}") + + version_raw = nil + conn = create_winrm_connection(datastore['RHOSTS'], domain, user, datastore['WINRM_TIMEOUT']) + # Get the build number over WinRM by querying the Update Build Revision from the registry and appending it to the OS version. + # If there is no URB append 0 so we the string always ends in a numberical value + conn.shell(:powershell) do |shell| + ps = <<~PS + $os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop + $ubr = (Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion' -Name UBR -ErrorAction SilentlyContinue).UBR + if ($ubr -eq $null) { $ubr = 0 } + Write-Output ("{0}.{1}" -f $os.Version, $ubr) + PS + output = shell.run(ps) + version_raw = output.stdout&.lines&.first&.strip + shell.close + end + + if version_raw.blank? + fail_with(Failure::Unknown, "Could not retrieve Windows version string from #{datastore['RHOSTS']} via WinRM.") + end + + version_obj = Rex::Version.new(version_raw) + + print_status("Detected target Windows version: #{version_raw}") + + # Product ranges: [ Product name, RTM version, Sept2025 patch version ] + # Replace the 'patch_version' entries with actual September 2025 version/build strings. + ranges = [ + [Msf::WindowsVersion::ServerNameMapping[:Server2025], Msf::WindowsVersion::Server2025, Rex::Version.new('10.0.26100.6588')], + [Msf::WindowsVersion::ServerNameMapping[:Server2022], Msf::WindowsVersion::Server2022, Rex::Version.new('10.0.20348.4171')], + [Msf::WindowsVersion::ServerNameMapping[:Server2019], Msf::WindowsVersion::Server2019, Rex::Version.new('10.0.17763.7792')], + [Msf::WindowsVersion::ServerNameMapping[:Server2016], Msf::WindowsVersion::Server2016, Rex::Version.new('10.0.14393.8422')], + ] + + ranges.each do |product, rtm_version, patch_version| + if version_obj >= rtm_version && version_obj < patch_version + print_good("Detected #{product} version #{version_obj} — appears vulnerable (below Sept 2025 threshold #{patch_version}). Module will continue.") + return false + end + + if version_obj >= patch_version + fail_with(Failure::NotVulnerable, "Detected #{product} version #{version_obj} which is at-or-above the September 2025 threshold (#{patch_version}). Target appears patched. Weak certificate mappings/ ESC techniques are not exploitable on this domain controller") + end + end + + fail_with(Failure::Unknown, "Could not map detected Windows version #{version_obj} to a known product range. Cannot proceed with module execution.") + end + def validate super if (datastore['RUN_REGISTRY_CHECKS']) && !%w[auto plaintext ntlm].include?(datastore['LDAP::Auth'].downcase) @@ -1064,6 +1116,11 @@ def run end @ldap = ldap + # If the domain controller is patched up to Sept 2025, the CA can still issue Certificates which appear + # vulnerable (ie. Subject Alt Names can be specified with UPN: Administrator) however the Domain controller no + # longer accepts weak certificate mappings regardless of the StrongCertificateBindingEnforcement/ CertificaateMappingMethod registry key. + domain_controller_version_check + templates = query_ldap_server('(objectClass=pkicertificatetemplate)', CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE) fail_with(Failure::NotFound, 'No certificate templates were found.') if templates.empty? From 98e9b3016aa418b24130989d64d8dc30f6c9a589 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 24 Oct 2025 16:51:17 -0700 Subject: [PATCH 04/15] Fixed module description --- modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb b/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb index 1e25b407e8240..5696dcf8638fa 100644 --- a/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb +++ b/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb @@ -27,6 +27,9 @@ def initialize(info = {}) not provided. It then uses the admin/kerberos/get_ticket module to retrieve the NTLM hash of the target user and requests a certificate via MS-ICPR. The resulting certificate can be used for various operations, such as authentication. + + The module ensures that any changes made by the ldap_object_attribute or shadow_credentials module are + reverted after execution to maintain system integrity. }, 'License' => MSF_LICENSE, 'Author' => [ From f49c1caf5466eaf06577ac685fdc377312994540 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Wed, 29 Oct 2025 18:25:49 -0700 Subject: [PATCH 05/15] ESC16 and can_enroll key updates --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 89 +++++++++++++------ 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index 45daff9c4f5b7..d975adb64651f 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -192,7 +192,7 @@ def query_ldap_server_certificates(esc_raw_filter, esc_id, notes: []) esc_entries.each do |entry| certificate_symbol = entry[:cn][0].to_sym certificate_details = @certificate_details[certificate_symbol] - + certificate_details[:can_enroll] = can_enroll?(certificate_details) certificate_details[:techniques] << esc_id certificate_details[:notes] += notes end @@ -499,6 +499,7 @@ def find_esc9_vuln_cert_templates enroll_sids = @certificate_details[certificate_symbol][:enroll_sids] users = find_users_with_write_and_enroll_rights(enroll_sids) next if users.empty? + next unless users_compatible_with_template?(users, template['mspki-certificate-name-flag']) user_plural = users.size > 1 ? 'accounts' : 'account' has_plural = users.size > 1 ? 'have' : 'has' @@ -509,6 +510,7 @@ def find_esc9_vuln_cert_templates if @registry_values[:strong_certificate_binding_enforcement].present? note += " Registry value: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}." end + @certificate_details[certificate_symbol][:can_enroll] = can_enroll?(@certificate_details[certificate_symbol]) @certificate_details[certificate_symbol][:target_users] = users @certificate_details[certificate_symbol][:certificate_name_flags] = template['mspki-certificate-name-flag'] @certificate_details[certificate_symbol][:techniques] << 'ESC9' @@ -526,11 +528,11 @@ def find_esc10_vuln_cert_templates "(pkiextendedkeyusage=#{OIDs::OID_ANY_EXTENDED_KEY_USAGE.value})"\ '(!(pkiextendedkeyusage=*))'\ ')'\ - '(|'\ - "(mspki-certificate-name-flag:1.2.840.113556.1.4.804:=#{CT_FLAG_SUBJECT_ALT_REQUIRE_UPN})"\ - "(mspki-certificate-name-flag:1.2.840.113556.1.4.804:=#{CT_FLAG_SUBJECT_ALT_REQUIRE_DNS})"\ - ')'\ - ')' + '(|'\ + "(mspki-certificate-name-flag:1.2.840.113556.1.4.804:=#{CT_FLAG_SUBJECT_ALT_REQUIRE_UPN})"\ + "(mspki-certificate-name-flag:1.2.840.113556.1.4.804:=#{CT_FLAG_SUBJECT_ALT_REQUIRE_DNS})"\ + ')'\ + ')' esc10_templates = query_ldap_server(esc10_raw_filter, CERTIFICATE_ATTRIBUTES + ['msPKI-Certificate-Name-Flag'], base_prefix: CERTIFICATE_TEMPLATES_BASE) esc10_templates.each do |template| @@ -539,6 +541,7 @@ def find_esc10_vuln_cert_templates enroll_sids = @certificate_details[certificate_symbol][:enroll_sids] users = find_users_with_write_and_enroll_rights(enroll_sids) next if users.empty? + next unless users_compatible_with_template?(users, template['mspki-certificate-name-flag']) user_plural = users.size > 1 ? 'accounts' : 'account' has_plural = users.size > 1 ? 'have' : 'has' @@ -550,6 +553,7 @@ def find_esc10_vuln_cert_templates if @registry_values[:strong_certificate_binding_enforcement].present? && @registry_values[:certificate_mapping_methods].present? note += " Registry values: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}, CertificateMappingMethods=#{@registry_values[:certificate_mapping_methods]}." end + @certificate_details[certificate_symbol][:can_enroll] = can_enroll?(@certificate_details[certificate_symbol]) @certificate_details[certificate_symbol][:target_users] = users @certificate_details[certificate_symbol][:certificate_name_flags] = template['mspki-certificate-name-flag'] @certificate_details[certificate_symbol][:techniques] << 'ESC10' @@ -697,6 +701,24 @@ def find_esc15_vuln_cert_templates query_ldap_server_certificates(esc_raw_filter, 'ESC15', notes: notes) end + def users_compatible_with_template?(users, flag_values) + return false if users.blank? || flag_values.blank? + + raw = flag_values.is_a?(Array) ? flag_values.first : flag_values + return false if raw.nil? + + # Normalize to a 32-bit unsigned mask (handles negative signed strings) + mask = raw.to_i & 0xffffffff + + if (mask & CT_FLAG_SUBJECT_ALT_REQUIRE_DNS) != 0 && users.any? { |user| user.end_with?('$') } + true + elsif (mask & CT_FLAG_SUBJECT_ALT_REQUIRE_UPN) != 0 && users.any? { |user| !user.end_with?('$') } + true + else + false + end + end + def find_esc16_vuln_cert_templates # if we were able to read the registry values and this OID is not explicitly disabled, then we know for certain the server is not vulnerable esc16_raw_filter = '(&'\ @@ -720,33 +742,41 @@ def find_esc16_vuln_cert_templates # Get the CA servers that issue this template and we'll check their registry values @certificate_details[certificate_symbol][:ca_servers].each_value do |ca_server| ca_name = ca_server[:name].to_sym - next unless @registry_values.present? && @registry_values.key?(ca_name) - # ESC16 revolves around the szOID_NTDS_CA_SECURITY_EXT being globally disabled on the CA server via the disable_extension_list. If it's not disabled, skip - next if (@registry_values[ca_name][:disable_extension_list] && !@registry_values[ca_name][:disable_extension_list].include?('1.3.6.1.4.1.311.25.2')) + @certificate_details[certificate_symbol][:certificate_name_flags] = entry['mspki-certificate-name-flag'] + enroll_sids = @certificate_details[certificate_symbol][:enroll_sids] + users = find_users_with_write_and_enroll_rights(enroll_sids) + user_plural = users.size > 1 ? 'accounts' : 'account' + has_plural = users.size > 1 ? 'have' : 'has' + current_user = adds_get_current_user(@ldap)[:samaccountname].first + @certificate_details[certificate_symbol][:target_users] = users - if @registry_values[:strong_certificate_binding_enforcement] && (@registry_values[:strong_certificate_binding_enforcement] == 0 || @registry_values[:strong_certificate_binding_enforcement] == 1) - enroll_sids = @certificate_details[certificate_symbol][:enroll_sids] - users = find_users_with_write_and_enroll_rights(enroll_sids) + # ESC16 revolves around the szOID_NTDS_CA_SECURITY_EXT being globally disabled on the CA server via the disable_extension_list. If it's not disabled, skip + if @registry_values[ca_name]&.[](:disable_extension_list)&.include?('1.3.6.1.4.1.311.25.2') && @registry_values[:strong_certificate_binding_enforcement] && (@registry_values[:strong_certificate_binding_enforcement] == 0 || @registry_values[:strong_certificate_binding_enforcement] == 1) next if users.empty? - - user_plural = users.size > 1 ? 'accounts' : 'account' - has_plural = users.size > 1 ? 'have' : 'has' - - current_user = adds_get_current_user(@ldap)[:samaccountname].first + next unless users_compatible_with_template?(users, entry['mspki-certificate-name-flag']) note = "ESC16: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." note += " Registry values: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}, CertificateMappingMethods=#{@registry_values[:certificate_mapping_methods]}." note += " The Certificate Authority: #{ca_name} has 1.3.6.1.4.1.311.25.2 defined in it's disabled extension list" # Scenario 1 - StrongCertificateBindingEnforcement = 1 or 0 then it's the same as ESC9 - mark them all as vulnerable - @certificate_details[certificate_symbol][:target_users] = users - @certificate_details[certificate_symbol][:certificate_name_flags] = entry['mspki-certificate-name-flag'] - @certificate_details[certificate_symbol][:techniques] << 'ESC16' + @certificate_details[certificate_symbol][:techniques] << 'ESC16_1' @certificate_details[certificate_symbol][:notes] << note - elsif @registry_values[ca_name][:edit_flags] & EDITF_ATTRIBUTESUBJECTALTNAME2 != 0 + elsif @registry_values[ca_name]&.[](:disable_extension_list)&.include?('1.3.6.1.4.1.311.25.2') && @registry_values[ca_name][:edit_flags] & EDITF_ATTRIBUTESUBJECTALTNAME2 != 0 # Scenario 2 - StrongCertificateBindingEnforcement = 2 but the edit_flags contain EDITF_ATTRIBUTESUBJECTALTNAME2 which re-enables the ability to exploit the certificate in the same way as ESC6 - @certificate_details[certificate_symbol][:techniques] << 'ESC16' + @certificate_details[certificate_symbol][:techniques] << 'ESC16_2' @certificate_details[certificate_symbol][:notes] << "ESC16: Template is vulnerable due to the active policy EditFlags having: EDITF_ATTRIBUTESUBJECTALTNAME2 set (which is essentially ESC6) on the Certificate Authority: #{ca_name}. Also the CA having 1.3.6.1.4.1.311.25.2 defined in it's disabled extension list" + elsif @registry_values.blank? + # We couldn't read the registry values - mark as potentially vulnerable + @certificate_details[certificate_symbol][:can_enroll] = can_enroll?(@certificate_details[certificate_symbol]) + @certificate_details[certificate_symbol][:techniques] << 'ESC16_2' + @certificate_details[certificate_symbol][:notes] << 'ESC16_2: Template appears to be vulnerable (most templates do)' + + next if users.empty? + next unless users_compatible_with_template?(users, entry['mspki-certificate-name-flag']) + + @certificate_details[certificate_symbol][:techniques] << 'ESC16_1' + @certificate_details[certificate_symbol][:notes] << "ESC16_1: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." end end end @@ -777,7 +807,7 @@ def find_enrollable_vuln_certificate_templates def reporting_split_techniques(template) # these techniques are special in the sense that the exploit steps involve a different user performing the request # meaning that whether or not we can issue them is irrelevant - enroll_by_proxy = %w[ESC9 ESC10 ESC16] + enroll_by_proxy = %w[ESC9 ESC10 ESC16_1] # technically ESC15 might be patched and we can't fingerprint that status but we live it in the "vulnerable" category # when we have the registry values, we can tell the vulnerabilities for certain @@ -884,8 +914,11 @@ def print_vulnerable_cert_info if potentially_vulnerable_techniques.include?('ESC10') print_warning(' Potentially vulnerable to: ESC10 (the template is in a vulnerable configuration but in order to exploit registry key StrongCertificateBindingEnforcement must be set to 0 or CertificateMappingMethods must be set to 4)') end - if potentially_vulnerable_techniques.include?('ESC16') - print_warning(' Potentially vulnerable to: ESC16 (the template is in a vulnerable configuration but in order to exploit registry key StrongCertificateBindingEnforcement must be set to either 0 or 1. If StrongCertificateBindingEnforcement is set to 2, ESC16 is exploitable if the active policy EditFlags has EDITF_ATTRIBUTESUBJECTALTNAME2 set.') + if potentially_vulnerable_techniques.include?('ESC16_1') + print_warning(' Potentially vulnerable to: ESC16_1 (the template is in a vulnerable configuration but in order to exploit registry key StrongCertificateBindingEnforcement must be set to either 0 or 1 and the CA must have the SID security extention OID: 1.3.6.1.4.1.311.25.2 listed under the DisbaledExtensionlist registry key.') + end + if potentially_vulnerable_techniques.include?('ESC16_2') + print_warning(' Potentially vulnerable to: ESC16_2 (the template is in a vulnerable configuration but in order to exploit registry key StrongCertificateBindingEnforcement must be set to 2 and the CA must have the SID security extention OID: 1.3.6.1.4.1.311.25.2 listed under the DisbaledExtensionlist registry key and EDITF_ATTRIBUTESUBJECTALTNAME2 enabled in the EditFlags policy).') end print_status(" Permissions: #{hash[:permissions].join(', ')}") @@ -1119,7 +1152,11 @@ def run # If the domain controller is patched up to Sept 2025, the CA can still issue Certificates which appear # vulnerable (ie. Subject Alt Names can be specified with UPN: Administrator) however the Domain controller no # longer accepts weak certificate mappings regardless of the StrongCertificateBindingEnforcement/ CertificaateMappingMethod registry key. - domain_controller_version_check + begin + domain_controller_version_check + rescue WinRM::WinRMAuthorizationError => e + print_warning("Unable to determine the version of Window so these all might be false postives! WinRM authorization error: #{e.message}") + end templates = query_ldap_server('(objectClass=pkicertificatetemplate)', CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE) fail_with(Failure::NotFound, 'No certificate templates were found.') if templates.empty? From cd80cd940ad4cf70e34c8ecc7ca50bfb7caca94a Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Wed, 29 Oct 2025 22:20:18 -0700 Subject: [PATCH 06/15] add set_can_enroll_flags method --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index d975adb64651f..f57fd66b7b259 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -192,7 +192,6 @@ def query_ldap_server_certificates(esc_raw_filter, esc_id, notes: []) esc_entries.each do |entry| certificate_symbol = entry[:cn][0].to_sym certificate_details = @certificate_details[certificate_symbol] - certificate_details[:can_enroll] = can_enroll?(certificate_details) certificate_details[:techniques] << esc_id certificate_details[:notes] += notes end @@ -510,7 +509,6 @@ def find_esc9_vuln_cert_templates if @registry_values[:strong_certificate_binding_enforcement].present? note += " Registry value: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}." end - @certificate_details[certificate_symbol][:can_enroll] = can_enroll?(@certificate_details[certificate_symbol]) @certificate_details[certificate_symbol][:target_users] = users @certificate_details[certificate_symbol][:certificate_name_flags] = template['mspki-certificate-name-flag'] @certificate_details[certificate_symbol][:techniques] << 'ESC9' @@ -553,7 +551,7 @@ def find_esc10_vuln_cert_templates if @registry_values[:strong_certificate_binding_enforcement].present? && @registry_values[:certificate_mapping_methods].present? note += " Registry values: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}, CertificateMappingMethods=#{@registry_values[:certificate_mapping_methods]}." end - @certificate_details[certificate_symbol][:can_enroll] = can_enroll?(@certificate_details[certificate_symbol]) + @certificate_details[certificate_symbol][:target_users] = users @certificate_details[certificate_symbol][:certificate_name_flags] = template['mspki-certificate-name-flag'] @certificate_details[certificate_symbol][:techniques] << 'ESC10' @@ -768,7 +766,6 @@ def find_esc16_vuln_cert_templates @certificate_details[certificate_symbol][:notes] << "ESC16: Template is vulnerable due to the active policy EditFlags having: EDITF_ATTRIBUTESUBJECTALTNAME2 set (which is essentially ESC6) on the Certificate Authority: #{ca_name}. Also the CA having 1.3.6.1.4.1.311.25.2 defined in it's disabled extension list" elsif @registry_values.blank? # We couldn't read the registry values - mark as potentially vulnerable - @certificate_details[certificate_symbol][:can_enroll] = can_enroll?(@certificate_details[certificate_symbol]) @certificate_details[certificate_symbol][:techniques] << 'ESC16_2' @certificate_details[certificate_symbol][:notes] << 'ESC16_2: Template appears to be vulnerable (most templates do)' @@ -829,7 +826,7 @@ def reporting_split_techniques(template) end def can_enroll?(template) - (template[:permissions].include?('FULL CONTROL') || template[:permissions].include?('ENROLL')) && template[:ca_servers].values.any? { _1[:permissions].include?('REQUEST CERTIFICATES') } + (template[:permissions].include?('FULL CONTROL') || template[:permissions].include?('ENROLL')) && (template[:ca_servers].empty? || template[:ca_servers].values.any? { _1[:permissions].include?('REQUEST CERTIFICATES') }) end def print_vulnerable_cert_info @@ -1120,6 +1117,12 @@ def domain_controller_version_check fail_with(Failure::Unknown, "Could not map detected Windows version #{version_obj} to a known product range. Cannot proceed with module execution.") end + def set_can_enroll_flags + @certificate_details.each_key do |certificate_template| + @certificate_details[certificate_template][:can_enroll] = can_enroll?(@certificate_details[certificate_template]) + end + end + def validate super if (datastore['RUN_REGISTRY_CHECKS']) && !%w[auto plaintext ntlm].include?(datastore['LDAP::Auth'].downcase) @@ -1175,6 +1178,7 @@ def run end find_enrollable_vuln_certificate_templates + set_can_enroll_flags find_esc1_vuln_cert_templates find_esc2_vuln_cert_templates find_esc3_vuln_cert_templates From 84f7327415d15f4c580bc40841295d09a5ae32a0 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Thu, 30 Oct 2025 06:22:37 -0700 Subject: [PATCH 07/15] Fix spacing --- modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index f57fd66b7b259..eb78f8331612d 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -192,6 +192,7 @@ def query_ldap_server_certificates(esc_raw_filter, esc_id, notes: []) esc_entries.each do |entry| certificate_symbol = entry[:cn][0].to_sym certificate_details = @certificate_details[certificate_symbol] + certificate_details[:techniques] << esc_id certificate_details[:notes] += notes end @@ -705,7 +706,6 @@ def users_compatible_with_template?(users, flag_values) raw = flag_values.is_a?(Array) ? flag_values.first : flag_values return false if raw.nil? - # Normalize to a 32-bit unsigned mask (handles negative signed strings) mask = raw.to_i & 0xffffffff if (mask & CT_FLAG_SUBJECT_ALT_REQUIRE_DNS) != 0 && users.any? { |user| user.end_with?('$') } From 264b9289b98a3a98b8872cdee367de30186c463a Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 31 Oct 2025 09:47:56 -0700 Subject: [PATCH 08/15] Responded to comments --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index eb78f8331612d..e6c2458659eaf 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -749,7 +749,7 @@ def find_esc16_vuln_cert_templates @certificate_details[certificate_symbol][:target_users] = users # ESC16 revolves around the szOID_NTDS_CA_SECURITY_EXT being globally disabled on the CA server via the disable_extension_list. If it's not disabled, skip - if @registry_values[ca_name]&.[](:disable_extension_list)&.include?('1.3.6.1.4.1.311.25.2') && @registry_values[:strong_certificate_binding_enforcement] && (@registry_values[:strong_certificate_binding_enforcement] == 0 || @registry_values[:strong_certificate_binding_enforcement] == 1) + if vulnerable_to_esc16_1?(ca_name) next if users.empty? next unless users_compatible_with_template?(users, entry['mspki-certificate-name-flag']) @@ -760,7 +760,7 @@ def find_esc16_vuln_cert_templates # Scenario 1 - StrongCertificateBindingEnforcement = 1 or 0 then it's the same as ESC9 - mark them all as vulnerable @certificate_details[certificate_symbol][:techniques] << 'ESC16_1' @certificate_details[certificate_symbol][:notes] << note - elsif @registry_values[ca_name]&.[](:disable_extension_list)&.include?('1.3.6.1.4.1.311.25.2') && @registry_values[ca_name][:edit_flags] & EDITF_ATTRIBUTESUBJECTALTNAME2 != 0 + elsif vulnerable_to_esc16_2?(ca_name) # Scenario 2 - StrongCertificateBindingEnforcement = 2 but the edit_flags contain EDITF_ATTRIBUTESUBJECTALTNAME2 which re-enables the ability to exploit the certificate in the same way as ESC6 @certificate_details[certificate_symbol][:techniques] << 'ESC16_2' @certificate_details[certificate_symbol][:notes] << "ESC16: Template is vulnerable due to the active policy EditFlags having: EDITF_ATTRIBUTESUBJECTALTNAME2 set (which is essentially ESC6) on the Certificate Authority: #{ca_name}. Also the CA having 1.3.6.1.4.1.311.25.2 defined in it's disabled extension list" @@ -779,6 +779,14 @@ def find_esc16_vuln_cert_templates end end + def vulnerable_to_esc16_1?(ca_name) + @registry_values[ca_name]&.[](:disable_extension_list)&.include?('1.3.6.1.4.1.311.25.2') && @registry_values[:strong_certificate_binding_enforcement] && (@registry_values[:strong_certificate_binding_enforcement] == 0 || @registry_values[:strong_certificate_binding_enforcement] == 1) + end + + def vulnerable_to_esc16_2?(ca_name) + @registry_values[ca_name]&.[](:disable_extension_list)&.include?('1.3.6.1.4.1.311.25.2') && @registry_values[ca_name][:edit_flags] & EDITF_ATTRIBUTESUBJECTALTNAME2 != 0 && @registry_values[:strong_certificate_binding_enforcement] && @registry_values[:strong_certificate_binding_enforcement] == 2 + end + def find_enrollable_vuln_certificate_templates # For each of the vulnerable certificate templates, determine which servers # allows users to enroll in that certificate template and which users/groups @@ -1087,7 +1095,7 @@ def domain_controller_version_check end if version_raw.blank? - fail_with(Failure::Unknown, "Could not retrieve Windows version string from #{datastore['RHOSTS']} via WinRM.") + print_error("Could not retrieve Windows version string from #{datastore['RHOSTS']} via WinRM.") end version_obj = Rex::Version.new(version_raw) @@ -1114,7 +1122,7 @@ def domain_controller_version_check end end - fail_with(Failure::Unknown, "Could not map detected Windows version #{version_obj} to a known product range. Cannot proceed with module execution.") + print_error("Could not map detected Windows version #{version_obj} to a known product range.") end def set_can_enroll_flags From 9a03cab1ec6f62bc088d7de73c0ec51f6ec37289 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Mon, 3 Nov 2025 08:32:28 -0800 Subject: [PATCH 09/15] Updated compatible users check --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index e6c2458659eaf..203f9d1af6acb 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -498,14 +498,13 @@ def find_esc9_vuln_cert_templates enroll_sids = @certificate_details[certificate_symbol][:enroll_sids] users = find_users_with_write_and_enroll_rights(enroll_sids) + current_user = adds_get_current_user(@ldap)[:samaccountname].first next if users.empty? - next unless users_compatible_with_template?(users, template['mspki-certificate-name-flag']) + next unless users_compatible_with_template?(current_user, users, template['mspki-certificate-name-flag']) user_plural = users.size > 1 ? 'accounts' : 'account' has_plural = users.size > 1 ? 'have' : 'has' - current_user = adds_get_current_user(@ldap)[:samaccountname].first - note = "ESC9: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." if @registry_values[:strong_certificate_binding_enforcement].present? note += " Registry value: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}." @@ -539,14 +538,13 @@ def find_esc10_vuln_cert_templates enroll_sids = @certificate_details[certificate_symbol][:enroll_sids] users = find_users_with_write_and_enroll_rights(enroll_sids) + current_user = adds_get_current_user(@ldap)[:samaccountname].first next if users.empty? - next unless users_compatible_with_template?(users, template['mspki-certificate-name-flag']) + next unless users_compatible_with_template?(current_user, users, template['mspki-certificate-name-flag']) user_plural = users.size > 1 ? 'accounts' : 'account' has_plural = users.size > 1 ? 'have' : 'has' - current_user = adds_get_current_user(@ldap)[:samaccountname].first - note = "ESC10: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." if @registry_values[:strong_certificate_binding_enforcement].present? && @registry_values[:certificate_mapping_methods].present? @@ -700,7 +698,7 @@ def find_esc15_vuln_cert_templates query_ldap_server_certificates(esc_raw_filter, 'ESC15', notes: notes) end - def users_compatible_with_template?(users, flag_values) + def users_compatible_with_template?(current_user, users, flag_values) return false if users.blank? || flag_values.blank? raw = flag_values.is_a?(Array) ? flag_values.first : flag_values @@ -708,9 +706,9 @@ def users_compatible_with_template?(users, flag_values) mask = raw.to_i & 0xffffffff - if (mask & CT_FLAG_SUBJECT_ALT_REQUIRE_DNS) != 0 && users.any? { |user| user.end_with?('$') } + if (mask & CT_FLAG_SUBJECT_ALT_REQUIRE_DNS) != 0 && users.any? { |user| user.end_with?('$') } && current_user.to_s.end_with?('$') true - elsif (mask & CT_FLAG_SUBJECT_ALT_REQUIRE_UPN) != 0 && users.any? { |user| !user.end_with?('$') } + elsif (mask & CT_FLAG_SUBJECT_ALT_REQUIRE_UPN) != 0 && users.any? { |user| !user.end_with?('$') } && !current_user.to_s.end_with?('$') true else false @@ -751,7 +749,7 @@ def find_esc16_vuln_cert_templates # ESC16 revolves around the szOID_NTDS_CA_SECURITY_EXT being globally disabled on the CA server via the disable_extension_list. If it's not disabled, skip if vulnerable_to_esc16_1?(ca_name) next if users.empty? - next unless users_compatible_with_template?(users, entry['mspki-certificate-name-flag']) + next unless users_compatible_with_template?(current_user, users, entry['mspki-certificate-name-flag']) note = "ESC16: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." note += " Registry values: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}, CertificateMappingMethods=#{@registry_values[:certificate_mapping_methods]}." @@ -770,7 +768,7 @@ def find_esc16_vuln_cert_templates @certificate_details[certificate_symbol][:notes] << 'ESC16_2: Template appears to be vulnerable (most templates do)' next if users.empty? - next unless users_compatible_with_template?(users, entry['mspki-certificate-name-flag']) + next unless users_compatible_with_template?(current_user, users, entry['mspki-certificate-name-flag']) @certificate_details[certificate_symbol][:techniques] << 'ESC16_1' @certificate_details[certificate_symbol][:notes] << "ESC16_1: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." From b0ffe3e0edd34d7513b31968b3909fd59e3707bb Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Tue, 4 Nov 2025 21:50:13 -0800 Subject: [PATCH 10/15] Improve ESC16_2 accuracy --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index 203f9d1af6acb..3e6fd2e821b6f 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -500,7 +500,7 @@ def find_esc9_vuln_cert_templates users = find_users_with_write_and_enroll_rights(enroll_sids) current_user = adds_get_current_user(@ldap)[:samaccountname].first next if users.empty? - next unless users_compatible_with_template?(current_user, users, template['mspki-certificate-name-flag']) + next unless users_compatible_with_template?(current_user, template['mspki-certificate-name-flag'], users) user_plural = users.size > 1 ? 'accounts' : 'account' has_plural = users.size > 1 ? 'have' : 'has' @@ -540,7 +540,7 @@ def find_esc10_vuln_cert_templates users = find_users_with_write_and_enroll_rights(enroll_sids) current_user = adds_get_current_user(@ldap)[:samaccountname].first next if users.empty? - next unless users_compatible_with_template?(current_user, users, template['mspki-certificate-name-flag']) + next unless users_compatible_with_template?(current_user, template['mspki-certificate-name-flag'], users) user_plural = users.size > 1 ? 'accounts' : 'account' has_plural = users.size > 1 ? 'have' : 'has' @@ -698,17 +698,21 @@ def find_esc15_vuln_cert_templates query_ldap_server_certificates(esc_raw_filter, 'ESC15', notes: notes) end - def users_compatible_with_template?(current_user, users, flag_values) - return false if users.blank? || flag_values.blank? + # For ESC9, ESC10 and ESC16 + def users_compatible_with_template?(current_user, flag_values, users = nil) + return false if flag_values.blank? raw = flag_values.is_a?(Array) ? flag_values.first : flag_values return false if raw.nil? mask = raw.to_i & 0xffffffff - if (mask & CT_FLAG_SUBJECT_ALT_REQUIRE_DNS) != 0 && users.any? { |user| user.end_with?('$') } && current_user.to_s.end_with?('$') + dns_required = (mask & CT_FLAG_SUBJECT_ALT_REQUIRE_DNS) != 0 + upn_required = (mask & CT_FLAG_SUBJECT_ALT_REQUIRE_UPN) != 0 + + if dns_required && current_user.to_s.end_with?('$') && (users.blank? || users.any? { |user| user.end_with?('$') }) true - elsif (mask & CT_FLAG_SUBJECT_ALT_REQUIRE_UPN) != 0 && users.any? { |user| !user.end_with?('$') } && !current_user.to_s.end_with?('$') + elsif upn_required && !current_user.to_s.end_with?('$') && (users.blank? || users.any? { |user| !user.end_with?('$') }) true else false @@ -749,7 +753,7 @@ def find_esc16_vuln_cert_templates # ESC16 revolves around the szOID_NTDS_CA_SECURITY_EXT being globally disabled on the CA server via the disable_extension_list. If it's not disabled, skip if vulnerable_to_esc16_1?(ca_name) next if users.empty? - next unless users_compatible_with_template?(current_user, users, entry['mspki-certificate-name-flag']) + next unless users_compatible_with_template?(current_user, entry['mspki-certificate-name-flag'], users) note = "ESC16: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." note += " Registry values: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}, CertificateMappingMethods=#{@registry_values[:certificate_mapping_methods]}." @@ -764,11 +768,12 @@ def find_esc16_vuln_cert_templates @certificate_details[certificate_symbol][:notes] << "ESC16: Template is vulnerable due to the active policy EditFlags having: EDITF_ATTRIBUTESUBJECTALTNAME2 set (which is essentially ESC6) on the Certificate Authority: #{ca_name}. Also the CA having 1.3.6.1.4.1.311.25.2 defined in it's disabled extension list" elsif @registry_values.blank? # We couldn't read the registry values - mark as potentially vulnerable + next unless users_compatible_with_template?(current_user, entry['mspki-certificate-name-flag']) @certificate_details[certificate_symbol][:techniques] << 'ESC16_2' @certificate_details[certificate_symbol][:notes] << 'ESC16_2: Template appears to be vulnerable (most templates do)' next if users.empty? - next unless users_compatible_with_template?(current_user, users, entry['mspki-certificate-name-flag']) + next unless users_compatible_with_template?(current_user, entry['mspki-certificate-name-flag'], users) @certificate_details[certificate_symbol][:techniques] << 'ESC16_1' @certificate_details[certificate_symbol][:notes] << "ESC16_1: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." From dee55e0fa992f95e59728117260d70b65eed3a13 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Thu, 6 Nov 2025 11:58:59 -0500 Subject: [PATCH 11/15] Make parsing cert error more specific --- lib/msf/core/exploit/remote/ms_icpr.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/exploit/remote/ms_icpr.rb b/lib/msf/core/exploit/remote/ms_icpr.rb index fd22f581f9cc5..5c2a3299fbe86 100644 --- a/lib/msf/core/exploit/remote/ms_icpr.rb +++ b/lib/msf/core/exploit/remote/ms_icpr.rb @@ -431,7 +431,7 @@ def get_cert_policy_oids(cert) all_oids << (Rex::Proto::CryptoAsn1::OIDs.value(oid_string) || Rex::Proto::CryptoAsn1::ObjectId.new(oid_string)) end rescue StandardError => e - vprint_error("Failed to parse ms-app-policies: #{e.class}: #{e.message}") + vprint_error("Failed to parse ms-app-policies from certificate with subject:\"#{cert.subject.to_s}\" and issuer:\"#{cert.issuer.to_s}\". #{e.class}: #{e.message}") end end @@ -448,7 +448,7 @@ def get_cert_policy_oids(cert) end end rescue StandardError => e - vprint_error("Failed to parse extendedKeyUsage: #{e.class}: #{e.message}") + vprint_error("Failed to parse extendedKeyUsage from certificate with subject:\"#{cert.subject.to_s}\" and issuer:\"#{cert.issuer.to_s}\". #{e.class}: #{e.message}") end end From 5afcf5b82749a11b600a5286eb9cba691a0e3dbe Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Thu, 6 Nov 2025 13:43:46 -0500 Subject: [PATCH 12/15] Add empty line after guard clause --- modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index e4cbcb8e0a446..b1116dece5951 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -769,6 +769,7 @@ def find_esc16_vuln_cert_templates elsif @registry_values.blank? # We couldn't read the registry values - mark as potentially vulnerable next unless users_compatible_with_template?(current_user, entry['mspki-certificate-name-flag']) + @certificate_details[certificate_symbol][:techniques] << 'ESC16_2' @certificate_details[certificate_symbol][:notes] << 'ESC16_2: Template appears to be vulnerable (most templates do)' From bc55aee7f72fd50da73213de15abe6618586ccf6 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 7 Nov 2025 11:13:41 -0500 Subject: [PATCH 13/15] Updated log line when running as admin --- modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index b1116dece5951..6c64b533d965c 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -755,7 +755,7 @@ def find_esc16_vuln_cert_templates next if users.empty? next unless users_compatible_with_template?(current_user, entry['mspki-certificate-name-flag'], users) - note = "ESC16: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." + note = "ESC16_1: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." note += " Registry values: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}, CertificateMappingMethods=#{@registry_values[:certificate_mapping_methods]}." note += " The Certificate Authority: #{ca_name} has 1.3.6.1.4.1.311.25.2 defined in it's disabled extension list" @@ -765,7 +765,7 @@ def find_esc16_vuln_cert_templates elsif vulnerable_to_esc16_2?(ca_name) # Scenario 2 - StrongCertificateBindingEnforcement = 2 but the edit_flags contain EDITF_ATTRIBUTESUBJECTALTNAME2 which re-enables the ability to exploit the certificate in the same way as ESC6 @certificate_details[certificate_symbol][:techniques] << 'ESC16_2' - @certificate_details[certificate_symbol][:notes] << "ESC16: Template is vulnerable due to the active policy EditFlags having: EDITF_ATTRIBUTESUBJECTALTNAME2 set (which is essentially ESC6) on the Certificate Authority: #{ca_name}. Also the CA having 1.3.6.1.4.1.311.25.2 defined in it's disabled extension list" + @certificate_details[certificate_symbol][:notes] << "ESC16_2: Template is vulnerable due to the active policy EditFlags having: EDITF_ATTRIBUTESUBJECTALTNAME2 set (which is essentially ESC6) on the Certificate Authority: #{ca_name}. Also the CA having 1.3.6.1.4.1.311.25.2 defined in it's disabled extension list" elsif @registry_values.blank? # We couldn't read the registry values - mark as potentially vulnerable next unless users_compatible_with_template?(current_user, entry['mspki-certificate-name-flag']) From 0e455b138e432bd2af237932283dad2414a80230 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 7 Nov 2025 14:00:31 -0500 Subject: [PATCH 14/15] Update ESC16 if elsif --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index 6c64b533d965c..91437ed9152b8 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -762,23 +762,26 @@ def find_esc16_vuln_cert_templates # Scenario 1 - StrongCertificateBindingEnforcement = 1 or 0 then it's the same as ESC9 - mark them all as vulnerable @certificate_details[certificate_symbol][:techniques] << 'ESC16_1' @certificate_details[certificate_symbol][:notes] << note - elsif vulnerable_to_esc16_2?(ca_name) + end + + if vulnerable_to_esc16_2?(ca_name) # Scenario 2 - StrongCertificateBindingEnforcement = 2 but the edit_flags contain EDITF_ATTRIBUTESUBJECTALTNAME2 which re-enables the ability to exploit the certificate in the same way as ESC6 @certificate_details[certificate_symbol][:techniques] << 'ESC16_2' @certificate_details[certificate_symbol][:notes] << "ESC16_2: Template is vulnerable due to the active policy EditFlags having: EDITF_ATTRIBUTESUBJECTALTNAME2 set (which is essentially ESC6) on the Certificate Authority: #{ca_name}. Also the CA having 1.3.6.1.4.1.311.25.2 defined in it's disabled extension list" - elsif @registry_values.blank? - # We couldn't read the registry values - mark as potentially vulnerable - next unless users_compatible_with_template?(current_user, entry['mspki-certificate-name-flag']) + end - @certificate_details[certificate_symbol][:techniques] << 'ESC16_2' - @certificate_details[certificate_symbol][:notes] << 'ESC16_2: Template appears to be vulnerable (most templates do)' + next unless @registry_values.blank? + # We couldn't read the registry values - mark as potentially vulnerable + next unless users_compatible_with_template?(current_user, entry['mspki-certificate-name-flag']) - next if users.empty? - next unless users_compatible_with_template?(current_user, entry['mspki-certificate-name-flag'], users) + @certificate_details[certificate_symbol][:techniques] << 'ESC16_2' + @certificate_details[certificate_symbol][:notes] << 'ESC16_2: Template appears to be vulnerable (most templates do)' - @certificate_details[certificate_symbol][:techniques] << 'ESC16_1' - @certificate_details[certificate_symbol][:notes] << "ESC16_1: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." - end + next if users.empty? + next unless users_compatible_with_template?(current_user, entry['mspki-certificate-name-flag'], users) + + @certificate_details[certificate_symbol][:techniques] << 'ESC16_1' + @certificate_details[certificate_symbol][:notes] << "ESC16_1: The account: #{current_user} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." end end end From df977f9f6198fb6da7b46d09ee6b51465c45639c Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 7 Nov 2025 14:22:12 -0500 Subject: [PATCH 15/15] Fix ESC16_2 potentially vuln reporting when not enrollable --- .../auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index 91437ed9152b8..5d49eb07f3e14 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -820,14 +820,16 @@ def reporting_split_techniques(template) # these techniques are special in the sense that the exploit steps involve a different user performing the request # meaning that whether or not we can issue them is irrelevant enroll_by_proxy = %w[ESC9 ESC10 ESC16_1] - # technically ESC15 might be patched and we can't fingerprint that status but we live it in the "vulnerable" category + # technically ESC15 might be patched and we can't fingerprint that status but we leave it in the "vulnerable" category # when we have the registry values, we can tell the vulnerabilities for certain if @registry_values.present? potentially_vulnerable = [] vulnerable = template[:techniques].dup else - potentially_vulnerable = template[:techniques] & enroll_by_proxy + # ESC16_2 doesn't require a separate user to enroll, so it does not belong in the enroll_by_proxy array + # however should it should be reported as potentially vulnerable if we don't have registry data + potentially_vulnerable = template[:techniques] & (enroll_by_proxy + ['ESC16_2']) vulnerable = template[:techniques] - potentially_vulnerable end @@ -835,6 +837,9 @@ def reporting_split_techniques(template) vulnerable.keep_if do |technique| enroll_by_proxy.include?(technique) || can_enroll?(template) end + + potentially_vulnerable.delete('ESC16_2') if potentially_vulnerable.include?('ESC16_2') && !can_enroll?(template) + end [vulnerable, potentially_vulnerable]