Skip to content

Commit 70d2708

Browse files
committed
Update the ESC finder module's reporting
1 parent c494ad4 commit 70d2708

File tree

1 file changed

+114
-56
lines changed

1 file changed

+114
-56
lines changed

modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb

Lines changed: 114 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ def rid
2929
end
3030
end
3131

32+
attr_reader :certificate_details
33+
3234
def initialize(info = {})
3335
super(
3436
update_info(
@@ -190,15 +192,15 @@ def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: [])
190192
next if allowed_sids.empty?
191193

192194
certificate_symbol = entry[:cn][0].to_sym
193-
if @vuln_certificate_details.key?(certificate_symbol)
194-
@vuln_certificate_details[certificate_symbol][:vulns] << esc_name
195-
@vuln_certificate_details[certificate_symbol][:notes] += notes
195+
if @certificate_details.key?(certificate_symbol)
196+
@certificate_details[certificate_symbol][:techniques] << esc_name
197+
@certificate_details[certificate_symbol][:notes] += notes
196198
else
197-
@vuln_certificate_details[certificate_symbol] = {
198-
vulns: [esc_name],
199+
@certificate_details[certificate_symbol] = {
200+
techniques: [esc_name],
199201
dn: entry[:dn][0],
200-
certificate_enrollment_sids: convert_sids_to_human_readable_name(allowed_sids),
201-
ca_servers_n_enrollment_sids: {},
202+
enrollment_sids: convert_sids_to_human_readable_name(allowed_sids),
203+
ca_servers: {},
202204
manager_approval: ([entry[%s(mspki-enrollment-flag)].first.to_i].pack('l').unpack1('L') & Rex::Proto::MsCrtd::CT_FLAG_PEND_ALL_REQUESTS) != 0,
203205
required_signatures: [entry[%s(mspki-ra-signature)].first.to_i].pack('l').unpack1('L'),
204206
notes: notes
@@ -210,18 +212,14 @@ def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: [])
210212
def convert_sids_to_human_readable_name(sids_array)
211213
output = []
212214
for sid in sids_array
213-
raw_filter = "(objectSID=#{ldap_escape_filter(sid.to_s)})"
214-
attributes = ['sAMAccountName', 'name']
215-
base_prefix = 'CN=Configuration'
216-
sid_entry = query_ldap_server(raw_filter, attributes, base_prefix: base_prefix) # First try with prefix to find entries that may be group specific.
217-
sid_entry = query_ldap_server(raw_filter, attributes) if sid_entry.empty? # Retry without prefix if blank.
218-
if sid_entry.empty?
215+
sid_entry = get_object_by_sid(sid)
216+
if sid_entry.nil?
219217
print_warning("Could not find any details on the LDAP server for SID #{sid}!")
220218
output << [sid, nil, nil] # Still want to print out the SID even if we couldn't get additional information.
221-
elsif sid_entry[0][:samaccountname][0]
222-
output << [sid, sid_entry[0][:name][0], sid_entry[0][:samaccountname][0]]
219+
elsif sid_entry[:samaccountname][0]
220+
output << [sid, sid_entry[:name][0], sid_entry[:samaccountname][0]]
223221
else
224-
output << [sid, sid_entry[0][:name][0], nil]
222+
output << [sid, sid_entry[:name][0], nil]
225223
end
226224
end
227225

@@ -285,14 +283,14 @@ def find_esc3_vuln_cert_templates
285283
notes = [
286284
'ESC3: Template defines the Certificate Request Agent OID (PkiExtendedKeyUsage)'
287285
]
288-
query_ldap_server_certificates(esc3_template_1_raw_filter, 'ESC3_TEMPLATE_1', notes: notes)
286+
query_ldap_server_certificates(esc3_template_1_raw_filter, 'ESC3', notes: notes)
289287

290288
# Find the second vulnerable types of ESC3 templates, those that
291289
# have the right template schema version and, for those with a template
292290
# version of 2 or greater, have an Application Policy Insurance Requirement
293291
# requiring the Certificate Request Agent EKU.
294292
#
295-
# Additionally the certificate template must also allow for domain authentication
293+
# Additionally, the certificate template must also allow for domain authentication
296294
# and the CA must not have any enrollment agent restrictions.
297295
esc3_template_2_raw_filter = '(&'\
298296
'(objectclass=pkicertificatetemplate)'\
@@ -365,11 +363,18 @@ def find_esc13_vuln_cert_templates
365363

366364
note = "ESC13 groups: #{groups.join(', ')}"
367365
certificate_symbol = entry[:cn][0].to_sym
368-
if @vuln_certificate_details.key?(certificate_symbol)
369-
@vuln_certificate_details[certificate_symbol][:vulns] << 'ESC13'
370-
@vuln_certificate_details[certificate_symbol][:notes] << note
366+
if @certificate_details.key?(certificate_symbol)
367+
@certificate_details[certificate_symbol][:techniques] << 'ESC13'
368+
@certificate_details[certificate_symbol][:notes] << note
371369
else
372-
@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] }
370+
@certificate_details[certificate_symbol] = {
371+
name: certificate_symbol.to_s,
372+
techniques: ['ESC13'],
373+
dn: entry[:dn][0].to_s,
374+
enrollment_sids: convert_sids_to_human_readable_name(allowed_sids),
375+
ca_servers: {},
376+
notes: [note]
377+
}
373378
end
374379
end
375380
end
@@ -394,7 +399,7 @@ def find_enrollable_vuln_certificate_templates
394399
# allows users to enroll in that certificate template and which users/groups
395400
# have permissions to enroll in certificates on each server.
396401

