Skip to content

Commit 2ef1dd7

Browse files
committed
Update the ESC finder module's reporting
1 parent c494ad4 commit 2ef1dd7

File tree

1 file changed

+121
-60
lines changed

1 file changed

+121
-60
lines changed

modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb

Lines changed: 121 additions & 60 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(
@@ -167,7 +169,7 @@ def query_ldap_server(raw_filter, attributes, base_prefix: nil)
167169
end
168170

169171
def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: [])
170-
attributes = ['cn', 'description', 'ntSecurityDescriptor', 'msPKI-Enrollment-Flag', 'msPKI-RA-Signature', 'PkiExtendedKeyUsage']
172+
attributes = ['cn', 'name', 'description', 'ntSecurityDescriptor', 'msPKI-Enrollment-Flag', 'msPKI-RA-Signature', 'PkiExtendedKeyUsage']
171173
base_prefix = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration'
172174
esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: base_prefix)
173175

@@ -190,15 +192,16 @@ 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-
dn: entry[:dn][0],
200-
certificate_enrollment_sids: convert_sids_to_human_readable_name(allowed_sids),
201-
ca_servers_n_enrollment_sids: {},
199+
@certificate_details[certificate_symbol] = {
200+
name: entry[:name][0].to_s,
201+
techniques: [esc_name],
202+
dn: entry[:dn][0].to_s,
203+
enrollment_sids: convert_sids_to_human_readable_name(allowed_sids),
204+
ca_servers: {},
202205
manager_approval: ([entry[%s(mspki-enrollment-flag)].first.to_i].pack('l').unpack1('L') & Rex::Proto::MsCrtd::CT_FLAG_PEND_ALL_REQUESTS) != 0,
203206
required_signatures: [entry[%s(mspki-ra-signature)].first.to_i].pack('l').unpack1('L'),
204207
notes: notes
@@ -210,18 +213,14 @@ def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: [])
210213
def convert_sids_to_human_readable_name(sids_array)
211214
output = []
212215
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?
216+
sid_entry = get_object_by_sid(sid)
217+
if sid_entry.nil?
219218
print_warning("Could not find any details on the LDAP server for SID #{sid}!")
220219
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]]
220+
elsif sid_entry[:samaccountname][0]
221+
output << [sid, sid_entry[:name][0], sid_entry[:samaccountname][0]]
223222
else
224-
output << [sid, sid_entry[0][:name][0], nil]
223+
output << [sid, sid_entry[:name][0], nil]
225224
end
226225
end
227226

@@ -285,14 +284,14 @@ def find_esc3_vuln_cert_templates
285284
notes = [
286285
'ESC3: Template defines the Certificate Request Agent OID (PkiExtendedKeyUsage)'
287286
]
288-
query_ldap_server_certificates(esc3_template_1_raw_filter, 'ESC3_TEMPLATE_1', notes: notes)
287+
query_ldap_server_certificates(esc3_template_1_raw_filter, 'ESC3', notes: notes)
289288

290289
# Find the second vulnerable types of ESC3 templates, those that
291290
# have the right template schema version and, for those with a template
292291
# version of 2 or greater, have an Application Policy Insurance Requirement
293292
# requiring the Certificate Request Agent EKU.
294293
#
295-
# Additionally the certificate template must also allow for domain authentication
294+
# Additionally, the certificate template must also allow for domain authentication
296295
# and the CA must not have any enrollment agent restrictions.
297296
esc3_template_2_raw_filter = '(&'\
298297
'(objectclass=pkicertificatetemplate)'\
@@ -365,11 +364,18 @@ def find_esc13_vuln_cert_templates
365364

366365
note = "ESC13 groups: #{groups.join(', ')}"
367366
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
367+
if @certificate_details.key?(certificate_symbol)
368+
@certificate_details[certificate_symbol][:techniques] << 'ESC13'
369+
@certificate_details[certificate_symbol][:notes] << note
371370
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] }
371+
@certificate_details[certificate_symbol] = {
372+
name: certificate_symbol.to_s,
373+
techniques: ['ESC13'],
374+
dn: entry[:dn][0].to_s,
375+
enrollment_sids: convert_sids_to_human_readable_name(allowed_sids),
376+
ca_servers: {},
377+
notes: [note]
378+
}
373379
end
374380
end
375381
end
@@ -394,9 +400,9 @@ def find_enrollable_vuln_certificate_templates
394400
# allows users to enroll in that certificate template and which users/groups
395401
# have permissions to enroll in certificates on each server.
396402

