Skip to content

Commit dc87024

Browse files
committed
Update the ESC finder module's reporting
1 parent 157763b commit dc87024

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
@@ -31,6 +31,8 @@ def rid
3131
end
3232
end
3333

34+
attr_reader :certificate_details
35+
3436
def initialize(info = {})
3537
super(
3638
update_info(
@@ -206,7 +208,7 @@ def query_ldap_server(raw_filter, attributes, base_prefix: nil)
206208
end
207209

208210
def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: [])
209-
attributes = ['cn', 'description', 'ntSecurityDescriptor', 'msPKI-Enrollment-Flag', 'msPKI-RA-Signature', 'PkiExtendedKeyUsage']
211+
attributes = ['cn', 'name', 'description', 'ntSecurityDescriptor', 'msPKI-Enrollment-Flag', 'msPKI-RA-Signature', 'PkiExtendedKeyUsage']
210212
base_prefix = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration'
211213
esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: base_prefix)
212214

@@ -228,15 +230,16 @@ def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: [])
228230
next if allowed_sids.empty?
229231

230232
certificate_symbol = entry[:cn][0].to_sym
231-
if @vuln_certificate_details.key?(certificate_symbol)
232-
@vuln_certificate_details[certificate_symbol][:vulns] << esc_name
233-
@vuln_certificate_details[certificate_symbol][:notes] += notes
233+
if @certificate_details.key?(certificate_symbol)
234+
@certificate_details[certificate_symbol][:techniques] << esc_name
235+
@certificate_details[certificate_symbol][:notes] += notes
234236
else
235-
@vuln_certificate_details[certificate_symbol] = {
236-
vulns: [esc_name],
237-
dn: entry[:dn][0],
238-
certificate_enrollment_sids: convert_sids_to_human_readable_name(allowed_sids),
239-
ca_servers_n_enrollment_sids: {},
237+
@certificate_details[certificate_symbol] = {
238+
name: entry[:name][0].to_s,
239+
techniques: [esc_name],
240+
dn: entry[:dn][0].to_s,
241+
enrollment_sids: convert_sids_to_human_readable_name(allowed_sids),
242+
ca_servers: {},
240243
manager_approval: ([entry[%s(mspki-enrollment-flag)].first.to_i].pack('l').unpack1('L') & Rex::Proto::MsCrtd::CT_FLAG_PEND_ALL_REQUESTS) != 0,
241244
required_signatures: [entry[%s(mspki-ra-signature)].first.to_i].pack('l').unpack1('L'),
242245
notes: notes.dup
@@ -248,18 +251,14 @@ def query_ldap_server_certificates(esc_raw_filter, esc_name, notes: [])
248251
def convert_sids_to_human_readable_name(sids_array)
249252
output = []
250253
for sid in sids_array
251-
raw_filter = "(objectSID=#{ldap_escape_filter(sid.to_s)})"
252-
attributes = ['sAMAccountName', 'name']
253-
base_prefix = 'CN=Configuration'
254-
sid_entry = query_ldap_server(raw_filter, attributes, base_prefix: base_prefix) # First try with prefix to find entries that may be group specific.
255-
sid_entry = query_ldap_server(raw_filter, attributes) if sid_entry.empty? # Retry without prefix if blank.
256-
if sid_entry.empty?
254+
sid_entry = get_object_by_sid(sid)
255+
if sid_entry.nil?
257256
print_warning("Could not find any details on the LDAP server for SID #{sid}!")
258257
output << [sid, nil, nil] # Still want to print out the SID even if we couldn't get additional information.
259-
elsif sid_entry[0][:samaccountname][0]
260-
output << [sid, sid_entry[0][:name][0], sid_entry[0][:samaccountname][0]]
258+
elsif sid_entry[:samaccountname][0]
259+
output << [sid, sid_entry[:name][0], sid_entry[:samaccountname][0]]
261260
else
262-
output << [sid, sid_entry[0][:name][0], nil]
261+
output << [sid, sid_entry[:name][0], nil]
263262
end
264263
end
265264

@@ -323,14 +322,14 @@ def find_esc3_vuln_cert_templates
323322
notes = [
324323
'ESC3: Template defines the Certificate Request Agent OID (PkiExtendedKeyUsage)'
325324
]
326-
query_ldap_server_certificates(esc3_template_1_raw_filter, 'ESC3_TEMPLATE_1', notes: notes)
325+
query_ldap_server_certificates(esc3_template_1_raw_filter, 'ESC3', notes: notes)
327326

328327
# Find the second vulnerable types of ESC3 templates, those that
329328
# have the right template schema version and, for those with a template
330329
# version of 2 or greater, have an Application Policy Insurance Requirement
331330
# requiring the Certificate Request Agent EKU.
332331
#
333-
# Additionally the certificate template must also allow for domain authentication
332+
# Additionally, the certificate template must also allow for domain authentication
334333
# and the CA must not have any enrollment agent restrictions.
335334
esc3_template_2_raw_filter = '(&'\
336335
'(objectclass=pkicertificatetemplate)'\
@@ -521,11 +520,18 @@ def find_esc13_vuln_cert_templates
521520

522521
note = "ESC13 groups: #{groups.join(', ')}"
523522
certificate_symbol = entry[:cn][0].to_sym
524-
if @vuln_certificate_details.key?(certificate_symbol)
525-
@vuln_certificate_details[certificate_symbol][:vulns] << 'ESC13'
526-
@vuln_certificate_details[certificate_symbol][:notes] << note
523+
if @certificate_details.key?(certificate_symbol)
524+
@certificate_details[certificate_symbol][:techniques] << 'ESC13'
525+
@certificate_details[certificate_symbol][:notes] << note
527526
else
528-
@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] }
527+
@certificate_details[certificate_symbol] = {
528+
name: certificate_symbol.to_s,
529+
techniques: ['ESC13'],
530+
dn: entry[:dn][0].to_s,
531+
enrollment_sids: convert_sids_to_human_readable_name(allowed_sids),
532+
ca_servers: {},
533+
notes: [note]
534+
}
529535
end
530536
end
531537
end
@@ -550,9 +556,9 @@ def find_enrollable_vuln_certificate_templates
550556
# allows users to enroll in that certificate template and which users/groups
551557
# have permissions to enroll in certificates on each server.
552558

