Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
37 changes: 31 additions & 6 deletions lib/msf/core/exploit/remote/ms_icpr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -420,14 +420,39 @@ def build_on_behalf_of(csr:, on_behalf_of:, cert:, key:, algorithm: 'SHA256')
# @param [OpenSSL::X509::Certificate] cert
# @return [Array<Rex::Proto::CryptoAsn1::ObjectId>] 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


Expand Down
149 changes: 132 additions & 17 deletions modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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][: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
Expand All @@ -525,11 +527,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|
Expand All @@ -538,6 +540,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'
Expand All @@ -549,6 +552,8 @@ 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
Expand Down Expand Up @@ -695,6 +700,23 @@ 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?

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 = '(&'\
Expand All @@ -718,18 +740,40 @@ 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)
@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

# 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'))
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?
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"

if @registry_values[:strong_certificate_binding_enforcement] && (@registry_values[:strong_certificate_binding_enforcement] == 0 || @registry_values[:strong_certificate_binding_enforcement] == 1)
# Scenario 1 - StrongCertificateBindingEnforcement = 1 or 0 then it's the same as ESC9 - mark them all as vulnerable
@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"
elsif @registry_values[ca_name][:edit_flags] & EDITF_ATTRIBUTESUBJECTALTNAME2 != 0
@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
# 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][: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
Expand Down Expand Up @@ -760,7 +804,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
Expand All @@ -782,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
Expand Down Expand Up @@ -867,8 +911,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(', ')}")
Expand Down Expand Up @@ -1018,6 +1065,64 @@ 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 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)
Expand Down Expand Up @@ -1047,6 +1152,15 @@ 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.
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?

Expand All @@ -1057,13 +1171,14 @@ 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
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
Expand Down