diff --git a/documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md b/documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md index aa5ad9240a779..0cdf7f20115d1 100644 --- a/documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md +++ b/documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md @@ -222,25 +222,24 @@ In order to create a template vulnerable to ESC16 scenario 1, follow the first 1 which is all the steps up to and excluding the `msPKI-Enrollment-Flag", 0x80000` powershell step which is how you set the `CT_FLAG_NO_SECURITY_EXTENSION`. Ensure that `StrongCertificateBindingEnforcement` is set to `0` or `1` (not `2`) by running the following command listed in `Configuring Windows to be Vulnerable to ESC9` -### ESC16 Scenario 2 +#### ESC16 Scenario 2 When a CA has the OID `1.3.6.1.4.1.311.25.2` added to its `policy\DisableExtensionList` and `StrongCertificateBindingEnforcement` is set to `2`, there is still a way to exploit the template. If the policy module's `EditFlags` has the `EDITF_ATTRIBUTESUBJECTALTNAME2` flag set (which is essentially ESC6), then the template is vulnerable to ESC16 scenario 2. Ensure the `EDITF_ATTRIBUTESUBJECTALTNAME2` flag is set by running following PowerShell command: ```powershell -$EDITF_ATTRIBUTESUBJECTALTNAME2 = 0x00040000 -$activePolicyName = (Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\*\PolicyModules" -Name "Active").Active -$editFlagsPath = "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\*\PolicyModules\$activePolicyName" -$editFlags = (Get-ItemProperty -Path $editFlagsPath -Name "EditFlags").EditFlags +certutil -setreg policy\EditFlags +EDITF_ATTRIBUTESUBJECTALTNAME2 +``` -if ($editFlags -band $EDITF_ATTRIBUTESUBJECTALTNAME2) { - Write-Output "The EDITF_ATTRIBUTESUBJECTALTNAME2 flag is already enabled." -} else { - # Enable the flag by setting it in the EditFlags value - $newEditFlags = $editFlags -bor $EDITF_ATTRIBUTESUBJECTALTNAME2 - Set-ItemProperty -Path $editFlagsPath -Name "EditFlags" -Value $newEditFlags - Write-Output "The EDITF_ATTRIBUTESUBJECTALTNAME2 flag has been enabled." -} +Then restart the Certificate Services service: +```powershell +net stop certsvc +net start certsvc +``` + +Then vefify the flag is set by running: +```powershell +certutil -getreg policy\EditFlags ``` ## Module usage diff --git a/lib/msf/core/exploit/remote/ldap/active_directory.rb b/lib/msf/core/exploit/remote/ldap/active_directory.rb index 5abe06e9ac048..5cd29029bcc35 100644 --- a/lib/msf/core/exploit/remote/ldap/active_directory.rb +++ b/lib/msf/core/exploit/remote/ldap/active_directory.rb @@ -253,6 +253,26 @@ def adds_get_domain_info(ldap) } end + # Query the LDAP server to retrieve all Certificate Authority (Enterprise CA) servers in the domain. + # @param [Net::LDAP::Connection] ldap The LDAP connection to use for querying. + # @return [Array] An array of hashes, where each hash contains the `:name` and `:dNSHostName` of a CA server. + # @rtype [Array] + def adds_get_ca_servers(ldap) + base_dn = "CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,#{@base_dn}" + filter = '(objectClass=pKIEnrollmentService)' + attributes = ['cn', 'dNSHostName'] + ca_servers = [] + + ldap.search(base: base_dn, filter: filter, attributes: attributes) do |entry| + ca_servers << { + name: entry[:cn]&.first, + dNSHostName: entry[:dNSHostName]&.first + } + end + + ca_servers + end + # Determine if a security descriptor will grant the permissions identified by *matcher* to the # *test_sid*. # diff --git a/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb b/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb index c6f19fa18f59e..5696dcf8638fa 100644 --- a/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb +++ b/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb @@ -7,6 +7,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::LDAP + include Msf::Exploit::Remote::LDAP::ActiveDirectory include Msf::Exploit::Remote::MsIcpr include Msf::Exploit::Remote::SMB::Client::Authenticated include Msf::Exploit::Remote::DCERPC @@ -89,6 +90,7 @@ def validate_options end def run + @dc_ip = datastore['RHOSTS'] validate_options send("action_#{action.name.downcase}") rescue MsIcprConnectionError, SmbIpcConnectionError => e @@ -214,13 +216,15 @@ def action_request_cert @device_id, cert_path = call_shadow_credentials_module('add') smbpass = automate_get_hash(cert_path, datastore['TARGET_USERNAME'], datastore['LDAPDomain'], datastore['RHOSTS']) end - + ca_ip = resolve_ca_ip with_ipc_tree do |opts| datastore['SMBUser'] = datastore['TARGET_USERNAME'] datastore['SMBPass'] = smbpass + datastore['RHOSTS'] = ca_ip request_certificate(opts) end ensure + datastore['RHOSTS'] = @dc_ip unless @device_id.nil? print_status('Removing shadow credential') call_shadow_credentials_module('remove', device_id: @device_id) @@ -229,6 +233,48 @@ def action_request_cert revert_ldap_object end + def resolve_ca_ip + vprint_status('Finding CA server in LDAP') + ca_servers = [] + ldap_connect(port: datastore['LDAPRport']) do |ldap| + validate_bind_success!(ldap) + if (@base_dn = datastore['BASE_DN']) + print_status("User-specified base DN: #{@base_dn}") + else + print_status('Discovering base DN automatically') + + unless (@base_dn = ldap.base_dn) + fail_with(Failure::NotFound, "Couldn't discover base DN!") + end + end + ca_servers = adds_get_ca_servers(ldap) + vprint_status("Found #{ca_servers.length} CA servers in LDAP") + end + + if ca_servers.empty? + fail_with(Msf::Module::Failure::UnexpectedReply, 'No Certificate Authority servers found in LDAP.') + return + else + ca_servers.each do |ca| + vprint_good("Found CA: #{ca[:name]} (#{ca[:dNSHostName]})") + end + end + + ca_entry = ca_servers.find { |ca| ca[:name].casecmp?(datastore['CA']) } + + unless ca_entry + fail_with(Msf::Module::Failure::UnexpectedReply, "CA #{datastore['CA']} not found in LDAP. Checking registry values is unable to continue") + end + + ca_dns_hostname = ca_entry[:dNSHostName] + ca_ip_address = Rex::Socket.getaddress(ca_dns_hostname, false) + unless ca_ip_address + print_error("Unable to resolve the DNS Host Name of the CA server: #{ca_dns_hostname}. Checking registry values is unable to continue") + return + end + ca_ip_address + end + def revert_ldap_object # If the UPN was changed the certificate we requested won't work until we revert the UPN change. If the # dnsHostName was changed the cert will still work however we'll revert the change to keep the system clean. diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index 7bf1e4c9dd940..e9f88a7b6204f 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -97,12 +97,13 @@ def initialize(info = {}) OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']), OptEnum.new('REPORT', [true, 'What templates to report (applies filtering to results)', 'vulnerable-and-published', %w[all published enrollable vulnerable vulnerable-and-published vulnerable-and-enrollable]]), OptBool.new('RUN_REGISTRY_CHECKS', [true, 'Authenticate to WinRM to query the registry values to enhance reporting for ESC9, ESC10 and ESC16. Must be a privileged user in order to query successfully', false]), + OptInt.new('WINRM_TIMEOUT', [false, 'The WinRM timeout when running registry checks', 20], conditions: %w[RUN_REGISTRY_CHECKS == true]), ]) end # TODO: Spencer to check all of these are still used and shouldn't be moved # Constants Definition - CERTIFICATE_ATTRIBUTES = %w[cn name description nTSecurityDescriptor msPKI-Certificate-Policy msPKI-Enrollment-Flag msPKI-RA-Signature msPKI-Template-Schema-Version pkiExtendedKeyUsage] + CERTIFICATE_ATTRIBUTES = %w[cn name description nTSecurityDescriptor msPKI-Certificate-Policy msPKI-Enrollment-Flag msPKI-RA-Signature msPKI-Template-Schema-Version pkiExtendedKeyUsage msPKI-Certificate-Name-Flag] CERTIFICATE_TEMPLATES_BASE = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration'.freeze CONTROL_ACCESS = 0x00000100 @@ -349,35 +350,78 @@ def run_registry_command(shell, path, property_name, dynamic_value = nil) value end - def enum_registry_values - @registry_values ||= {} - - endpoint = "http://#{datastore['RHOST']}:5985/wsman" - domain = adds_get_domain_info(@ldap)[:dns_name] - user = adds_get_current_user(@ldap)[:sAMAccountName].first.to_s - pass = datastore['LDAPPassword'] - conn = WinRM::Connection.new( + def create_winrm_connection(host, domain, user, timeout) + endpoint = "http://#{host}:5985/wsman" + WinRM::Connection.new( endpoint: endpoint, domain: domain, user: user, - password: pass, - transport: :negotiate + password: datastore['LDAPPassword'], + transport: :negotiate, + operation_timeout: timeout ) + end - begin - conn.shell(:powershell) do |shell| - @registry_values[:certificate_mapping_methods] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\Schannel', 'CertificateMappingMethods').to_i - @registry_values[:strong_certificate_binding_enforcement] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Kdc', 'StrongCertificateBindingEnforcement').to_i + def query_ca_policy_values(shell) + active_policy_name = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\*\\PolicyModules', 'Active') + disable_ext = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\*\\PolicyModules', 'DisableExtensionList', active_policy_name) + edit_flags = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\*\\PolicyModules', 'EditFlags', active_policy_name).to_i + { disable_extension_list: disable_ext, edit_flags: edit_flags } + end - active_policy_name = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\*\\PolicyModules', 'Active') - @registry_values[:disable_extension_list] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\*\\PolicyModules', 'DisableExtensionList', active_policy_name) - @registry_values[:edit_flags] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\*\\PolicyModules', 'EditFlags', active_policy_name).to_i + def query_dc_reg_values(ca_name, ca_ip_address, domain, user) + conn = create_winrm_connection(datastore['RHOST'], domain, user, datastore['WINRM_TIMEOUT']) + handled_locally = false + conn.shell(:powershell) do |shell| + @registry_values[:certificate_mapping_methods] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\Schannel', 'CertificateMappingMethods').to_i + @registry_values[:strong_certificate_binding_enforcement] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Kdc', 'StrongCertificateBindingEnforcement').to_i + if datastore['RHOST'] == ca_ip_address + @registry_values[ca_name.to_sym] = query_ca_policy_values(shell) + handled_locally = true end - rescue StandardError => e - vprint_warning("Failed to query registry values: #{e.message}") + shell.close + end + return if handled_locally + + query_ca_reg_values(ca_name, ca_ip_address, domain, user) + end + + def query_ca_reg_values(ca_name, ca_ip_address, domain, user) + conn = create_winrm_connection(ca_ip_address, domain, user, datastore['WINRM_TIMEOUT']) + conn.shell(:powershell) do |shell| + @registry_values[ca_name.to_sym] = query_ca_policy_values(shell) + shell.close + end + end + + def enum_registry_values + @registry_values ||= {} + domain = adds_get_domain_info(@ldap)[:dns_name] + user = adds_get_current_user(@ldap)[:sAMAccountName].first.to_s + ca_servers = adds_get_ca_servers(@ldap) + if ca_servers.empty? + print_warning('No Certificate Authority servers found in LDAP.') + return + end + + ca_servers.each do |ca_server| + vprint_good("Found CA: #{ca_server[:name]} (#{ca_server[:dNSHostName]})") + ca_ip_address = Rex::Socket.getaddress(ca_server[:dNSHostName], false) + unless ca_ip_address + vprint_error("Unable to resolve the DNS Host Name of the CA server: #{ca_server[:dNSHostName]}. Checking registry values is unable to continue") + next + end + + query_dc_reg_values(ca_server[:name], ca_ip_address, domain, user) end @registry_values + rescue StandardError => e + print_error("Failed to query all registry values. Ensure the user has sufficient privileges to query the Domain Controller and Certificate Authorities: #{e.message}.") + if @registry_values.key?(:certificate_mapping_methods) && @registry_values.key?(:strong_certificate_binding_enforcement) + print_error(' The user was able to query the Domain Controller but not the Certificate Authorities, meaning the user is likely an Admin but not a Domain Admin. ESC16 reporting will be inaccurate.') + return @registry_values + end end def resolve_group_memberships(user_dn) @@ -448,7 +492,7 @@ def find_esc9_vuln_cert_templates ')'\ ')' - esc9_templates = query_ldap_server(esc9_raw_filter, CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE) + esc9_templates = query_ldap_server(esc9_raw_filter, CERTIFICATE_ATTRIBUTES + ['msPKI-Certificate-Name-Flag'], base_prefix: CERTIFICATE_TEMPLATES_BASE) esc9_templates.each do |template| certificate_symbol = template[:cn][0].to_sym @@ -465,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][:certificate_name_flags] = template['mspki-certificate-name-flag'] @certificate_details[certificate_symbol][:techniques] << 'ESC9' @certificate_details[certificate_symbol][:notes] << note end @@ -486,7 +531,7 @@ def find_esc10_vuln_cert_templates ')'\ ')' - esc10_templates = query_ldap_server(esc10_raw_filter, CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE) + esc10_templates = query_ldap_server(esc10_raw_filter, CERTIFICATE_ATTRIBUTES + ['msPKI-Certificate-Name-Flag'], base_prefix: CERTIFICATE_TEMPLATES_BASE) esc10_templates.each do |template| certificate_symbol = template[:cn][0].to_sym @@ -504,7 +549,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][:certificate_name_flags] = template['mspki-certificate-name-flag'] @certificate_details[certificate_symbol][:techniques] << 'ESC10' @certificate_details[certificate_symbol][:notes] << note end @@ -652,8 +697,6 @@ def find_esc15_vuln_cert_templates 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 - return if @registry_values.present? && @registry_values[:disable_extension_list] && !@registry_values[:disable_extension_list].include?('1.3.6.1.4.1.311.25.2') - esc16_raw_filter = '(&'\ '(|'\ "(mspki-certificate-name-flag:1.2.840.113556.1.4.804:=#{CT_FLAG_SUBJECT_ALT_REQUIRE_UPN})"\ @@ -666,24 +709,28 @@ def find_esc16_vuln_cert_templates '(pkiextendedkeyusage=*)'\ ')' - esc_entries = query_ldap_server(esc16_raw_filter, CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE) + esc_entries = query_ldap_server(esc16_raw_filter, CERTIFICATE_ATTRIBUTES + ['msPKI-Certificate-Name-Flag'], base_prefix: CERTIFICATE_TEMPLATES_BASE) return if esc_entries.empty? - 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 - esc_entries.each do |entry| - certificate_symbol = entry[:cn][0].to_sym - - @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 CA's disabled policy extension list includes: 1.3.6.1.4.1.311.25.2." - end - elsif @registry_values[:edit_flags] & EDITF_ATTRIBUTESUBJECTALTNAME2 != 0 - # Scenario 2 - StrongCertificateBindingEnforcement = 2 (or nil) but if EditFlags in the active policy module has EDITF_ATTRIBUTESUBJECTALTNAME2 set then ESC6 is essentially re-enabled and we mark them all as vulnerable - esc_entries.each do |entry| - certificate_symbol = entry[:cn][0].to_sym + esc_entries.each do |entry| + certificate_symbol = entry[:cn][0].to_sym - @certificate_details[certificate_symbol][:techniques] << 'ESC16' - @certificate_details[certificate_symbol][:notes] << 'ESC16: Template is vulnerable due to the active policy EditFlags having: EDITF_ATTRIBUTESUBJECTALTNAME2 set (which is essentially ESC6) combined with the CA\'s disabled policy extension list including: 1.3.6.1.4.1.311.25.2.' + # 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')) + + 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 + # 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][: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" + end end end end @@ -1010,6 +1057,12 @@ def run registry_values = enum_registry_values if datastore['RUN_REGISTRY_CHECKS'] + if registry_values.any? + registry_values.each do |key, value| + vprint_good("#{key}: #{value.inspect}") + end + end + find_enrollable_vuln_certificate_templates find_esc1_vuln_cert_templates find_esc2_vuln_cert_templates