553-
@vuln_certificate_details.each_key do |certificate_template|
559+
@certificate_details.each_key do |certificate_template|
554560
certificate_enrollment_raw_filter = "(&(objectClass=pKIEnrollmentService)(certificateTemplates=#{ldap_escape_filter(certificate_template.to_s)}))"
555-
attributes = ['cn', 'dnsHostname', 'ntsecuritydescriptor']
561+
attributes = ['cn', 'name', 'dnsHostname', 'ntsecuritydescriptor']
556562
base_prefix = 'CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration'
557563
enrollment_ca_data = query_ldap_server(certificate_enrollment_raw_filter, attributes, base_prefix: base_prefix)
558564
next if enrollment_ca_data.empty?
@@ -567,18 +573,43 @@ def find_enrollable_vuln_certificate_templates
567573
allowed_sids = parse_acl(security_descriptor.dacl) if security_descriptor.dacl
568574
next if allowed_sids.empty?
569575

576+
service = report_service({
577+
host: ca_server[:dnshostname][0],
578+
port: 445,
579+
proto: 'tcp',
580+
name: 'AD CS',
581+
info: "AD CS CA name: #{ca_server[:name][0]}"
582+
})
583+
584+
report_note({
585+
data: ca_server[:dn][0].to_s,
586+
service: service,
587+
host: ca_server[:dnshostname][0],
588+
ntype: 'windows.ad.cs.ca.dn'
589+
})
590+
591+
report_host({
592+
host: ca_server[:dnshostname][0],
593+
name: ca_server[:dnshostname][0]
594+
})
595+
570596
ca_server_key = ca_server[:dnshostname][0].to_sym
571-
unless @vuln_certificate_details[certificate_template][:ca_servers_n_enrollment_sids].key?(ca_server_key)
572-
@vuln_certificate_details[certificate_template][:ca_servers_n_enrollment_sids][ca_server_key] = { cn: ca_server[:cn][0], ca_enrollment_sids: allowed_sids }
573-
end
597+
next if @certificate_details[certificate_template][:ca_servers].key?(ca_server_key)
598+
599+
@certificate_details[certificate_template][:ca_servers][ca_server_key] = {
600+
hostname: ca_server[:dnshostname][0].to_s,
601+
enrollment_sids: allowed_sids,
602+
name: ca_server[:name][0].to_s,
603+
dn: ca_server[:dn][0].to_s
604+
}
574605
end
575606
end
576607
end
577608