397-
@vuln_certificate_details.each_key do |certificate_template|
403+
@certificate_details.each_key do |certificate_template|
398404
certificate_enrollment_raw_filter = "(&(objectClass=pKIEnrollmentService)(certificateTemplates=#{ldap_escape_filter(certificate_template.to_s)}))"
399-
attributes = ['cn', 'dnsHostname', 'ntsecuritydescriptor']
405+
attributes = ['cn', 'name', 'dnsHostname', 'ntsecuritydescriptor']
400406
base_prefix = 'CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration'
401407
enrollment_ca_data = query_ldap_server(certificate_enrollment_raw_filter, attributes, base_prefix: base_prefix)
402408
next if enrollment_ca_data.empty?
@@ -411,18 +417,43 @@ def find_enrollable_vuln_certificate_templates
411417
allowed_sids = parse_acl(security_descriptor.dacl) if security_descriptor.dacl
412418
next if allowed_sids.empty?
413419

420+
service = report_service({
421+
host: ca_server[:dnshostname][0],
422+
port: 445,
423+
proto: 'tcp',
424+
name: 'AD CS',
425+
info: "AD CS CA name: #{ca_server[:name][0]}"
426+
})
427+
428+
report_note({
429+
data: ca_server[:dn][0].to_s,
430+
service: service,
431+
host: ca_server[:dnshostname][0],
432+
ntype: 'windows.ad.cs.ca.dn'
433+
})
434+
435+
report_host({
436+
host: ca_server[:dnshostname][0],
437+
name: ca_server[:dnshostname][0]
438+
})
439+
414440
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
441+
next if @certificate_details[certificate_template][:ca_servers].key?(ca_server_key)
442+
443+
@certificate_details[certificate_template][:ca_servers][ca_server_key] = {
444+
hostname: ca_server[:dnshostname][0].to_s,
445+
enrollment_sids: allowed_sids,
446+
name: ca_server[:name][0].to_s,
447+
dn: ca_server[:dn][0].to_s
448+
}
418449
end
419450
end
420451
end
421452

422453
def print_vulnerable_cert_info
423-
vuln_certificate_details = @vuln_certificate_details.select do |_key, hash|
454+
vuln_certificate_details = @certificate_details.select do |_key, hash|
424455
select = true
425-
select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:certificate_enrollment_sids].any? do |sid|
456+
select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:enrollment_sids].any? do |sid|
426457
# compare based on RIDs to avoid issues language specific issues
427458
!(sid.value.starts_with?("#{WellKnownSids::SECURITY_NT_NON_UNIQUE}-") && [
428459
# RID checks
@@ -437,44 +468,62 @@ def print_vulnerable_cert_info
437468
].include?(sid.value)
438469
end
439470

440-
select = false unless datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers_n_enrollment_sids].any?
471+
select = false unless datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers].any?
441472
select
442473
end
443474

444475
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?)
476+
hash[:techniques].include?('ESC3') && (datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers].any?)
446477
end
447478

448479
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?
480+
techniques = hash[:techniques].dup
481+
techniques.delete('ESC3_TEMPLATE_2') unless any_esc3t1 # don't report ESC3_TEMPLATE_2 if there are no instances of ESC3
482+
next if techniques.empty?
452483

453-
vulns.each do |vuln|
454-
vuln = 'ESC3' if vuln == 'ESC3_TEMPLATE_1'
484+
techniques.each do |vuln|
455485
next if vuln == 'ESC3_TEMPLATE_2'
456486

457487
prefix = "#{vuln}:"
458488
info = hash[:notes].select { |note| note.start_with?(prefix) }.map { |note| note.delete_prefix(prefix).strip }.join("\n")
459489
info = nil if info.blank?
460490

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-
)
491+
hash[:ca_servers].each do |dnshostname, ca_server|
492+
service = report_service({
493+
host: dnshostname.to_s,
494+
port: 445,
495+
proto: 'tcp',
496+
name: 'AD CS',
497+
info: "AD CS CA name: #{ca_server[:name]}"
498+
})
499+
500+
vuln = report_vuln(
501+
host: dnshostname.to_s,
502+
port: 445,
503+
proto: 'tcp',
504+
sname: 'AD CS',
505+
name: "#{vuln} - #{key}",
506+
info: info,
507+
refs: REFERENCES[vuln],
508+
service: service
509+
)
510+
511+
report_note({
512+
data: hash[:dn],
513+
service: service,
514+
host: dnshostname.to_s,
515+
ntype: 'windows.ad.cs.ca.template.dn',
516+
vuln_id: vuln.id
517+
})
518+
end
470519
end
471520