397-
@vuln_certificate_details.each_key do |certificate_template|
402+
@certificate_details.each_key do |certificate_template|
398403
certificate_enrollment_raw_filter = "(&(objectClass=pKIEnrollmentService)(certificateTemplates=#{ldap_escape_filter(certificate_template.to_s)}))"
399404
attributes = ['cn', 'dnsHostname', 'ntsecuritydescriptor']
400405
base_prefix = 'CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration'
@@ -411,18 +416,41 @@ def find_enrollable_vuln_certificate_templates
411416
allowed_sids = parse_acl(security_descriptor.dacl) if security_descriptor.dacl
412417
next if allowed_sids.empty?
413418

419+
service = report_service({
420+
host: ca_server[:dnshostname][0],
421+
port: 445,
422+
name: 'AD CS',
423+
info: ca_server[:cn][0]
424+
})
425+
426+
report_note({
427+
data: ca_server[:dn][0].to_s,
428+
service: service,
429+
host: ca_server[:dnshostname][0],
430+
ntype: 'windows.ad.cs.ca.dn'
431+
})
432+
433+
report_host({
434+
host: ca_server[:dnshostname][0],
435+
name: ca_server[:dnshostname][0]
436+
})
437+
414438
ca_server_key = ca_server[:dnshostname][0].to_sym
415-
unless @vuln_certificate_details[certificate_template][:ca_servers_n_enrollment_sids].key?(ca_server_key)
416-
@vuln_certificate_details[certificate_template][:ca_servers_n_enrollment_sids][ca_server_key] = { cn: ca_server[:cn][0], ca_enrollment_sids: allowed_sids }
417-
end
439+
next if @certificate_details[certificate_template][:ca_servers].key?(ca_server_key)
440+
441+
@certificate_details[certificate_template][:ca_servers][ca_server_key] = {
442+
enrollment_sids: allowed_sids,
443+
cn: ca_server[:cn][0].to_s,
444+
dn: ca_server[:dn][0].to_s
445+
}
418446
end
419447
end
420448
end
421449