578609
def print_vulnerable_cert_info
579-
vuln_certificate_details = @vuln_certificate_details.select do |_key, hash|
610+
vuln_certificate_details = @certificate_details.select do |_key, hash|
580611
select = true
581-
select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:certificate_enrollment_sids].any? do |sid|
612+
select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:enrollment_sids].any? do |sid|
582613
# compare based on RIDs to avoid issues language specific issues
583614
!(sid.value.starts_with?("#{WellKnownSids::SECURITY_NT_NON_UNIQUE}-") && [
584615
# RID checks
@@ -593,44 +624,62 @@ def print_vulnerable_cert_info
593624
].include?(sid.value)
594625
end
595626

596-
select = false unless datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers_n_enrollment_sids].any?
627+
select = false unless datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers].any?
597628
select
598629
end
599630

600631
any_esc3t1 = vuln_certificate_details.values.any? do |hash|
601-
hash[:vulns].include?('ESC3_TEMPLATE_1') && (datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers_n_enrollment_sids].any?)
632+
hash[:techniques].include?('ESC3') && (datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers].any?)
602633
end
603634

604635
vuln_certificate_details.each do |key, hash|
605-
vulns = hash[:vulns]
606-
vulns.delete('ESC3_TEMPLATE_2') unless any_esc3t1 # don't report ESC3_TEMPLATE_2 if there are no instances of ESC3_TEMPLATE_1
607-
next if vulns.empty?
636+
techniques = hash[:techniques].dup
637+
techniques.delete('ESC3_TEMPLATE_2') unless any_esc3t1 # don't report ESC3_TEMPLATE_2 if there are no instances of ESC3
638+
next if techniques.empty?
608639

609-
vulns.each do |vuln|
610-
vuln = 'ESC3' if vuln == 'ESC3_TEMPLATE_1'
640+
techniques.each do |vuln|
611641
next if vuln == 'ESC3_TEMPLATE_2'
612642

613643
prefix = "#{vuln}:"
614644
info = hash[:notes].select { |note| note.start_with?(prefix) }.map { |note| note.delete_prefix(prefix).strip }.join("\n")
615645
info = nil if info.blank?
616646

617-
report_vuln(
618-
host: rhost,
619-
port: rport,
620-
proto: 'tcp',
621-
sname: 'AD CS',
622-
name: "#{vuln} - #{key}",
623-
info: info,
624-
refs: REFERENCES[vuln]
625-
)
647+
hash[:ca_servers].each do |dnshostname, ca_server|
648+
service = report_service({
649+
host: dnshostname.to_s,
650+
port: 445,
651+
proto: 'tcp',
652+
name: 'AD CS',
653+
info: "AD CS CA name: #{ca_server[:name]}"
654+
})
655+
656+
vuln = report_vuln(
657+
host: dnshostname.to_s,
658+
port: 445,
659+
proto: 'tcp',
660+
sname: 'AD CS',
661+
name: "#{vuln} - #{key}",
662+
info: info,
663+
refs: REFERENCES[vuln],
664+
service: service
665+
)
666+
667+
report_note({
668+
data: hash[:dn],
669+
service: service,
670+
host: dnshostname.to_s,
671+
ntype: 'windows.ad.cs.ca.template.dn',
672+
vuln_id: vuln.id
673+
})
674+
end
626675
end
627676

628677
print_good("Template: #{key}")
629678

630679
print_status(" Distinguished Name: #{hash[:dn]}")
631680
print_status(" Manager Approval: #{hash[:manager_approval] ? '%redRequired' : '%grnDisabled'}%clr")
632681
print_status(" Required Signatures: #{hash[:required_signatures] == 0 ? '%grn0' : '%red' + hash[:required_signatures].to_s}%clr")
633-
print_good(" Vulnerable to: #{vulns.join(', ')}")
682+
print_good(" Vulnerable to: #{techniques.join(', ')}")
634683
if hash[:notes].present? && hash[:notes].length == 1
635684
print_status(" Notes: #{hash[:notes].first}")
636685
elsif hash[:notes].present? && hash[:notes].length > 1
@@ -648,15 +697,15 @@ def print_vulnerable_cert_info
648697
end
649698

