Skip to content

Commit fa33c84

Browse files
committed
Evaluate permissions for templates and CAs
1 parent 13b3af3 commit fa33c84

File tree

1 file changed

+92
-74
lines changed

1 file changed

+92
-74
lines changed

modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb

Lines changed: 92 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,8 @@ def initialize(info = {})
9595

9696
register_options([
9797
OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']),
98-
OptBool.new('REPORT_NONENROLLABLE', [true, 'Report nonenrollable certificate templates', false]),
99-
OptBool.new('REPORT_PRIVENROLLABLE', [true, 'Report certificate templates restricted to domain and enterprise admins', false]),
100-
OptBool.new('RUN_REGISTRY_CHECKS', [true, 'Authenticate to WinRM to query the registry values to enhance reporting for ESC9 and ESC10. Must be a privleged user in order to query successfully', false]),
98+
OptEnum.new('REPORT', [true, 'What templates to report (applies filtering to results)', 'all', [ 'all', 'vulnerable-and-enrollable' ]]),
99+
OptBool.new('RUN_REGISTRY_CHECKS', [true, 'Authenticate to WinRM to query the registry values to enhance reporting for ESC9, ESC10 and ESC16. Must be a privileged user in order to query successfully', false]),
101100
])
102101
end
103102

@@ -212,7 +211,7 @@ def query_ldap_server(raw_filter, attributes, base_prefix: nil)
212211
returned_entries
213212
end
214213

215-
def query_ldap_server_certificates(esc_raw_filter, esc_id, notes: [])
214+
def query_ldap_server_certificates(esc_raw_filter, esc_id, notes: [], check_enrollment: true)
216215
esc_entries = query_ldap_server(esc_raw_filter, CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE)
217216

218217
if esc_entries.empty?
@@ -224,10 +223,12 @@ def query_ldap_server_certificates(esc_raw_filter, esc_id, notes: [])
224223
# Also print out the list of SIDs that can enroll in that server.
225224
esc_entries.each do |entry|
226225
certificate_symbol = entry[:cn][0].to_sym
227-
next if @certificate_details[certificate_symbol][:enroll_sids].empty?
226+
certificate_details = @certificate_details[certificate_symbol]
227+
next if certificate_details[:enroll_sids].empty?
228+
next unless (!check_enrollment || can_enroll?(@certificate_details[certificate_symbol]))
228229

229-
@certificate_details[certificate_symbol][:techniques] << esc_id
230-
@certificate_details[certificate_symbol][:notes] += notes
230+
certificate_details[:techniques] << esc_id
231+
certificate_details[:notes] += notes
231232
end
232233
end
233234

@@ -353,7 +354,7 @@ def find_esc4_vuln_cert_templates
353354
current_user = adds_get_current_user(@ldap)[:samaccountname].first
354355
esc_entries.each do |entry|
355356
certificate_symbol = entry[:cn][0].to_sym
356-
next if @certificate_details[certificate_symbol][:enroll_sids].empty?
357+
next unless can_enroll?(@certificate_details[certificate_symbol])
357358

358359
if adds_obj_grants_permissions?(@ldap, entry, SecurityDescriptorMatcher::Allow.any(%i[WP]))
359360
@certificate_details[certificate_symbol][:techniques] << 'ESC4'
@@ -489,9 +490,7 @@ def find_esc9_vuln_cert_templates
489490
certificate_symbol = template[:cn][0].to_sym
490491

491492
enroll_sids = @certificate_details[certificate_symbol][:enroll_sids]
492-
493493
users = find_users_with_write_and_enroll_rights(enroll_sids)
494-
495494
next if users.empty?
496495

497496
user_plural = users.size > 1 ? 'accounts' : 'account'
@@ -527,9 +526,9 @@ def find_esc10_vuln_cert_templates
527526
esc10_templates = query_ldap_server(esc10_raw_filter, CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE)
528527
esc10_templates.each do |template|
529528
certificate_symbol = template[:cn][0].to_sym
529+
530530
enroll_sids = @certificate_details[certificate_symbol][:enroll_sids]
531531
users = find_users_with_write_and_enroll_rights(enroll_sids)
532-
533532
next if users.empty?
534533

535534
user_plural = users.size > 1 ? 'accounts' : 'account'
@@ -568,7 +567,7 @@ def find_esc13_vuln_cert_templates
568567
# Also print out the list of SIDs that can enroll in that server.
569568
esc_entries.each do |entry|
570569
certificate_symbol = entry[:cn][0].to_sym
571-
next if @certificate_details[certificate_symbol][:enroll_sids].empty?
570+
next unless can_enroll?(@certificate_details[certificate_symbol])
572571