422450
def print_vulnerable_cert_info
423-
vuln_certificate_details = @vuln_certificate_details.select do |_key, hash|
451+
vuln_certificate_details = @certificate_details.select do |_key, hash|
424452
select = true
425-
select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:certificate_enrollment_sids].any? do |sid|
453+
select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:enrollment_sids].any? do |sid|
426454
# compare based on RIDs to avoid issues language specific issues
427455
!(sid.value.starts_with?("#{WellKnownSids::SECURITY_NT_NON_UNIQUE}-") && [
428456
# RID checks
@@ -437,44 +465,62 @@ def print_vulnerable_cert_info
437465
].include?(sid.value)
438466
end
439467

440-
select = false unless datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers_n_enrollment_sids].any?
468+
select = false unless datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers].any?
441469
select
442470
end
443471

444472
any_esc3t1 = vuln_certificate_details.values.any? do |hash|
445-
hash[:vulns].include?('ESC3_TEMPLATE_1') && (datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers_n_enrollment_sids].any?)
473+
hash[:techniques].include?('ESC3') && (datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers].any?)
446474
end
447475

448476
vuln_certificate_details.each do |key, hash|
449-
vulns = hash[:vulns]
450-
vulns.delete('ESC3_TEMPLATE_2') unless any_esc3t1 # don't report ESC3_TEMPLATE_2 if there are no instances of ESC3_TEMPLATE_1
451-
next if vulns.empty?
477+
techniques = hash[:techniques]
478+
techniques.delete('ESC3_TEMPLATE_2') unless any_esc3t1 # don't report ESC3_TEMPLATE_2 if there are no instances of ESC3
479+
next if techniques.empty?
452480

453-
vulns.each do |vuln|
454-
vuln = 'ESC3' if vuln == 'ESC3_TEMPLATE_1'
481+
techniques.each do |vuln|
455482
next if vuln == 'ESC3_TEMPLATE_2'
456483

457484
prefix = "#{vuln}:"
458485
info = hash[:notes].select { |note| note.start_with?(prefix) }.map { |note| note.delete_prefix(prefix).strip }.join("\n")
459486
info = nil if info.blank?
460487

461-
report_vuln(
462-
host: rhost,
463-
port: rport,
464-
proto: 'tcp',
465-
sname: 'AD CS',
466-
name: "#{vuln} - #{key}",
467-
info: info,
468-
refs: REFERENCES[vuln]
469-
)
488+
hash[:ca_servers].each do |dnshostname, ca_server|
489+
service = report_service({
490+
host: dnshostname.to_s,
491+
port: 445,
492+
proto: 'tcp',
493+
name: 'AD CS',
494+
info: "AD CS CA name: #{ca_server[:cn]}"
495+
})
496+
497+
vuln = report_vuln(
498+
host: dnshostname.to_s,
499+
port: 445,
500+
proto: 'tcp',
501+
sname: 'AD CS',
502+
name: "#{vuln} - #{key}",
503+
info: info,
504+
refs: REFERENCES[vuln],
505+
service: service
506+
)
507+
508+
report_note({
509+
data: hash[:dn],
510+
service: service,
511+
host: dnshostname.to_s,
512+
ntype: 'windows.ad.cs.ca.template.dn',
513+
vuln_id: vuln.id
514+
})
515+
end
470516
end
471517

472518
print_good("Template: #{key}")
473519

474520
print_status(" Distinguished Name: #{hash[:dn]}")
475521
print_status(" Manager Approval: #{hash[:manager_approval] ? '%redRequired' : '%grnDisabled'}%clr")
476522
print_status(" Required Signatures: #{hash[:required_signatures] == 0 ? '%grn0' : '%red' + hash[:required_signatures].to_s}%clr")
477-
print_good(" Vulnerable to: #{vulns.join(', ')}")
523+
print_good(" Vulnerable to: #{techniques.join(', ')}")
478524
if hash[:notes].present? && hash[:notes].length == 1
479525
print_status(" Notes: #{hash[:notes].first}")
480526
elsif hash[:notes].present? && hash[:notes].length > 1
@@ -485,15 +531,15 @@ def print_vulnerable_cert_info
485531
end
486532