650699
print_status(' Certificate Template Enrollment SIDs:')
651-
hash[:certificate_enrollment_sids].each do |sid|
700+
hash[:enrollment_sids].each do |sid|
652701
print_status(" * #{highlight_sid(sid)}")
653702
end
654703

655-
if hash[:ca_servers_n_enrollment_sids].any?
656-
hash[:ca_servers_n_enrollment_sids].each do |ca_hostname, ca_hash|
657-
print_good(" Issuing CA: #{ca_hash[:cn]} (#{ca_hostname})")
704+
if hash[:ca_servers].any?
705+
hash[:ca_servers].each do |ca_hostname, ca_hash|
706+
print_good(" Issuing CA: #{ca_hash[:name]} (#{ca_hostname})")
658707
print_status(' Enrollment SIDs:')
659-
convert_sids_to_human_readable_name(ca_hash[:ca_enrollment_sids]).each do |sid|
708+
convert_sids_to_human_readable_name(ca_hash[:enrollment_sids]).each do |sid|
660709
print_status(" * #{highlight_sid(sid)}")
661710
end
662711
end
@@ -678,21 +727,21 @@ def highlight_sid(sid)
678727
end
679728

680729
def get_pki_object_by_oid(oid)
681-
pki_object = @ldap_mspki_enterprise_oids.find { |o| o['mspki-cert-template-oid'].first == oid }
730+
pki_object = @ldap_objects.find { |o| o['mspki-cert-template-oid']&.first == oid }
682731

683732
if pki_object.nil?
684733
pki_object = query_ldap_server(
685734
"(&(objectClass=msPKI-Enterprise-Oid)(msPKI-Cert-Template-OID=#{ldap_escape_filter(oid.to_s)}))",
686735
nil,
687736
base_prefix: 'CN=OID,CN=Public Key Services,CN=Services,CN=Configuration'
688737
)&.first
689-
@ldap_mspki_enterprise_oids << pki_object if pki_object
738+
@ldap_objects << pki_object if pki_object
690739
end
691740
pki_object
692741
end
693742

694743
def get_group_by_dn(group_dn)
695-
group = @ldap_groups.find { |o| o['dn'].first == group_dn }
744+
group = @ldap_objects.find { |o| o['dn']&.first == group_dn }
696745

697746
if group.nil?
698747
cn, _, base = group_dn.partition(',')
@@ -702,18 +751,29 @@ def get_group_by_dn(group_dn)
702751
nil,
703752
base_prefix: base
704753
)&.first
705-
@ldap_groups << group if group
754+
@ldap_objects << group if group
706755
end
707756

708757
group
709758
end
710759

760+
def get_object_by_sid(object_sid)
761+
object_sid = Rex::Proto::MsDtyp::MsDtypSid.new(object_sid)
762+
object = @ldap_objects.find { |o| o['objectSID'].first == object_sid.to_binary_s }
763+
764+
if object.nil?
765+
object = query_ldap_server("(objectSID=#{ldap_escape_filter(object_sid.to_s)})", nil)&.first
766+
@ldap_objects << object if object
767+
end
768+
769+
object
770+
end
771+
711772
def run
712773
# Define our instance variables real quick.
713774
@base_dn = nil
714-
@ldap_mspki_enterprise_oids = []
715-
@ldap_groups = []
716-
@vuln_certificate_details = {} # Initialize to empty hash since we want to only keep one copy of each certificate template along with its details.
775+
@ldap_objects = []
776+
@certificate_details = {} # Initialize to empty hash since we want to only keep one copy of each certificate template along with its details.
717777

718778
ldap_connect do |ldap|
719779
validate_bind_success!(ldap)
@@ -738,6 +798,7 @@ def run
738798

739799
find_enrollable_vuln_certificate_templates
740800
print_vulnerable_cert_info
801+
@certificate_details
741802
end
742803
rescue Errno::ECONNRESET
743804
fail_with(Failure::Disconnected, 'The connection was reset.')

0 commit comments

Comments
 (0)