573572
groups = []
574573
entry['mspki-certificate-policy'].each do |certificate_policy_oid|
@@ -595,7 +594,52 @@ def find_esc13_vuln_cert_templates
595594
end
596595
end
597596

598-
def build_certificate_details(ldap_object, techniques: [], notes: [])
597+
def build_authority_details(ldap_object)
598+
ca_server_fqdn = ldap_object[:dnshostname][0].to_s.downcase
599+
return unless ca_server_fqdn.present?
600+
601+
ca_server_ip_address = get_ip_addresses_by_fqdn(ca_server_fqdn)&.first
602+
603+
if ca_server_ip_address
604+
report_service({
605+
host: ca_server_ip_address,
606+
port: 445,
607+
proto: 'tcp',
608+
name: 'AD CS',
609+
info: "AD CS CA name: #{ldap_object[:name][0]}"
610+
})
611+
612+
report_host({
613+
host: ca_server_ip_address,
614+
name: ca_server_fqdn
615+
})
616+
end
617+
618+
begin
619+
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(ldap_object[:ntsecuritydescriptor][0])
620+
rescue IOError => e
621+
fail_with(Failure::UnexpectedReply, "Unable to read security descriptor! Error was: #{e.message}")
622+
end
623+
return unless security_descriptor.dacl
624+
625+
if adds_obj_grants_permissions?(@ldap, ldap_object, SecurityDescriptorMatcher::Allow.full_control)
626+
permissions = [ 'FULL CONTROL' ]
627+
else
628+
permissions = [ 'READ' ] # if we have the object, we can assume we have read permissions
629+
permissions << 'REQUEST CERTIFICATES' if adds_obj_grants_permissions?(@ldap, ldap_object, SecurityDescriptorMatcher::Allow.certificate_enrollment)
630+
end
631+
632+
{
633+
fqdn: ca_server_fqdn,
634+
ip_address: ca_server_ip_address,
635+
enroll_sids: get_sids_for_enroll(security_descriptor.dacl),
636+
permissions: permissions,
637+
name: ldap_object[:name][0].to_s,
638+
dn: ldap_object[:dn][0].to_s
639+
}
640+
end
641+
642+
def build_certificate_details(ldap_object)
599643
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(ldap_object[:ntsecuritydescriptor].first)
600644

601645
if security_descriptor.dacl
@@ -606,19 +650,29 @@ def build_certificate_details(ldap_object, techniques: [], notes: [])
606650
write_sids = nil
607651
end
608652

653+
if adds_obj_grants_permissions?(@ldap, ldap_object, SecurityDescriptorMatcher::Allow.full_control)
654+
permissions = [ 'FULL CONTROL' ]
655+
else
656+
permissions = [ 'READ' ] # if we have the object, we can assume we have read permissions
657+
permissions << 'WRITE' if adds_obj_grants_permissions?(@ldap, ldap_object, SecurityDescriptorMatcher::Allow.new(:WP))
658+
permissions << 'ENROLL' if adds_obj_grants_permissions?(@ldap, ldap_object, SecurityDescriptorMatcher::Allow.certificate_enrollment)
659+
permissions << 'AUTOENROLL' if adds_obj_grants_permissions?(@ldap, ldap_object, SecurityDescriptorMatcher::Allow.certificate_autoenrollment)
660+
end
661+
609662
{
610663
name: ldap_object[:cn][0].to_s,
611-
techniques: techniques,
664+
techniques: [],
612665
dn: ldap_object[:dn][0].to_s,
613666
enroll_sids: enroll_sids,
614667
write_sids: write_sids,
615668
security_descriptor: security_descriptor,
669+
permissions: permissions,
616670
ekus: ldap_object[:pkiextendedkeyusage].map(&:to_s),
617671
schema_version: ldap_object[%s(mspki-template-schema-version)].first,
618672
ca_servers: {},
619673
manager_approval: ([ldap_object[%s(mspki-enrollment-flag)].first.to_i].pack('l').unpack1('L') & Rex::Proto::MsCrtd::CT_FLAG_PEND_ALL_REQUESTS) != 0,
620674
required_signatures: [ldap_object[%s(mspki-ra-signature)].first.to_i].pack('l').unpack1('L'),
621-
notes: notes
675+
notes: []
622676
}
623677
end
624678

@@ -654,16 +708,18 @@ def find_esc16_vuln_cert_templates
654708
return if esc_entries.empty?
655709