487533
print_status(' Certificate Template Enrollment SIDs:')
488-
hash[:certificate_enrollment_sids].each do |sid|
534+
hash[:enrollment_sids].each do |sid|
489535
print_status(" * #{highlight_sid(sid)}")
490536
end
491537

492-
if hash[:ca_servers_n_enrollment_sids].any?
493-
hash[:ca_servers_n_enrollment_sids].each do |ca_hostname, ca_hash|
538+
if hash[:ca_servers].any?
539+
hash[:ca_servers].each do |ca_hostname, ca_hash|
494540
print_good(" Issuing CA: #{ca_hash[:cn]} (#{ca_hostname})")
495541
print_status(' Enrollment SIDs:')
496-
convert_sids_to_human_readable_name(ca_hash[:ca_enrollment_sids]).each do |sid|
542+
convert_sids_to_human_readable_name(ca_hash[:enrollment_sids]).each do |sid|
497543
print_status(" * #{highlight_sid(sid)}")
498544
end
499545
end
@@ -515,22 +561,22 @@ def highlight_sid(sid)
515561
end
516562

517563
def get_pki_object_by_oid(oid)
518-
pki_object = @ldap_mspki_enterprise_oids.find { |o| o['mspki-cert-template-oid'].first == oid }
564+
pki_object = @ldap_objects.find { |o| o['mspki-cert-template-oid']&.first == oid }
519565

520566
if pki_object.nil?
521567
pki_object = query_ldap_server(
522568
"(&(objectClass=msPKI-Enterprise-Oid)(msPKI-Cert-Template-OID=#{ldap_escape_filter(oid.to_s)}))",
523569
nil,
524570
base_prefix: 'CN=OID,CN=Public Key Services,CN=Services,CN=Configuration'
525571
)&.first
526-
@ldap_mspki_enterprise_oids << pki_object if pki_object
572+
@ldap_objects << pki_object if pki_object
527573
end
528574

529575
pki_object
530576
end
531577

532578
def get_group_by_dn(group_dn)
533-
group = @ldap_groups.find { |o| o['dn'].first == group_dn }
579+
group = @ldap_objects.find { |o| o['dn']&.first == group_dn }
534580

535581
if group.nil?
536582
cn, _, base = group_dn.partition(',')
@@ -540,18 +586,29 @@ def get_group_by_dn(group_dn)
540586
nil,
541587
base_prefix: base
542588
)&.first
543-
@ldap_groups << group if group
589+
@ldap_objects << group if group
544590
end
545591

546592
group
547593
end
548594

595+
def get_object_by_sid(object_sid)
596+
object_sid = Rex::Proto::MsDtyp::MsDtypSid.new(object_sid)
597+
object = @ldap_objects.find { |o| o['objectSID'].first == object_sid.to_binary_s }
598+
599+
if object.nil?
600+
object = query_ldap_server("(objectSID=#{ldap_escape_filter(object_sid.to_s)})", nil)&.first
601+
@ldap_objects << object if object
602+
end
603+
604+
object
605+
end
606+
549607
def run
550608
# Define our instance variables real quick.
551609
@base_dn = nil
552-
@ldap_mspki_enterprise_oids = []
553-
@ldap_groups = []
554-
@vuln_certificate_details = {} # Initialize to empty hash since we want to only keep one copy of each certificate template along with its details.
610+
@ldap_objects = []
611+
@certificate_details = {} # Initialize to empty hash since we want to only keep one copy of each certificate template along with its details.
555612

556613
ldap_connect do |ldap|
557614
validate_bind_success!(ldap)
@@ -575,6 +632,7 @@ def run
575632

576633
find_enrollable_vuln_certificate_templates
577634
print_vulnerable_cert_info
635+
@certificate_details
578636
end
579637
rescue Errno::ECONNRESET
580638
fail_with(Failure::Disconnected, 'The connection was reset.')

0 commit comments

Comments
 (0)