472521
print_good("Template: #{key}")
473522

474523
print_status(" Distinguished Name: #{hash[:dn]}")
475524
print_status(" Manager Approval: #{hash[:manager_approval] ? '%redRequired' : '%grnDisabled'}%clr")
476525
print_status(" Required Signatures: #{hash[:required_signatures] == 0 ? '%grn0' : '%red' + hash[:required_signatures].to_s}%clr")
477-
print_good(" Vulnerable to: #{vulns.join(', ')}")
526+
print_good(" Vulnerable to: #{techniques.join(', ')}")
478527
if hash[:notes].present? && hash[:notes].length == 1
479528
print_status(" Notes: #{hash[:notes].first}")
480529
elsif hash[:notes].present? && hash[:notes].length > 1
@@ -485,15 +534,15 @@ def print_vulnerable_cert_info
485534
end
486535

487536
print_status(' Certificate Template Enrollment SIDs:')
488-
hash[:certificate_enrollment_sids].each do |sid|
537+
hash[:enrollment_sids].each do |sid|
489538
print_status(" * #{highlight_sid(sid)}")
490539
end
491540

492-
if hash[:ca_servers_n_enrollment_sids].any?
493-
hash[:ca_servers_n_enrollment_sids].each do |ca_hostname, ca_hash|
494-
print_good(" Issuing CA: #{ca_hash[:cn]} (#{ca_hostname})")
541+
if hash[:ca_servers].any?
542+
hash[:ca_servers].each do |ca_hostname, ca_hash|
543+
print_good(" Issuing CA: #{ca_hash[:name]} (#{ca_hostname})")
495544
print_status(' Enrollment SIDs:')
496-
convert_sids_to_human_readable_name(ca_hash[:ca_enrollment_sids]).each do |sid|
545+
convert_sids_to_human_readable_name(ca_hash[:enrollment_sids]).each do |sid|
497546
print_status(" * #{highlight_sid(sid)}")
498547
end
499548
end
@@ -515,22 +564,22 @@ def highlight_sid(sid)
515564
end
516565

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

520569
if pki_object.nil?
521570
pki_object = query_ldap_server(
522571
"(&(objectClass=msPKI-Enterprise-Oid)(msPKI-Cert-Template-OID=#{ldap_escape_filter(oid.to_s)}))",
523572
nil,
524573
base_prefix: 'CN=OID,CN=Public Key Services,CN=Services,CN=Configuration'
525574
)&.first
526-
@ldap_mspki_enterprise_oids << pki_object if pki_object
575+
@ldap_objects << pki_object if pki_object
527576
end
528577

529578
pki_object
530579
end
531580

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

535584
if group.nil?
536585
cn, _, base = group_dn.partition(',')
@@ -540,18 +589,29 @@ def get_group_by_dn(group_dn)
540589
nil,
541590
base_prefix: base
542591
)&.first
543-
@ldap_groups << group if group
592+
@ldap_objects << group if group
544593
end
545594

546595
group
547596
end
548597

598+
def get_object_by_sid(object_sid)
599+
object_sid = Rex::Proto::MsDtyp::MsDtypSid.new(object_sid)
600+
object = @ldap_objects.find { |o| o['objectSID'].first == object_sid.to_binary_s }
601+
602+
if object.nil?
603+
object = query_ldap_server("(objectSID=#{ldap_escape_filter(object_sid.to_s)})", nil)&.first
604+
@ldap_objects << object if object
605+
end
606+
607+
object
608+
end
609+
549610
def run
550611
# Define our instance variables real quick.
551612
@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.
613+
@ldap_objects = []
614+
@certificate_details = {} # Initialize to empty hash since we want to only keep one copy of each certificate template along with its details.
555615

556616
ldap_connect do |ldap|
557617
validate_bind_success!(ldap)
@@ -575,6 +635,7 @@ def run
575635

576636
find_enrollable_vuln_certificate_templates
577637
print_vulnerable_cert_info
638+
@certificate_details
578639
end
579640
rescue Errno::ECONNRESET
580641
fail_with(Failure::Disconnected, 'The connection was reset.')

0 commit comments

Comments
 (0)