656710
if @registry_values[:strong_certificate_binding_enforcement] && (@registry_values[:strong_certificate_binding_enforcement] == 0 || @registry_values[:strong_certificate_binding_enforcement] == 1)
657-
# Scenario 1 - StrongCertificateBindingEnforcement = 1 or 0 then it's same same as ESC9 - mark them all as vulnerable
711+
# Scenario 1 - StrongCertificateBindingEnforcement = 1 or 0 then it's the same as ESC9 - mark them all as vulnerable
658712
esc_entries.each do |entry|
659713
certificate_symbol = entry[:cn][0].to_sym
714+
660715
@certificate_details[certificate_symbol][:techniques] << 'ESC16'
661716
@certificate_details[certificate_symbol][:notes] << "ESC16: Template is vulnerable due StrongCertificateBindingEnforcement = #{@registry_values[:strong_certificate_binding_enforcement]} and the CA's disabled policy extension list includes: 1.3.6.1.4.1.311.25.2."
662717
end
663718
elsif @registry_values[:edit_flags] & EDITF_ATTRIBUTESUBJECTALTNAME2 != 0
664719
# Scenario 2 - StrongCertificateBindingEnforcement = 2 (or nil) but if EditFlags in the active policy module has EDITF_ATTRIBUTESUBJECTALTNAME2 set then ESC6 is essentially re-enabled and we mark them all as vulnerable
665720
esc_entries.each do |entry|
666721
certificate_symbol = entry[:cn][0].to_sym
722+
667723
@certificate_details[certificate_symbol][:techniques] << 'ESC16'
668724
@certificate_details[certificate_symbol][:notes] << 'ESC16: Template is vulnerable due to the active policy EditFlags having: EDITF_ATTRIBUTESUBJECTALTNAME2 set (which is essentially ESC6) combined with the CA\'s disabled policy extension list including: 1.3.6.1.4.1.311.25.2.'
669725
end
@@ -675,84 +731,36 @@ def find_enrollable_vuln_certificate_templates
675731
# allows users to enroll in that certificate template and which users/groups
676732
# have permissions to enroll in certificates on each server.
677733

734+
authority_details = {}
678735
@certificate_details.each_key do |certificate_template|
679736
certificate_enrollment_raw_filter = "(&(objectClass=pKIEnrollmentService)(certificateTemplates=#{ldap_escape_filter(certificate_template.to_s)}))"
680737
attributes = ['cn', 'name', 'dnsHostname', 'ntsecuritydescriptor']
681738
base_prefix = 'CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration'
682739
enrollment_ca_data = query_ldap_server(certificate_enrollment_raw_filter, attributes, base_prefix: base_prefix)
683740
next if enrollment_ca_data.empty?
684741

685-
enrollment_ca_data.each do |ca_server|
686-
begin
687-
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(ca_server[:ntsecuritydescriptor][0])
688-
rescue IOError => e
689-
fail_with(Failure::UnexpectedReply, "Unable to read security descriptor! Error was: #{e.message}")
690-
end
691-
692-
enroll_sids = get_sids_for_enroll(security_descriptor.dacl) if security_descriptor.dacl
693-
next if enroll_sids.empty?
694-
695-
ca_server_fqdn = ca_server[:dnshostname][0].to_s.downcase
696-
unless ca_server_fqdn.blank?
697-
ca_server_ip_address = get_ip_addresses_by_fqdn(ca_server_fqdn)&.first
698-
699-
if ca_server_ip_address
700-
report_service({
701-
host: ca_server_ip_address,
702-
port: 445,
703-
proto: 'tcp',
704-
name: 'AD CS',
705-
info: "AD CS CA name: #{ca_server[:name][0]}"
706-
})
707-
708-
report_host({
709-
host: ca_server_ip_address,
710-
name: ca_server_fqdn
711-
})
712-
end
713-
end
714-
715-
ca_server_key = ca_server_fqdn.to_sym
742+
enrollment_ca_data.each do |ldap_object|
743+
ca_server_key = ldap_object[:dnshostname].first.to_s.downcase.to_sym
716744
next if @certificate_details[certificate_template][:ca_servers].key?(ca_server_key)
717745

718-
@certificate_details[certificate_template][:ca_servers][ca_server_key] = {
719-
fqdn: ca_server_fqdn,
720-
ip_address: ca_server_ip_address,
721-
enroll_sids: enroll_sids,
722-
name: ca_server[:name][0].to_s,
723-
dn: ca_server[:dn][0].to_s
724-
}
746+
authority_details[ca_server_key] = @certificate_details[certificate_template][:ca_servers][ca_server_key] = authority_details.fetch(ca_server_key) { build_authority_details(ldap_object) }
725747
end
726748
end
727749
end
728750

