From 3fb94b46c481a37fabc9c61fc0461775af6df89c Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 14 Jan 2025 16:43:04 +0000 Subject: [PATCH 01/11] Update the ESC finder module's reporting --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 181 ++++++++++++------ 1 file changed, 121 insertions(+), 60 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index b72ab5f2d335..eb2c5c8398b0 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -31,6 +31,8 @@ def rid end end + attr_reader :certificate_details + def initialize(info = {}) super( update_info( @@ -206,7 +208,7 @@ def query_ldap_server(raw_filter, attributes, base_prefix: nil) end def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: []) - attributes = ['cn', 'description', 'ntSecurityDescriptor', 'msPKI-Enrollment-Flag', 'msPKI-RA-Signature', 'PkiExtendedKeyUsage'] + attributes = ['cn', 'name', 'description', 'ntSecurityDescriptor', 'msPKI-Enrollment-Flag', 'msPKI-RA-Signature', 'PkiExtendedKeyUsage'] base_prefix = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration' esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: base_prefix) @@ -228,15 +230,16 @@ def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: []) next if allowed_sids.empty? certificate_symbol = entry[:cn][0].to_sym - if @vuln_certificate_details.key?(certificate_symbol) - @vuln_certificate_details[certificate_symbol][:vulns] << esc_name - @vuln_certificate_details[certificate_symbol][:notes] += notes + if @certificate_details.key?(certificate_symbol) + @certificate_details[certificate_symbol][:techniques] << esc_name + @certificate_details[certificate_symbol][:notes] += notes else - @vuln_certificate_details[certificate_symbol] = { - vulns: [esc_name], - dn: entry[:dn][0], - certificate_enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), - ca_servers_n_enrollment_sids: {}, + @certificate_details[certificate_symbol] = { + name: entry[:name][0].to_s, + techniques: [esc_name], + dn: entry[:dn][0].to_s, + enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), + ca_servers: {}, manager_approval: ([entry[%s(mspki-enrollment-flag)].first.to_i].pack('l').unpack1('L') & Rex::Proto::MsCrtd::CT_FLAG_PEND_ALL_REQUESTS) != 0, required_signatures: [entry[%s(mspki-ra-signature)].first.to_i].pack('l').unpack1('L'), notes: notes.dup @@ -248,18 +251,14 @@ def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: []) def convert_sids_to_human_readable_name(sids_array) output = [] for sid in sids_array - raw_filter = "(objectSID=#{ldap_escape_filter(sid.to_s)})" - attributes = ['sAMAccountName', 'name'] - base_prefix = 'CN=Configuration' - sid_entry = query_ldap_server(raw_filter, attributes, base_prefix: base_prefix) # First try with prefix to find entries that may be group specific. - sid_entry = query_ldap_server(raw_filter, attributes) if sid_entry.empty? # Retry without prefix if blank. - if sid_entry.empty? + sid_entry = get_object_by_sid(sid) + if sid_entry.nil? print_warning("Could not find any details on the LDAP server for SID #{sid}!") output << [sid, nil, nil] # Still want to print out the SID even if we couldn't get additional information. - elsif sid_entry[0][:samaccountname][0] - output << [sid, sid_entry[0][:name][0], sid_entry[0][:samaccountname][0]] + elsif sid_entry[:samaccountname][0] + output << [sid, sid_entry[:name][0], sid_entry[:samaccountname][0]] else - output << [sid, sid_entry[0][:name][0], nil] + output << [sid, sid_entry[:name][0], nil] end end @@ -323,14 +322,14 @@ def find_esc3_vuln_cert_templates notes = [ 'ESC3: Template defines the Certificate Request Agent OID (PkiExtendedKeyUsage)' ] - query_ldap_server_certificates(esc3_template_1_raw_filter, 'ESC3_TEMPLATE_1', notes: notes) + query_ldap_server_certificates(esc3_template_1_raw_filter, 'ESC3', notes: notes) # Find the second vulnerable types of ESC3 templates, those that # have the right template schema version and, for those with a template # version of 2 or greater, have an Application Policy Insurance Requirement # requiring the Certificate Request Agent EKU. # - # Additionally the certificate template must also allow for domain authentication + # Additionally, the certificate template must also allow for domain authentication # and the CA must not have any enrollment agent restrictions. esc3_template_2_raw_filter = '(&'\ '(objectclass=pkicertificatetemplate)'\ @@ -521,11 +520,18 @@ def find_esc13_vuln_cert_templates note = "ESC13 groups: #{groups.join(', ')}" certificate_symbol = entry[:cn][0].to_sym - if @vuln_certificate_details.key?(certificate_symbol) - @vuln_certificate_details[certificate_symbol][:vulns] << 'ESC13' - @vuln_certificate_details[certificate_symbol][:notes] << note + if @certificate_details.key?(certificate_symbol) + @certificate_details[certificate_symbol][:techniques] << 'ESC13' + @certificate_details[certificate_symbol][:notes] << note else - @vuln_certificate_details[certificate_symbol] = { vulns: ['ESC13'], dn: entry[:dn][0], certificate_enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), ca_servers_n_enrollment_sids: {}, notes: [note] } + @certificate_details[certificate_symbol] = { + name: certificate_symbol.to_s, + techniques: ['ESC13'], + dn: entry[:dn][0].to_s, + enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), + ca_servers: {}, + notes: [note] + } end end end @@ -550,9 +556,9 @@ def find_enrollable_vuln_certificate_templates # allows users to enroll in that certificate template and which users/groups # have permissions to enroll in certificates on each server. - @vuln_certificate_details.each_key do |certificate_template| + @certificate_details.each_key do |certificate_template| certificate_enrollment_raw_filter = "(&(objectClass=pKIEnrollmentService)(certificateTemplates=#{ldap_escape_filter(certificate_template.to_s)}))" - attributes = ['cn', 'dnsHostname', 'ntsecuritydescriptor'] + attributes = ['cn', 'name', 'dnsHostname', 'ntsecuritydescriptor'] base_prefix = 'CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration' enrollment_ca_data = query_ldap_server(certificate_enrollment_raw_filter, attributes, base_prefix: base_prefix) next if enrollment_ca_data.empty? @@ -567,18 +573,43 @@ def find_enrollable_vuln_certificate_templates allowed_sids = parse_acl(security_descriptor.dacl) if security_descriptor.dacl next if allowed_sids.empty? + service = report_service({ + host: ca_server[:dnshostname][0], + port: 445, + proto: 'tcp', + name: 'AD CS', + info: "AD CS CA name: #{ca_server[:name][0]}" + }) + + report_note({ + data: ca_server[:dn][0].to_s, + service: service, + host: ca_server[:dnshostname][0], + ntype: 'windows.ad.cs.ca.dn' + }) + + report_host({ + host: ca_server[:dnshostname][0], + name: ca_server[:dnshostname][0] + }) + ca_server_key = ca_server[:dnshostname][0].to_sym - unless @vuln_certificate_details[certificate_template][:ca_servers_n_enrollment_sids].key?(ca_server_key) - @vuln_certificate_details[certificate_template][:ca_servers_n_enrollment_sids][ca_server_key] = { cn: ca_server[:cn][0], ca_enrollment_sids: allowed_sids } - end + next if @certificate_details[certificate_template][:ca_servers].key?(ca_server_key) + + @certificate_details[certificate_template][:ca_servers][ca_server_key] = { + hostname: ca_server[:dnshostname][0].to_s, + enrollment_sids: allowed_sids, + name: ca_server[:name][0].to_s, + dn: ca_server[:dn][0].to_s + } end end end def print_vulnerable_cert_info - vuln_certificate_details = @vuln_certificate_details.select do |_key, hash| + vuln_certificate_details = @certificate_details.select do |_key, hash| select = true - select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:certificate_enrollment_sids].any? do |sid| + select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:enrollment_sids].any? do |sid| # compare based on RIDs to avoid issues language specific issues !(sid.value.starts_with?("#{WellKnownSids::SECURITY_NT_NON_UNIQUE}-") && [ # RID checks @@ -593,36 +624,54 @@ def print_vulnerable_cert_info ].include?(sid.value) end - select = false unless datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers_n_enrollment_sids].any? + select = false unless datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers].any? select end any_esc3t1 = vuln_certificate_details.values.any? do |hash| - hash[:vulns].include?('ESC3_TEMPLATE_1') && (datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers_n_enrollment_sids].any?) + hash[:techniques].include?('ESC3') && (datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers].any?) end vuln_certificate_details.each do |key, hash| - vulns = hash[:vulns] - vulns.delete('ESC3_TEMPLATE_2') unless any_esc3t1 # don't report ESC3_TEMPLATE_2 if there are no instances of ESC3_TEMPLATE_1 - next if vulns.empty? + techniques = hash[:techniques].dup + techniques.delete('ESC3_TEMPLATE_2') unless any_esc3t1 # don't report ESC3_TEMPLATE_2 if there are no instances of ESC3 + next if techniques.empty? - vulns.each do |vuln| - vuln = 'ESC3' if vuln == 'ESC3_TEMPLATE_1' + techniques.each do |vuln| next if vuln == 'ESC3_TEMPLATE_2' prefix = "#{vuln}:" info = hash[:notes].select { |note| note.start_with?(prefix) }.map { |note| note.delete_prefix(prefix).strip }.join("\n") info = nil if info.blank? - report_vuln( - host: rhost, - port: rport, - proto: 'tcp', - sname: 'AD CS', - name: "#{vuln} - #{key}", - info: info, - refs: REFERENCES[vuln] - ) + hash[:ca_servers].each do |dnshostname, ca_server| + service = report_service({ + host: dnshostname.to_s, + port: 445, + proto: 'tcp', + name: 'AD CS', + info: "AD CS CA name: #{ca_server[:name]}" + }) + + vuln = report_vuln( + host: dnshostname.to_s, + port: 445, + proto: 'tcp', + sname: 'AD CS', + name: "#{vuln} - #{key}", + info: info, + refs: REFERENCES[vuln], + service: service + ) + + report_note({ + data: hash[:dn], + service: service, + host: dnshostname.to_s, + ntype: 'windows.ad.cs.ca.template.dn', + vuln_id: vuln.id + }) + end end print_good("Template: #{key}") @@ -630,7 +679,7 @@ def print_vulnerable_cert_info print_status(" Distinguished Name: #{hash[:dn]}") print_status(" Manager Approval: #{hash[:manager_approval] ? '%redRequired' : '%grnDisabled'}%clr") print_status(" Required Signatures: #{hash[:required_signatures] == 0 ? '%grn0' : '%red' + hash[:required_signatures].to_s}%clr") - print_good(" Vulnerable to: #{vulns.join(', ')}") + print_good(" Vulnerable to: #{techniques.join(', ')}") if hash[:notes].present? && hash[:notes].length == 1 print_status(" Notes: #{hash[:notes].first}") elsif hash[:notes].present? && hash[:notes].length > 1 @@ -648,15 +697,15 @@ def print_vulnerable_cert_info end print_status(' Certificate Template Enrollment SIDs:') - hash[:certificate_enrollment_sids].each do |sid| + hash[:enrollment_sids].each do |sid| print_status(" * #{highlight_sid(sid)}") end - if hash[:ca_servers_n_enrollment_sids].any? - hash[:ca_servers_n_enrollment_sids].each do |ca_hostname, ca_hash| - print_good(" Issuing CA: #{ca_hash[:cn]} (#{ca_hostname})") + if hash[:ca_servers].any? + hash[:ca_servers].each do |ca_hostname, ca_hash| + print_good(" Issuing CA: #{ca_hash[:name]} (#{ca_hostname})") print_status(' Enrollment SIDs:') - convert_sids_to_human_readable_name(ca_hash[:ca_enrollment_sids]).each do |sid| + convert_sids_to_human_readable_name(ca_hash[:enrollment_sids]).each do |sid| print_status(" * #{highlight_sid(sid)}") end end @@ -678,7 +727,7 @@ def highlight_sid(sid) end def get_pki_object_by_oid(oid) - pki_object = @ldap_mspki_enterprise_oids.find { |o| o['mspki-cert-template-oid'].first == oid } + pki_object = @ldap_objects.find { |o| o['mspki-cert-template-oid']&.first == oid } if pki_object.nil? pki_object = query_ldap_server( @@ -686,13 +735,13 @@ def get_pki_object_by_oid(oid) nil, base_prefix: 'CN=OID,CN=Public Key Services,CN=Services,CN=Configuration' )&.first - @ldap_mspki_enterprise_oids << pki_object if pki_object + @ldap_objects << pki_object if pki_object end pki_object end def get_group_by_dn(group_dn) - group = @ldap_groups.find { |o| o['dn'].first == group_dn } + group = @ldap_objects.find { |o| o['dn']&.first == group_dn } if group.nil? cn, _, base = group_dn.partition(',') @@ -702,18 +751,29 @@ def get_group_by_dn(group_dn) nil, base_prefix: base )&.first - @ldap_groups << group if group + @ldap_objects << group if group end group end + def get_object_by_sid(object_sid) + object_sid = Rex::Proto::MsDtyp::MsDtypSid.new(object_sid) + object = @ldap_objects.find { |o| o['objectSID'].first == object_sid.to_binary_s } + + if object.nil? + object = query_ldap_server("(objectSID=#{ldap_escape_filter(object_sid.to_s)})", nil)&.first + @ldap_objects << object if object + end + + object + end + def run # Define our instance variables real quick. @base_dn = nil - @ldap_mspki_enterprise_oids = [] - @ldap_groups = [] - @vuln_certificate_details = {} # Initialize to empty hash since we want to only keep one copy of each certificate template along with its details. + @ldap_objects = [] + @certificate_details = {} # Initialize to empty hash since we want to only keep one copy of each certificate template along with its details. ldap_connect do |ldap| validate_bind_success!(ldap) @@ -738,6 +798,7 @@ def run find_enrollable_vuln_certificate_templates print_vulnerable_cert_info + @certificate_details end rescue Errno::ECONNRESET fail_with(Failure::Disconnected, 'The connection was reset.') From f0f1aa9eb3a8d9c6faa1ce5d14616a0a856ff5dd Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 15 Jan 2025 18:00:18 +0000 Subject: [PATCH 02/11] Add initial MsDnsp data structures --- lib/rex/proto/ms_dnsp.rb | 102 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 lib/rex/proto/ms_dnsp.rb diff --git a/lib/rex/proto/ms_dnsp.rb b/lib/rex/proto/ms_dnsp.rb new file mode 100644 index 000000000000..7f11d3da3bbd --- /dev/null +++ b/lib/rex/proto/ms_dnsp.rb @@ -0,0 +1,102 @@ +# -*- coding: binary -*- +# frozen_string_literal: true + +require 'bindata' + +module Rex::Proto + module MsDnsp + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/39b03b89-2264-4063-8198-d62f62a6441a + class DnsRecordType + DNS_TYPE_ZERO = 0x0000 + DNS_TYPE_A = 0x0001 + DNS_TYPE_NS = 0x0002 + DNS_TYPE_MD = 0x0003 + DNS_TYPE_MF = 0x0004 + DNS_TYPE_CNAME = 0x0005 + DNS_TYPE_SOA = 0x0006 + DNS_TYPE_MB = 0x0007 + DNS_TYPE_MG = 0x0008 + DNS_TYPE_MR = 0x0009 + DNS_TYPE_NULL = 0x000A + DNS_TYPE_WKS = 0x000B + DNS_TYPE_PTR = 0x000C + DNS_TYPE_HINFO = 0x000D + DNS_TYPE_MINFO = 0x000E + DNS_TYPE_MX = 0x000F + DNS_TYPE_TXT = 0x0010 + DNS_TYPE_RP = 0x0011 + DNS_TYPE_AFSDB = 0x0012 + DNS_TYPE_X25 = 0x0013 + DNS_TYPE_ISDN = 0x0014 + DNS_TYPE_RT = 0x0015 + DNS_TYPE_SIG = 0x0018 + DNS_TYPE_KEY = 0x0019 + DNS_TYPE_AAAA = 0x001C + DNS_TYPE_LOC = 0x001D + DNS_TYPE_NXT = 0x001E + DNS_TYPE_SRV = 0x0021 + DNS_TYPE_ATMA = 0x0022 + DNS_TYPE_NAPTR = 0x0023 + DNS_TYPE_DNAME = 0x0027 + DNS_TYPE_DS = 0x002B + DNS_TYPE_RRSIG = 0x002E + DNS_TYPE_NSEC = 0x002F + DNS_TYPE_DNSKEY = 0x0030 + DNS_TYPE_DHCID = 0x0031 + DNS_TYPE_NSEC3 = 0x0032 + DNS_TYPE_NSEC3PARAM = 0x0033 + DNS_TYPE_TLSA = 0x0034 + DNS_TYPE_ALL = 0x00FF + DNS_TYPE_WINS = 0xFF01 + DNS_TYPE_WINSR = 0xFF02 + end + + class MsDnspAddr4 < BinData::Primitive + string :data, length: 4 + + def get + Rex::Socket.addr_ntoa(self.data) + end + + def set(v) + raise TypeError, 'must be an IPv4 address' unless Rex::Socket.is_ipv4?(v) + + self.data = Rex::Socket.addr_aton(v) + end + end + + class MsDnspAddr6 < BinData::Primitive + string :data, length: 16 + + def get + Rex::Socket.addr_ntoa(self.data) + end + + def set(v) + raise TypeError, 'must be an IPv6 address' unless Rex::Socket.is_ipv6?(v) + + self.data = Rex::Socket.addr_aton(v) + end + end + + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/6912b338-5472-4f59-b912-0edb536b6ed8 + class MsDnspDnsRecord < BinData::Record + endian :little + + uint16 :data_length, initial_value: -> { data.length } + uint16 :record_type + uint8 :version + uint8 :rank + uint16 :flags + uint32 :serial + uint32be :ttl_seconds + uint32 :reserved + uint32 :timestamp + choice :data, selection: :record_type do + ms_dnsp_addr4 DnsRecordType::DNS_TYPE_A + ms_dnsp_addr6 DnsRecordType::DNS_TYPE_AAAA + string :default, read_length: :data_length + end + end + end +end From 1aa4a1f8c874ff015e004c4375ec90c548cad49b Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 16 Jan 2025 11:37:11 +0000 Subject: [PATCH 03/11] Resolve the CA address via DNS records in LDAP --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 101 +++++++++++++----- 1 file changed, 73 insertions(+), 28 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index eb2c5c8398b0..ea1bb510ac56 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -3,6 +3,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report include Msf::Exploit::Remote::LDAP include Msf::OptionalSession::LDAP + include Rex::Proto::MsDnsp include Rex::Proto::Secauthz include Rex::Proto::LDAP @@ -573,31 +574,37 @@ def find_enrollable_vuln_certificate_templates allowed_sids = parse_acl(security_descriptor.dacl) if security_descriptor.dacl next if allowed_sids.empty? - service = report_service({ - host: ca_server[:dnshostname][0], - port: 445, - proto: 'tcp', - name: 'AD CS', - info: "AD CS CA name: #{ca_server[:name][0]}" - }) - - report_note({ - data: ca_server[:dn][0].to_s, - service: service, - host: ca_server[:dnshostname][0], - ntype: 'windows.ad.cs.ca.dn' - }) - - report_host({ - host: ca_server[:dnshostname][0], - name: ca_server[:dnshostname][0] - }) - - ca_server_key = ca_server[:dnshostname][0].to_sym + ca_server_fqdn = ca_server[:dnshostname][0].to_s.downcase + ca_server_ip_address = get_ip_addresses_by_fqdn(ca_server_fqdn)&.first + + if ca_server_ip_address + service = report_service({ + host: ca_server_ip_address, + port: 445, + proto: 'tcp', + name: 'AD CS', + info: "AD CS CA name: #{ca_server[:name][0]}" + }) + + report_note({ + data: ca_server[:dn][0].to_s, + service: service, + host: ca_server_ip_address, + ntype: 'windows.ad.cs.ca.dn' + }) + + report_host({ + host: ca_server_ip_address, + name: ca_server_fqdn + }) + end + + ca_server_key = ca_server_fqdn.to_sym next if @certificate_details[certificate_template][:ca_servers].key?(ca_server_key) @certificate_details[certificate_template][:ca_servers][ca_server_key] = { - hostname: ca_server[:dnshostname][0].to_s, + fqdn: ca_server_fqdn, + ip_address: ca_server_ip_address, enrollment_sids: allowed_sids, name: ca_server[:name][0].to_s, dn: ca_server[:dn][0].to_s @@ -644,9 +651,9 @@ def print_vulnerable_cert_info info = hash[:notes].select { |note| note.start_with?(prefix) }.map { |note| note.delete_prefix(prefix).strip }.join("\n") info = nil if info.blank? - hash[:ca_servers].each do |dnshostname, ca_server| + hash[:ca_servers].each do |ca_fqdn, ca_server| service = report_service({ - host: dnshostname.to_s, + host: ca_server[:ip_address], port: 445, proto: 'tcp', name: 'AD CS', @@ -654,7 +661,7 @@ def print_vulnerable_cert_info }) vuln = report_vuln( - host: dnshostname.to_s, + host: ca_server[:ip_address], port: 445, proto: 'tcp', sname: 'AD CS', @@ -667,7 +674,7 @@ def print_vulnerable_cert_info report_note({ data: hash[:dn], service: service, - host: dnshostname.to_s, + host: ca_fqdn.to_s, ntype: 'windows.ad.cs.ca.template.dn', vuln_id: vuln.id }) @@ -702,8 +709,8 @@ def print_vulnerable_cert_info end if hash[:ca_servers].any? - hash[:ca_servers].each do |ca_hostname, ca_hash| - print_good(" Issuing CA: #{ca_hash[:name]} (#{ca_hostname})") + hash[:ca_servers].each do |ca_fqdn, ca_hash| + print_good(" Issuing CA: #{ca_hash[:name]} (#{ca_fqdn})") print_status(' Enrollment SIDs:') convert_sids_to_human_readable_name(ca_hash[:enrollment_sids]).each do |sid| print_status(" * #{highlight_sid(sid)}") @@ -769,10 +776,48 @@ def get_object_by_sid(object_sid) object end + def get_ip_addresses_by_fqdn(host_fqdn) + return @fqdns[host_fqdn] if @fqdns.key?(host_fqdn) + + vprint_status("Looking up DNS records for #{host_fqdn} in LDAP.") + hostname, _, domain = host_fqdn.partition('.') + results = query_ldap_server( + "(&(objectClass=dnsNode)(DC=#{ldap_escape_filter(hostname)}))", + %w[dnsRecord], + base_prefix: "DC=#{ldap_escape_filter(domain)},CN=MicrosoftDNS,DC=DomainDnsZones" + ) + return nil if results.blank? + + ip_addresses = [] + results.first[:dnsrecord].each do |packed| + begin + unpacked = MsDnspDnsRecord.read(packed) + rescue ::EOFError + next + rescue ::IOError + next + end + + next unless [ DnsRecordType::DNS_TYPE_A, DnsRecordType::DNS_TYPE_AAAA ].include?(unpacked.record_type) + + ip_addresses << unpacked.data.to_s + end + + @fqdns[host_fqdn] = ip_addresses + if ip_addresses.empty? + print_warning("No A or AAAA DNS records were found for #{host_fqdn} in LDAP.") + else + vprint_status("Found #{ip_addresses.length} IP address#{ip_addresses.length > 1 ? 'es' : ''} via A and AAAA DNS records.") + end + + ip_addresses + end + def run # Define our instance variables real quick. @base_dn = nil @ldap_objects = [] + @fqdns = {} @certificate_details = {} # Initialize to empty hash since we want to only keep one copy of each certificate template along with its details. ldap_connect do |ldap| From 7b03844312dbf3060aa4abbba7052eba55790566 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 16 Jan 2025 15:21:46 +0000 Subject: [PATCH 04/11] Consolidate the report details --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index ea1bb510ac56..3d192b702af9 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -208,13 +208,13 @@ def query_ldap_server(raw_filter, attributes, base_prefix: nil) returned_entries end - def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: []) + def query_ldap_server_certificates(esc_raw_filter, esc_id, notes: []) attributes = ['cn', 'name', 'description', 'ntSecurityDescriptor', 'msPKI-Enrollment-Flag', 'msPKI-RA-Signature', 'PkiExtendedKeyUsage'] base_prefix = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration' esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: base_prefix) if esc_entries.empty? - print_warning("Couldn't find any vulnerable #{esc_name} templates!") + print_warning("Couldn't find any vulnerable #{esc_id} templates!") return end @@ -232,19 +232,15 @@ def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: []) certificate_symbol = entry[:cn][0].to_sym if @certificate_details.key?(certificate_symbol) - @certificate_details[certificate_symbol][:techniques] << esc_name + @certificate_details[certificate_symbol][:techniques] << esc_id @certificate_details[certificate_symbol][:notes] += notes else - @certificate_details[certificate_symbol] = { - name: entry[:name][0].to_s, - techniques: [esc_name], - dn: entry[:dn][0].to_s, - enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), - ca_servers: {}, - manager_approval: ([entry[%s(mspki-enrollment-flag)].first.to_i].pack('l').unpack1('L') & Rex::Proto::MsCrtd::CT_FLAG_PEND_ALL_REQUESTS) != 0, - required_signatures: [entry[%s(mspki-ra-signature)].first.to_i].pack('l').unpack1('L'), + @certificate_details[certificate_symbol] = build_certificate_details( + entry, + allowed_sids, + techniques: [esc_id], notes: notes.dup - } + ) end end end @@ -480,7 +476,7 @@ def find_esc13_vuln_cert_templates (mspki-certificate-policy=*) ) FILTER - attributes = ['cn', 'description', 'ntSecurityDescriptor', 'msPKI-Certificate-Policy'] + attributes = ['cn', 'description', 'ntSecurityDescriptor', 'msPKI-Certificate-Policy', 'msPKI-Enrollment-Flag', 'msPKI-RA-Signature'] base_prefix = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration' esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: base_prefix) @@ -525,18 +521,24 @@ def find_esc13_vuln_cert_templates @certificate_details[certificate_symbol][:techniques] << 'ESC13' @certificate_details[certificate_symbol][:notes] << note else - @certificate_details[certificate_symbol] = { - name: certificate_symbol.to_s, - techniques: ['ESC13'], - dn: entry[:dn][0].to_s, - enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), - ca_servers: {}, - notes: [note] - } + @certificate_details[certificate_symbol] = build_certificate_details(entry, allowed_sids, techniques: %w[ESC13], notes: [note]) end end end + def build_certificate_details(ldap_object, allowed_sids, techniques: [], notes: []) + { + name: ldap_object[:cn][0].to_s, + techniques: techniques, + dn: ldap_object[:dn][0].to_s, + enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), + ca_servers: {}, + manager_approval: ([ldap_object[%s(mspki-enrollment-flag)].first.to_i].pack('l').unpack1('L') & Rex::Proto::MsCrtd::CT_FLAG_PEND_ALL_REQUESTS) != 0, + required_signatures: [ldap_object[%s(mspki-ra-signature)].first.to_i].pack('l').unpack1('L'), + notes: notes + } + end + def find_esc15_vuln_cert_templates esc_raw_filter = '(&'\ '(objectclass=pkicertificatetemplate)'\ @@ -697,7 +699,7 @@ def print_vulnerable_cert_info end if hash[:certificate_write_priv_sids] - print_status(' Users or Groups SIDs with Certificate Template write access:') + print_status(' Certificate Template Write-Enabled SIDs:') hash[:certificate_write_priv_sids].each do |sid| print_status(" * #{highlight_sid(sid)}") end @@ -744,6 +746,7 @@ def get_pki_object_by_oid(oid) )&.first @ldap_objects << pki_object if pki_object end + pki_object end From e07246804274b8441b8b68cdf1dd10ea9a20caea Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 24 Jan 2025 17:03:09 -0500 Subject: [PATCH 05/11] Some adjustments for ESC4 compatibility with MSP --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index 3d192b702af9..da7b7016ee4f 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -50,9 +50,9 @@ def initialize(info = {}) allows enrollment in and which SIDs are authorized to use that certificate server to perform this enrollment operation. - Currently the module is capable of checking for certificates that are vulnerable to ESC1, ESC2, ESC3, ESC13, - and ESC15. The module is limited to checking for these techniques due to them being identifiable remotely from - a normal user account by analyzing the objects in LDAP. + Currently the module is capable of checking for certificates that are vulnerable to ESC1, ESC2, ESC3, ESC4, + ESC13, and ESC15. The module is limited to checking for these techniques due to them being identifiable + remotely from a normal user account by analyzing the objects in LDAP. }, 'Author' => [ 'Grant Willcox', # Original module author @@ -247,7 +247,7 @@ def query_ldap_server_certificates(esc_raw_filter, esc_id, notes: []) def convert_sids_to_human_readable_name(sids_array) output = [] - for sid in sids_array + sids_array.each do |sid| sid_entry = get_object_by_sid(sid) if sid_entry.nil? print_warning("Could not find any details on the LDAP server for SID #{sid}!") @@ -392,8 +392,9 @@ def find_esc4_vuln_cert_templates # https://learn.microsoft.com/en-us/windows/win32/adsi/search-filter-syntax?redirectedfrom=MSDN filter_with_user = "(|(member:1.2.840.113556.1.4.1941:=#{our_account[:dn].first})" user_groups.each do |sid| - obj = query_ldap_server("(objectSid=#{sid})", ['dn'])&.first + obj = get_object_by_sid(sid) print_error('Failed to lookup SID.') unless obj + filter_with_user << "(member:1.2.840.113556.1.4.1941:=#{obj[:dn].first})" if obj end filter_with_user << ')' @@ -442,28 +443,28 @@ def find_esc4_vuln_cert_templates # SIDs that can edit the template that the user we've authenticated with are also a part of user_write_priv_sids = [] - note = [] + notes = [] # Main reason for splitting user_can_edit and group_can_edit is so "note" can be more descriptive if user_can_edit user_write_priv_sids << user_can_edit - note << "ESC4: The account: #{sam_account_name} has edit permissions over the template #{certificate_symbol} making it vulnerable to ESC4" + notes << "ESC4: The account: #{sam_account_name} has edit permissions over the template #{certificate_symbol} making it vulnerable to ESC4" end if group_can_edit.any? user_write_priv_sids.concat(group_can_edit.map(&:to_s)) - note << "ESC4: The account: #{sam_account_name} is a part of the following groups: (#{convert_sids_to_human_readable_name(group_can_edit).map(&:name).join(', ')}) which have edit permissions over the template #{certificate_symbol} making it vulnerable to ESC4" + notes << "ESC4: The account: #{sam_account_name} is a part of the following groups: (#{convert_sids_to_human_readable_name(group_can_edit).map(&:name).join(', ')}) which have edit permissions over the template object" end next unless user_write_priv_sids.any? - if @vuln_certificate_details.key?(certificate_symbol) - @vuln_certificate_details[certificate_symbol][:vulns] << 'ESC4' - @vuln_certificate_details[certificate_symbol][:notes].concat(note) - @vuln_certificate_details[certificate_symbol][:certificate_write_priv_sids] ||= convert_sids_to_human_readable_name(user_write_priv_sids) + if @certificate_details.key?(certificate_symbol) + @certificate_details[certificate_symbol][:techniques] << 'ESC4' + @certificate_details[certificate_symbol][:notes].concat(notes) else - @vuln_certificate_details[certificate_symbol] = { vulns: ['ESC4'], dn: entry[:dn][0], certificate_enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), ca_servers_n_enrollment_sids: {}, certificate_write_priv_sids: convert_sids_to_human_readable_name(user_write_priv_sids), notes: note } + @certificate_details[certificate_symbol] = build_certificate_details(entry, allowed_sids, techniques: %w[ESC4], notes: notes) end + @certificate_details[certificate_symbol][:write_enabled_sids] ||= convert_sids_to_human_readable_name(user_write_priv_sids) end end @@ -616,7 +617,7 @@ def find_enrollable_vuln_certificate_templates end def print_vulnerable_cert_info - vuln_certificate_details = @certificate_details.select do |_key, hash| + vuln_certificate_details = @certificate_details.sort.to_h.select do |_key, hash| select = true select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:enrollment_sids].any? do |sid| # compare based on RIDs to avoid issues language specific issues @@ -698,9 +699,9 @@ def print_vulnerable_cert_info end end - if hash[:certificate_write_priv_sids] + if hash[:write_enabled_sids] print_status(' Certificate Template Write-Enabled SIDs:') - hash[:certificate_write_priv_sids].each do |sid| + hash[:write_enabled_sids].each do |sid| print_status(" * #{highlight_sid(sid)}") end end From 210b780f8388d806386e1459f19c038f0368940c Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 28 Jan 2025 11:57:24 -0500 Subject: [PATCH 06/11] Refactor reporting template permissions --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 236 ++++++++---------- 1 file changed, 103 insertions(+), 133 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index da7b7016ee4f..c92467b3d289 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -23,8 +23,12 @@ class MetasploitModule < Msf::Auxiliary }.freeze SID = Struct.new(:value, :name) do + def ==(other) + value == other.value + end + def to_s - name.present? ? "#{value} (#{name})" : value + name.present? ? "#{value} (#{name})" : value.to_s end def rid @@ -86,6 +90,8 @@ def initialize(info = {}) end # Constants Definition + CERTIFICATE_ATTRIBUTES = %w[cn name description nTSecurityDescriptor msPKI-Certificate-Policy msPKI-Enrollment-Flag msPKI-RA-Signature msPKI-Template-Schema-Version pkiExtendedKeyUsage] + CERTIFICATE_TEMPLATES_BASE = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration'.freeze CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT = '0e10c968-78fb-11d2-90d4-00c04f79dc55'.freeze CERTIFICATE_AUTOENROLLMENT_EXTENDED_RIGHT = 'a05b8cc2-17bc-4802-a710-e7c15ab866a2'.freeze CONTROL_ACCESS = 0x00000100 @@ -97,63 +103,32 @@ def initialize(info = {}) DACL_SECURITY_INFORMATION = 0x4 SACL_SECURITY_INFORMATION = 0x8 - # This will return a list of SIDs that can edit the template from which the ACL is derived - # The method checks the WriteOwner, WriteDacl and GenericWrite bits of the access_mask to see if the user or group has write permissions over the Certificate - def parse_acl_for_esc4(acl) - allowed_sids = [] - + # This returns a list of SIDs that have the CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT or CERTIFICATE_AUTOENROLLMENT_EXTENDED_RIGHT for the given ACL + def enum_acl_aces(acl) acl.aces.each do |ace| - ace_header = ace[:header] - ace_body = ace[:body] - - if ace_body[:access_mask].blank? + if ace[:body][:access_mask].blank? fail_with(Failure::UnexpectedReply, 'Encountered a DACL/SACL object without an access mask! Either data is an unrecognized type or we are reading it wrong!') end - ace_type_name = Rex::Proto::MsDtyp::MsDtypAceType.name(ace_header[:ace_type]) - + ace_type_name = Rex::Proto::MsDtyp::MsDtypAceType.name(ace[:header][:ace_type]) if ace_type_name.blank? - print_error("Skipping unexpected ACE of type #{ace_header[:ace_type]}. Either the data was read incorrectly or we currently don't support this type.") + print_error("Skipping unexpected ACE of type #{ace[:header][:ace_type]}. Either the data was read incorrectly or we currently don't support this type.") next end - - if ace_header[:ace_flags][:inherit_only_ace] == 1 - print_warning(' ACE only affects those that inherit from it, not those that it is attached to. Ignoring this ACE, as its not relevant.') + if ace[:header][:ace_flags][:inherit_only_ace] == 1 + # ACE only affects those that inherit from it, not those that it is attached to. Ignoring this ACE, as its not relevant. next end - # Look at WriteOwner, WriteDacl and GenericWrite to see if the user has write permissions over the Certificate - if !(ace_body[:access_mask][:wo] == 1 || ace_body[:access_mask][:wd] == 1 || ace_body[:access_mask][:gw] == 1) - - next - end - - allowed_sids << ace_body[:sid].to_s + yield ace_type_name, ace end - allowed_sids end - # This returns a list of SIDs that have the CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT or CERTIFICATE_AUTOENROLLMENT_EXTENDED_RIGHT for the given ACL - def parse_acl(acl) + def get_sids_for_enroll(acl) allowed_sids = [] - acl.aces.each do |ace| - ace_header = ace[:header] - ace_body = ace[:body] - if ace_body[:access_mask].blank? - fail_with(Failure::UnexpectedReply, 'Encountered a DACL/SACL object without an access mask! Either data is an unrecognized type or we are reading it wrong!') - end - ace_type_name = Rex::Proto::MsDtyp::MsDtypAceType.name(ace_header[:ace_type]) - if ace_type_name.blank? - print_error("Skipping unexpected ACE of type #{ace_header[:ace_type]}. Either the data was read incorrectly or we currently don't support this type.") - next - end - if ace_header[:ace_flags][:inherit_only_ace] == 1 - vprint_warning(' ACE only affects those that inherit from it, not those that it is attached to. Ignoring this ACE, as its not relevant.') - next - end - + enum_acl_aces(acl) do |ace_type_name, ace| # To decode the ObjectType we need to do another query to CN=Configuration,DC=daforest,DC=com # and look at either schemaIDGUID or rightsGUID fields to see if they match this value. - if (object_type = ace_body[:object_type]) && !(object_type == CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT || object_type == CERTIFICATE_AUTOENROLLMENT_EXTENDED_RIGHT) + if (object_type = ace[:body][:object_type]) && !(object_type == CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT || object_type == CERTIFICATE_AUTOENROLLMENT_EXTENDED_RIGHT) # If an object type was specified, only process the rest if it is one of these two (note that objects with no # object types will be processed to make sure we can detect vulnerable templates post exploiting ESC4). next @@ -162,14 +137,31 @@ def parse_acl(acl) # Skip entry if it is not related to an extended access control right, where extended access control right is # described as ADS_RIGHT_DS_CONTROL_ACCESS in the ObjectType field of ACCESS_ALLOWED_OBJECT_ACE. This is # detailed further at https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-access_allowed_object_ace - next unless (ace_body.access_mask.protocol & CONTROL_ACCESS) == CONTROL_ACCESS + next unless (ace[:body].access_mask.protocol & CONTROL_ACCESS) == CONTROL_ACCESS if ace_type_name.match(/ALLOWED/) - allowed_sids << ace_body[:sid].to_s + allowed_sids << ace[:body][:sid] end end - allowed_sids + map_sids_to_names(allowed_sids) + end + + # This will return a list of SIDs that can edit the template from which the ACL is derived + # The method checks the WriteOwner, WriteDacl and GenericWrite bits of the access_mask to see if the user or group has write permissions over the Certificate + def get_sids_for_write(acl) + allowed_sids = [] + + enum_acl_aces(acl) do |_ace_type_name, ace| + # Look at WriteOwner, WriteDacl and GenericWrite to see if the user has write permissions over the Certificate + if !(ace[:body][:access_mask][:wo] == 1 || ace[:body][:access_mask][:wd] == 1 || ace[:body][:access_mask][:gw] == 1) + next + end + + allowed_sids << ace[:body][:sid] + end + + map_sids_to_names(allowed_sids) end def query_ldap_server(raw_filter, attributes, base_prefix: nil) @@ -209,9 +201,7 @@ def query_ldap_server(raw_filter, attributes, base_prefix: nil) end def query_ldap_server_certificates(esc_raw_filter, esc_id, notes: []) - attributes = ['cn', 'name', 'description', 'ntSecurityDescriptor', 'msPKI-Enrollment-Flag', 'msPKI-RA-Signature', 'PkiExtendedKeyUsage'] - base_prefix = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration' - esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: base_prefix) + esc_entries = query_ldap_server(esc_raw_filter, CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE) if esc_entries.empty? print_warning("Couldn't find any vulnerable #{esc_id} templates!") @@ -221,50 +211,35 @@ def query_ldap_server_certificates(esc_raw_filter, esc_id, notes: []) # Grab a list of certificates that contain vulnerable settings. # Also print out the list of SIDs that can enroll in that server. esc_entries.each do |entry| - begin - security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(entry[:ntsecuritydescriptor][0]) - rescue IOError => e - fail_with(Failure::UnexpectedReply, "Unable to read security descriptor! Error was: #{e.message}") - end - - allowed_sids = parse_acl(security_descriptor.dacl) if security_descriptor.dacl - next if allowed_sids.empty? - certificate_symbol = entry[:cn][0].to_sym - if @certificate_details.key?(certificate_symbol) - @certificate_details[certificate_symbol][:techniques] << esc_id - @certificate_details[certificate_symbol][:notes] += notes - else - @certificate_details[certificate_symbol] = build_certificate_details( - entry, - allowed_sids, - techniques: [esc_id], - notes: notes.dup - ) - end + next if @certificate_details[certificate_symbol][:enroll_sids].empty? + + @certificate_details[certificate_symbol][:techniques] << esc_id + @certificate_details[certificate_symbol][:notes] += notes end end - def convert_sids_to_human_readable_name(sids_array) - output = [] + def map_sids_to_names(sids_array) + mapped = [] sids_array.each do |sid| + # this common SID doesn't always have an entry + if sid == Rex::Proto::Secauthz::WellKnownSids::SECURITY_AUTHENTICATED_USER_SID + mapped << SID.new(sid, 'Authenticated Users') + next + end + sid_entry = get_object_by_sid(sid) if sid_entry.nil? print_warning("Could not find any details on the LDAP server for SID #{sid}!") - output << [sid, nil, nil] # Still want to print out the SID even if we couldn't get additional information. - elsif sid_entry[:samaccountname][0] - output << [sid, sid_entry[:name][0], sid_entry[:samaccountname][0]] - else - output << [sid, sid_entry[:name][0], nil] + mapped << SID.new(sid, name) + elsif sid_entry[:samaccountname].present? + mapped << SID.new(sid, sid_entry[:samaccountname].first.to_s) + elsif sid_entry[:name].present? + mapped << SID.new(sid, sid_entry[:name].first.to_s) end end - results = [] - output.each do |sid_string, sid_name, sam_account_name| - results << SID.new(sid_string, sam_account_name || sid_name) - end - - results + mapped end def find_esc1_vuln_cert_templates @@ -373,8 +348,8 @@ def find_esc4_vuln_cert_templates return end - user_sid = Rex::Proto::MsDtyp::MsDtypSid.read(our_account[:objectsid].first).value - domain_sid = user_sid.rpartition('-').first + user_sid = map_sids_to_names([Rex::Proto::MsDtyp::MsDtypSid.read(our_account[:objectsid].first).value]).first + domain_sid = user_sid.value.to_s.rpartition('-').first user_groups = [] if our_account[:primarygroupID] @@ -406,33 +381,23 @@ def find_esc4_vuln_cert_templates group_sid = Rex::Proto::MsDtyp::MsDtypSid.read(entry['ObjectSid'].first).value user_groups << group_sid end + user_groups = map_sids_to_names(user_groups) # Determine what Certificate Templates are available to us esc_raw_filter = '(objectclass=pkicertificatetemplate)' attributes = ['cn', 'description', 'ntSecurityDescriptor'] - base_prefix = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration' - esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: base_prefix) + esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: CERTIFICATE_TEMPLATES_BASE) return if esc_entries.empty? # Determine if the user we've authenticated with has the ability to edit esc_entries.each do |entry| certificate_symbol = entry[:cn][0].to_sym - - begin - security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(entry[:ntsecuritydescriptor][0]) - rescue IOError => e - print_warning("Unable to read security descriptor for #{certificate_symbol}, skipping. Error was: #{e.message}") - next - end - - # SIDS that can enroll in the template - allowed_sids = parse_acl(security_descriptor.dacl) if security_descriptor.dacl - next if allowed_sids.empty? + next if @certificate_details[certificate_symbol][:enroll_sids].empty? # SIDs that can edit the template - write_priv_sids = parse_acl_for_esc4(security_descriptor.dacl) if security_descriptor.dacl + write_priv_sids = @certificate_details[certificate_symbol][:write_sids] next if write_priv_sids.empty? # Check if the user has been give access to edit the template @@ -452,19 +417,14 @@ def find_esc4_vuln_cert_templates end if group_can_edit.any? - user_write_priv_sids.concat(group_can_edit.map(&:to_s)) - notes << "ESC4: The account: #{sam_account_name} is a part of the following groups: (#{convert_sids_to_human_readable_name(group_can_edit).map(&:name).join(', ')}) which have edit permissions over the template object" + user_write_priv_sids.concat(group_can_edit) + notes << "ESC4: The account: #{sam_account_name} is a part of the following groups: (#{group_can_edit.map(&:name).join(', ')}) which have edit permissions over the template object" end next unless user_write_priv_sids.any? - if @certificate_details.key?(certificate_symbol) - @certificate_details[certificate_symbol][:techniques] << 'ESC4' - @certificate_details[certificate_symbol][:notes].concat(notes) - else - @certificate_details[certificate_symbol] = build_certificate_details(entry, allowed_sids, techniques: %w[ESC4], notes: notes) - end - @certificate_details[certificate_symbol][:write_enabled_sids] ||= convert_sids_to_human_readable_name(user_write_priv_sids) + @certificate_details[certificate_symbol][:techniques] << 'ESC4' + @certificate_details[certificate_symbol][:notes].concat(notes) end end @@ -477,9 +437,7 @@ def find_esc13_vuln_cert_templates (mspki-certificate-policy=*) ) FILTER - attributes = ['cn', 'description', 'ntSecurityDescriptor', 'msPKI-Certificate-Policy', 'msPKI-Enrollment-Flag', 'msPKI-RA-Signature'] - base_prefix = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration' - esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: base_prefix) + esc_entries = query_ldap_server(esc_raw_filter, CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE) if esc_entries.empty? print_warning("Couldn't find any vulnerable ESC13 templates!") @@ -489,14 +447,8 @@ def find_esc13_vuln_cert_templates # Grab a list of certificates that contain vulnerable settings. # Also print out the list of SIDs that can enroll in that server. esc_entries.each do |entry| - begin - security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(entry[:ntsecuritydescriptor][0]) - rescue IOError => e - fail_with(Failure::UnexpectedReply, "Unable to read security descriptor! Error was: #{e.message}") - end - - allowed_sids = parse_acl(security_descriptor.dacl) if security_descriptor.dacl - next if allowed_sids.empty? + certificate_symbol = entry[:cn][0].to_sym + next if @certificate_details[certificate_symbol][:enroll_sids].empty? groups = [] entry['mspki-certificate-policy'].each do |certificate_policy_oid| @@ -516,23 +468,32 @@ def find_esc13_vuln_cert_templates end next if groups.empty? - note = "ESC13 groups: #{groups.join(', ')}" certificate_symbol = entry[:cn][0].to_sym - if @certificate_details.key?(certificate_symbol) - @certificate_details[certificate_symbol][:techniques] << 'ESC13' - @certificate_details[certificate_symbol][:notes] << note - else - @certificate_details[certificate_symbol] = build_certificate_details(entry, allowed_sids, techniques: %w[ESC13], notes: [note]) - end + @certificate_details[certificate_symbol][:techniques] << 'ESC13' + @certificate_details[certificate_symbol][:notes] << "ESC13 groups: #{groups.join(', ')}" end end - def build_certificate_details(ldap_object, allowed_sids, techniques: [], notes: []) + def build_certificate_details(ldap_object, techniques: [], notes: []) + security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(ldap_object[:ntsecuritydescriptor].first) + + if security_descriptor.dacl + enroll_sids = get_sids_for_enroll(security_descriptor.dacl) + write_sids = get_sids_for_write(security_descriptor.dacl) + else + enroll_sids = nil + write_sids = nil + end + { name: ldap_object[:cn][0].to_s, techniques: techniques, dn: ldap_object[:dn][0].to_s, - enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), + enroll_sids: enroll_sids, + write_sids: write_sids, + security_descriptor: security_descriptor, + ekus: ldap_object[:pkiextendedkeyusage].map(&:to_s), + schema_version: ldap_object[%s(mspki-template-schema-version)].first, ca_servers: {}, manager_approval: ([ldap_object[%s(mspki-enrollment-flag)].first.to_i].pack('l').unpack1('L') & Rex::Proto::MsCrtd::CT_FLAG_PEND_ALL_REQUESTS) != 0, required_signatures: [ldap_object[%s(mspki-ra-signature)].first.to_i].pack('l').unpack1('L'), @@ -574,8 +535,8 @@ def find_enrollable_vuln_certificate_templates fail_with(Failure::UnexpectedReply, "Unable to read security descriptor! Error was: #{e.message}") end - allowed_sids = parse_acl(security_descriptor.dacl) if security_descriptor.dacl - next if allowed_sids.empty? + enroll_sids = get_sids_for_enroll(security_descriptor.dacl) if security_descriptor.dacl + next if enroll_sids.empty? ca_server_fqdn = ca_server[:dnshostname][0].to_s.downcase ca_server_ip_address = get_ip_addresses_by_fqdn(ca_server_fqdn)&.first @@ -608,7 +569,7 @@ def find_enrollable_vuln_certificate_templates @certificate_details[certificate_template][:ca_servers][ca_server_key] = { fqdn: ca_server_fqdn, ip_address: ca_server_ip_address, - enrollment_sids: allowed_sids, + enroll_sids: enroll_sids, name: ca_server[:name][0].to_s, dn: ca_server[:dn][0].to_s } @@ -619,7 +580,7 @@ def find_enrollable_vuln_certificate_templates def print_vulnerable_cert_info vuln_certificate_details = @certificate_details.sort.to_h.select do |_key, hash| select = true - select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:enrollment_sids].any? do |sid| + select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:enroll_sids].any? do |sid| # compare based on RIDs to avoid issues language specific issues !(sid.value.starts_with?("#{WellKnownSids::SECURITY_NT_NON_UNIQUE}-") && [ # RID checks @@ -707,7 +668,7 @@ def print_vulnerable_cert_info end print_status(' Certificate Template Enrollment SIDs:') - hash[:enrollment_sids].each do |sid| + hash[:enroll_sids].each do |sid| print_status(" * #{highlight_sid(sid)}") end @@ -715,7 +676,7 @@ def print_vulnerable_cert_info hash[:ca_servers].each do |ca_fqdn, ca_hash| print_good(" Issuing CA: #{ca_hash[:name]} (#{ca_fqdn})") print_status(' Enrollment SIDs:') - convert_sids_to_human_readable_name(ca_hash[:enrollment_sids]).each do |sid| + ca_hash[:enroll_sids].each do |sid| print_status(" * #{highlight_sid(sid)}") end end @@ -838,6 +799,14 @@ def run end @ldap = ldap + 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? + + templates.each do |template| + certificate_symbol = template[:cn].first.to_sym + @certificate_details[certificate_symbol] = build_certificate_details(template) + end + find_esc1_vuln_cert_templates find_esc2_vuln_cert_templates find_esc3_vuln_cert_templates @@ -847,6 +816,7 @@ def run find_enrollable_vuln_certificate_templates print_vulnerable_cert_info + @certificate_details end rescue Errno::ECONNRESET From 441b671edd1531ceb457017fa7600aeee42072f7 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 28 Jan 2025 17:20:10 -0500 Subject: [PATCH 07/11] Update to include return values --- .../admin/ldap/ad_cs_cert_template.rb | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb b/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb index 83813a1ffb57..940ff63afc19 100644 --- a/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb +++ b/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb @@ -116,8 +116,9 @@ def run end @ldap = ldap - send("action_#{action.name.downcase}") + result = send("action_#{action.name.downcase}") print_good('The operation completed successfully!') + result end rescue Errno::ECONNRESET fail_with(Failure::Disconnected, 'The connection was reset.') @@ -147,7 +148,7 @@ def get_certificate_template "#{datastore['CERT_TEMPLATE']} Certificate Template" ) print_status("Certificate template data written to: #{stored}") - obj + [obj, stored] end def get_domain_sid @@ -323,17 +324,19 @@ def action_create print_status("Creating: #{dn}") @ldap.add(dn: dn, attributes: attributes) validate_query_result!(@ldap.get_operation_result.table) + dn end def action_delete - obj = get_certificate_template + obj, = get_certificate_template @ldap.delete(dn: obj['dn'].first) validate_query_result!(@ldap.get_operation_result.table) + true end def action_read - obj = get_certificate_template + obj, stored = get_certificate_template print_status('Certificate Template:') print_status(" distinguishedName: #{obj['distinguishedname'].first}") @@ -477,10 +480,12 @@ def action_read if obj['pkimaxissuingdepth'].present? print_status(" pKIMaxIssuingDepth: #{obj['pkimaxissuingdepth'].first.to_i}") end + + { object: obj, file: stored } end def action_update - obj = get_certificate_template + obj, = get_certificate_template new_configuration = load_local_template operations = [] @@ -492,6 +497,8 @@ def action_update unless value.tally == new_value.tally operations << [:replace, attribute, new_value] end + elsif attribute == 'ntsecuritydescriptor' + # the security descriptor can't be deleted so leave it alone unless specified else operations << [:delete, attribute, nil] end @@ -506,10 +513,11 @@ def action_update if operations.empty? print_good('There are no changes to be made.') - return + return true end @ldap.modify(dn: obj['dn'].first, operations: operations, controls: [ms_security_descriptor_control(DACL_SECURITY_INFORMATION)]) validate_query_result!(@ldap.get_operation_result.table) + true end end From 5c2056b2e1221dcc9299c5e3a3748442c9d58d27 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 29 Jan 2025 14:25:33 -0500 Subject: [PATCH 08/11] Update kerberos/get_ticket to return values --- modules/auxiliary/admin/kerberos/get_ticket.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/auxiliary/admin/kerberos/get_ticket.rb b/modules/auxiliary/admin/kerberos/get_ticket.rb index aeb2638d753b..c5eaf3d13e3b 100644 --- a/modules/auxiliary/admin/kerberos/get_ticket.rb +++ b/modules/auxiliary/admin/kerberos/get_ticket.rb @@ -142,7 +142,7 @@ def validate_options def run validate_options - send("action_#{action.name.downcase}") + result = send("action_#{action.name.downcase}") report_service( host: rhost, @@ -151,6 +151,8 @@ def run name: 'kerberos', info: "Module: #{fullname}, KDC for domain #{@realm}" ) + + result rescue ::Rex::ConnectionError => e elog('Connection error', error: e) fail_with(Failure::Unreachable, e.message) @@ -276,6 +278,7 @@ def action_get_hash print_good("Found NTLM hash for #{@username}: #{ntlm_hash}") report_ntlm(ntlm_hash) + ntlm_hash end def report_ntlm(hash) From 61a09810137920182250fcbcada5d6f7e85c7cb4 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 30 Jan 2025 13:32:04 -0500 Subject: [PATCH 09/11] Update the spec to accept the failure --- spec/acceptance/ldap_spec.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/acceptance/ldap_spec.rb b/spec/acceptance/ldap_spec.rb index 74773dff6985..abf18ce9542b 100644 --- a/spec/acceptance/ldap_spec.rb +++ b/spec/acceptance/ldap_spec.rb @@ -98,6 +98,11 @@ required: [ /Successfully queried/ ] + }, + linux: { + known_failures: [ + /Auxiliary aborted due to failure: not-found/ + ] } } }, @@ -187,7 +192,7 @@ def with_test_harness(module_test) # Skip any ignored lines from the validation input validated_lines = test_result.lines.reject do |line| is_acceptable = known_failures.any? do |acceptable_failure| - is_matching_line = is_matching_line.value.is_a?(Regexp) ? line.match?(acceptable_failure.value) : line.include?(acceptable_failure.value) + is_matching_line = acceptable_failure.value.is_a?(Regexp) ? line.match?(acceptable_failure.value) : line.include?(acceptable_failure.value) is_matching_line && acceptable_failure.if?(test_environment) end || line.match?(/Passed: \d+; Failed: \d+/) From f8dfaae599e7364044b1e1421ccd35ecfd05b770 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 31 Jan 2025 09:42:22 -0500 Subject: [PATCH 10/11] Guard FQDN lookup logic a bit more Use DNS first, then fail back to LDAP --- lib/rex/proto/ms_dnsp.rb | 2 +- .../gather/ldap_esc_vulnerable_cert_finder.rb | 104 +++++++++++------- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/lib/rex/proto/ms_dnsp.rb b/lib/rex/proto/ms_dnsp.rb index 7f11d3da3bbd..1b1445b92cd7 100644 --- a/lib/rex/proto/ms_dnsp.rb +++ b/lib/rex/proto/ms_dnsp.rb @@ -59,7 +59,7 @@ def get end def set(v) - raise TypeError, 'must be an IPv4 address' unless Rex::Socket.is_ipv4?(v) + raise TypeError, 'must be an IPv4 address' unless Rex::Socket.is_ipv4?(v) self.data = Rex::Socket.addr_aton(v) end diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index c92467b3d289..61cc1a4a2141 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -539,28 +539,30 @@ def find_enrollable_vuln_certificate_templates next if enroll_sids.empty? ca_server_fqdn = ca_server[:dnshostname][0].to_s.downcase - ca_server_ip_address = get_ip_addresses_by_fqdn(ca_server_fqdn)&.first - - if ca_server_ip_address - service = report_service({ - host: ca_server_ip_address, - port: 445, - proto: 'tcp', - name: 'AD CS', - info: "AD CS CA name: #{ca_server[:name][0]}" - }) - - report_note({ - data: ca_server[:dn][0].to_s, - service: service, - host: ca_server_ip_address, - ntype: 'windows.ad.cs.ca.dn' - }) - - report_host({ - host: ca_server_ip_address, - name: ca_server_fqdn - }) + unless ca_server_fqdn.blank? + ca_server_ip_address = get_ip_addresses_by_fqdn(ca_server_fqdn)&.first + + if ca_server_ip_address + service = report_service({ + host: ca_server_ip_address, + port: 445, + proto: 'tcp', + name: 'AD CS', + info: "AD CS CA name: #{ca_server[:name][0]}" + }) + + report_note({ + data: ca_server[:dn][0].to_s, + service: service, + host: ca_server_ip_address, + ntype: 'windows.ad.cs.ca.dn' + }) + + report_host({ + host: ca_server_ip_address, + name: ca_server_fqdn + }) + end end ca_server_key = ca_server_fqdn.to_sym @@ -606,7 +608,7 @@ def print_vulnerable_cert_info vuln_certificate_details.each do |key, hash| techniques = hash[:techniques].dup techniques.delete('ESC3_TEMPLATE_2') unless any_esc3t1 # don't report ESC3_TEMPLATE_2 if there are no instances of ESC3 - next if techniques.empty? + next if techniques.empty? || !db techniques.each do |vuln| next if vuln == 'ESC3_TEMPLATE_2' @@ -624,23 +626,27 @@ def print_vulnerable_cert_info info: "AD CS CA name: #{ca_server[:name]}" }) - vuln = report_vuln( - host: ca_server[:ip_address], - port: 445, - proto: 'tcp', - sname: 'AD CS', - name: "#{vuln} - #{key}", - info: info, - refs: REFERENCES[vuln], - service: service - ) + if ca_server[:ip_address].present? + vuln = report_vuln( + host: ca_server[:ip_address], + port: 445, + proto: 'tcp', + sname: 'AD CS', + name: "#{vuln} - #{key}", + info: info, + refs: REFERENCES[vuln], + service: service + ) + else + vuln = nil + end report_note({ data: hash[:dn], service: service, host: ca_fqdn.to_s, ntype: 'windows.ad.cs.ca.template.dn', - vuln_id: vuln.id + vuln_id: vuln&.id }) end end @@ -660,9 +666,9 @@ def print_vulnerable_cert_info end end - if hash[:write_enabled_sids] + if hash[:write_sids] print_status(' Certificate Template Write-Enabled SIDs:') - hash[:write_enabled_sids].each do |sid| + hash[:write_sids].each do |sid| print_status(" * #{highlight_sid(sid)}") end end @@ -744,13 +750,29 @@ def get_object_by_sid(object_sid) def get_ip_addresses_by_fqdn(host_fqdn) return @fqdns[host_fqdn] if @fqdns.key?(host_fqdn) + vprint_status("Resolving addresses for #{host_fqdn} via DNS.") + begin + ip_addresses = Rex::Socket.getaddresses(host_fqdn) + rescue ::SocketError + print_warning("No IP addresses were found for #{host_fqdn} via DNS.") + else + @fqdns[host_fqdn] = ip_addresses + vprint_status("Found #{ip_addresses.length} IP address#{ip_addresses.length > 1 ? 'es' : ''} via DNS.") + return ip_addresses + end + vprint_status("Looking up DNS records for #{host_fqdn} in LDAP.") hostname, _, domain = host_fqdn.partition('.') - results = query_ldap_server( - "(&(objectClass=dnsNode)(DC=#{ldap_escape_filter(hostname)}))", - %w[dnsRecord], - base_prefix: "DC=#{ldap_escape_filter(domain)},CN=MicrosoftDNS,DC=DomainDnsZones" - ) + begin + results = query_ldap_server( + "(&(objectClass=dnsNode)(DC=#{ldap_escape_filter(hostname)}))", + %w[dnsRecord], + base_prefix: "DC=#{ldap_escape_filter(domain)},CN=MicrosoftDNS,DC=DomainDnsZones" + ) + rescue Msf::Auxiliary::Failed + print_error('Encountered an error while querying LDAP for DNS records.') + @fqdns[host_fqdn] = nil + end return nil if results.blank? ip_addresses = [] From 0013db1822f431f8f30ade14efa09472e15b3c97 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 31 Jan 2025 14:48:57 -0500 Subject: [PATCH 11/11] Fix a regression in the loop logic --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index 61cc1a4a2141..74a95f9f605d 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -608,46 +608,48 @@ def print_vulnerable_cert_info vuln_certificate_details.each do |key, hash| techniques = hash[:techniques].dup techniques.delete('ESC3_TEMPLATE_2') unless any_esc3t1 # don't report ESC3_TEMPLATE_2 if there are no instances of ESC3 - next if techniques.empty? || !db - - techniques.each do |vuln| - next if vuln == 'ESC3_TEMPLATE_2' - - prefix = "#{vuln}:" - info = hash[:notes].select { |note| note.start_with?(prefix) }.map { |note| note.delete_prefix(prefix).strip }.join("\n") - info = nil if info.blank? - - hash[:ca_servers].each do |ca_fqdn, ca_server| - service = report_service({ - host: ca_server[:ip_address], - port: 445, - proto: 'tcp', - name: 'AD CS', - info: "AD CS CA name: #{ca_server[:name]}" - }) - - if ca_server[:ip_address].present? - vuln = report_vuln( + next if techniques.empty? + + if db + techniques.each do |vuln| + next if vuln == 'ESC3_TEMPLATE_2' + + prefix = "#{vuln}:" + info = hash[:notes].select { |note| note.start_with?(prefix) }.map { |note| note.delete_prefix(prefix).strip }.join("\n") + info = nil if info.blank? + + hash[:ca_servers].each do |ca_fqdn, ca_server| + service = report_service({ host: ca_server[:ip_address], port: 445, proto: 'tcp', - sname: 'AD CS', - name: "#{vuln} - #{key}", - info: info, - refs: REFERENCES[vuln], - service: service - ) - else - vuln = nil - end + name: 'AD CS', + info: "AD CS CA name: #{ca_server[:name]}" + }) + + if ca_server[:ip_address].present? + vuln = report_vuln( + host: ca_server[:ip_address], + port: 445, + proto: 'tcp', + sname: 'AD CS', + name: "#{vuln} - #{key}", + info: info, + refs: REFERENCES[vuln], + service: service + ) + else + vuln = nil + end - report_note({ - data: hash[:dn], - service: service, - host: ca_fqdn.to_s, - ntype: 'windows.ad.cs.ca.template.dn', - vuln_id: vuln&.id - }) + report_note({ + data: hash[:dn], + service: service, + host: ca_fqdn.to_s, + ntype: 'windows.ad.cs.ca.template.dn', + vuln_id: vuln&.id + }) + end end end