729751
def print_vulnerable_cert_info
730-
vuln_certificate_details = @certificate_details.sort.to_h.select do |_key, hash|
752+
filtered_certificate_details = @certificate_details.sort.to_h.select do |_key, details|
731753
select = true
732-
select = false unless datastore['REPORT_PRIVENROLLABLE'] || hash[:enroll_sids].any? do |sid|
733-
# compare based on RIDs to avoid issues language specific issues
734-
!(sid.value.starts_with?("#{WellKnownSids::SECURITY_NT_NON_UNIQUE}-") && [
735-
# RID checks
736-
WellKnownSids::DOMAIN_GROUP_RID_ADMINS,
737-
WellKnownSids::DOMAIN_GROUP_RID_ENTERPRISE_ADMINS,
738-
WellKnownSids::DOMAIN_GROUP_RID_ENTERPRISE_READONLY_DOMAIN_CONTROLLERS,
739-
WellKnownSids::DOMAIN_GROUP_RID_CONTROLLERS,
740-
WellKnownSids::DOMAIN_GROUP_RID_SCHEMA_ADMINS
741-
].include?(sid.rid)) && ![
742-
# SID checks
743-
WellKnownSids::SECURITY_ENTERPRISE_CONTROLLERS_SID
744-
].include?(sid.value)
745-
end
746754

747-
select = false unless datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers].any?
755+
select = false if datastore['REPORT'] != 'all' && details[:techniques].empty?
748756
select
749757
end
750758

751-
any_esc3t1 = vuln_certificate_details.values.any? do |hash|
752-
hash[:techniques].include?('ESC3') && (datastore['REPORT_NONENROLLABLE'] || hash[:ca_servers].any?)
759+
any_esc3t1 = filtered_certificate_details.values.any? do |hash|
760+
hash[:techniques].include?('ESC3') && (datastore['REPORT'] == 'all' || hash[:ca_servers].any?)
753761
end
754762

755-
vuln_certificate_details.each do |key, hash|
763+
filtered_certificate_details.each do |key, hash|
756764
techniques = hash[:techniques].dup
757765
techniques.delete('ESC3_TEMPLATE_2') unless any_esc3t1 # don't report ESC3_TEMPLATE_2 if there are no instances of ESC3
758766
next if techniques.empty?
@@ -811,6 +819,8 @@ def print_vulnerable_cert_info
811819
end
812820
end
813821

822+
print_status(" Permissions: #{hash[:permissions].join(', ')}")
823+
814824
if hash[:notes].present? && hash[:notes].length == 1
815825
print_status(" Notes: #{hash[:notes].first}")
816826
elsif hash[:notes].present? && hash[:notes].length > 1
@@ -835,6 +845,7 @@ def print_vulnerable_cert_info
835845
if hash[:ca_servers].any?
836846
hash[:ca_servers].each do |ca_fqdn, ca_hash|
837847
print_good(" Issuing CA: #{ca_hash[:name]} (#{ca_fqdn})")
848+
print_status(" Permissions: #{ca_hash[:permissions].join(', ')}")
838849
print_status(' Enrollment SIDs:')
839850
ca_hash[:enroll_sids].each do |sid|
840851
print_status(" * #{highlight_sid(sid)}")
@@ -954,6 +965,13 @@ def get_ip_addresses_by_fqdn(host_fqdn)
954965
ip_addresses
955966
end
956967

968+
def can_enroll?(details)
969+
return false unless (details[:permissions].include?('FULL CONTROL') || details[:permissions].include?('ENROLL'))
970+
return false unless details[:ca_servers].values.any? { _1[:permissions].include?('FULL CONTROL') || _1[:permissions].include?('REQUEST CERTIFICATES') }
971+
972+
true
973+
end
974+
957975
def validate
958976
super
959977
if (datastore['RUN_REGISTRY_CHECKS']) && !%w[auto plaintext ntlm].include?(datastore['LDAP::Auth'].downcase)
@@ -993,6 +1011,7 @@ def run
9931011

9941012
registry_values = enum_registry_values if datastore['RUN_REGISTRY_CHECKS']
9951013

1014+
find_enrollable_vuln_certificate_templates
9961015
find_esc1_vuln_cert_templates
9971016
find_esc2_vuln_cert_templates
9981017
find_esc3_vuln_cert_templates
@@ -1016,7 +1035,6 @@ def run
10161035
find_esc16_vuln_cert_templates
10171036
end
10181037

1019-
find_enrollable_vuln_certificate_templates
10201038
print_vulnerable_cert_info
10211039

10221040
@certificate_details

0 commit comments

Comments
 (0)