Skip to content

Commit 975b8ae

Browse files
committed
Update the ad_cs_cert_template module too
1 parent 3a5b275 commit 975b8ae

File tree

3 files changed

+62
-57
lines changed

3 files changed

+62
-57
lines changed

lib/msf/core/exploit/remote/ldap/active_directory.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def is_active_directory?(ldap)
4141
# security descriptor's DACL without the need for elevated permissions.
4242
#
4343
# @rtype String
44-
def adds_build_ldap_sd_control
44+
def adds_build_ldap_sd_control(owner: true, group: true, dacl: true, sacl: false)
4545
# Set the value of LDAP_SERVER_SD_FLAGS_OID flag so everything but
4646
# the SACL flag is set, as we need administrative privileges to retrieve
4747
# the SACL from the ntSecurityDescriptor attribute on Windows AD LDAP servers.
@@ -55,8 +55,12 @@ def adds_build_ldap_sd_control
5555
# however in reality LDAP will cause that attribute to just be blanked out if a part of it
5656
# cannot be retrieved, so we just will get nothing for the ntSecurityDescriptor attribute
5757
# in these cases if the user doesn't have permissions to read the SACL.
58-
all_but_sacl_flag = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION
59-
control_values = [all_but_sacl_flag].map(&:to_ber).to_ber_sequence.to_s.to_ber
58+
flags = 0
59+
flags |= OWNER_SECURITY_INFORMATION if owner
60+
flags |= GROUP_SECURITY_INFORMATION if group
61+
flags |= DACL_SECURITY_INFORMATION if dacl
62+
flags |= SACL_SECURITY_INFORMATION if sacl
63+
control_values = [flags].map(&:to_ber).to_ber_sequence.to_s.to_ber
6064
[LDAP_SERVER_SD_FLAGS_OID.to_ber, true.to_ber, control_values].to_ber_sequence
6165
end
6266

lib/msf/core/exploit/remote/ldap/active_directory/security_descriptor_matcher.rb

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@ def matches?
4444
end
4545
end
4646

47-
# A general purpose Security Descriptor matcher for a single permission.
47+
# A general purpose Security Descriptor matcher for permissions in a single ACE. You typically want to use this to
48+
# check for a single permission.
4849
class Allow < Base
4950
attr_reader :permissions
5051

51-
# @param [Symbol] permission The abbreviated permission name e.g. :WP.
52+
# @param [Symbol, Array<Symbol>] permissions The abbreviated permission names e.g. :WP.
5253
# @param [String] object_id An optional object GUID to use when matching the permission.
53-
def initialize(permission, object_id: nil)
54-
@permission = permission
54+
def initialize(permissions, object_id: nil)
55+
@permissions = Array.wrap(permissions)
5556
@object_id = object_id
5657
@result = nil
5758
end
@@ -66,7 +67,8 @@ def ignore_ace?(ace)
6667
return true if @object_id
6768
end
6869

69-
!ace.body.access_mask.permissions.include?(@permission)
70+
ace_permissions = ace.body.access_mask.permissions
71+
!@permissions.all? { |perm| ace_permissions.include?(perm) }
7072
end
7173

7274
def apply_ace!(ace)
@@ -107,9 +109,25 @@ def self.all(permissions, object_id: nil)
107109
MultipleAll.new(permissions.map { |permission| new(permission, object_id: object_id) })
108110
end
109111

112+
def self.full_control
113+
# Full Control is special and shouldn't be split across multiple ACEs, so to check that we use #new instead of
114+
# MultipleAll to ensure it's in 1 ACE.
115+
new(%i[ CC DC LC SW RP WP DT LO CR SD RC WD WO ])
116+
end
117+
118+
def self.certificate_autoenrollment
119+
MultipleAny.new([
120+
Allow.new(:CR, object_id: CERTIFICATE_AUTOENROLLMENT_EXTENDED_RIGHT),
121+
full_control
122+
])
123+
end
124+
110125
# Build a matcher that will check for a certificate's enrollment permission.
111126
def self.certificate_enrollment
112-
new(:CR, object_id: CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT)
127+
MultipleAny.new([
128+
Allow.new(:CR, object_id: CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT),
129+
full_control
130+
])
113131
end
114132
end
115133

modules/auxiliary/admin/ldap/ad_cs_cert_template.rb

Lines changed: 31 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
class MetasploitModule < Msf::Auxiliary
77

8-
include Msf::Exploit::Remote::LDAP
8+
include Msf::Exploit::Remote::LDAP::ActiveDirectory
99
include Msf::OptionalSession::LDAP
1010
include Msf::Auxiliary::Report
1111

@@ -30,13 +30,6 @@ class MetasploitModule < Msf::Auxiliary
3030
'msPKI-Template-Minor-Revision',
3131
].freeze
3232

33-
# LDAP_SERVER_SD_FLAGS constant definition, taken from https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID
34-
LDAP_SERVER_SD_FLAGS_OID = '1.2.840.113556.1.4.801'.freeze
35-
OWNER_SECURITY_INFORMATION = 0x1
36-
GROUP_SECURITY_INFORMATION = 0x2
37-
DACL_SECURITY_INFORMATION = 0x4
38-
SACL_SECURITY_INFORMATION = 0x8
39-
4033
def initialize(info = {})
4134
super(
4235
update_info(
@@ -88,20 +81,6 @@ def initialize(info = {})
8881
])
8982
end
9083

91-
def ldap_get(filter, attributes: [], base: nil, controls: [])
92-
base ||= @base_dn
93-
raw_obj = @ldap.search(base: base, filter: filter, attributes: attributes, controls: controls).first
94-
validate_query_result!(@ldap.get_operation_result.table)
95-
return nil unless raw_obj
96-
97-
obj = {}
98-
raw_obj.attribute_names.each do |attr|
99-
obj[attr.to_s] = raw_obj[attr].map(&:to_s)
100-
end
101-
102-
obj
103-
end
104-
10584
def run
10685
ldap_connect do |ldap|
10786
validate_bind_success!(ldap)
@@ -131,15 +110,20 @@ def run
131110
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
132111
end
133112

113+
def ms_security_descriptor_control(flags)
114+
control_values = [flags].map(&:to_ber).to_ber_sequence.to_s.to_ber
115+
[LDAP_SERVER_SD_FLAGS_OID.to_ber, control_values].to_ber_sequence
116+
end
117+
134118
def get_certificate_template
135-
obj = ldap_get(
136-
"(&(cn=#{datastore['CERT_TEMPLATE']})(objectClass=pKICertificateTemplate))",
119+
obj = @ldap.search(
120+
filter: "(&(cn=#{datastore['CERT_TEMPLATE']})(objectClass=pKICertificateTemplate))",
137121
base: "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,#{@base_dn}",
138-
controls: [ms_security_descriptor_control(DACL_SECURITY_INFORMATION)]
139-
)
122+
controls: [ms_security_descriptor_control(4)]
123+
)&.first
140124
fail_with(Failure::NotFound, 'The specified template was not found.') unless obj
141125

142-
print_good("Read certificate template data for: #{obj['dn'].first}")
126+
print_good("Read certificate template data for: #{obj.dn}")
143127
stored = store_loot(
144128
'windows.ad.cs.template',
145129
'application/json',
@@ -152,15 +136,6 @@ def get_certificate_template
152136
[obj, stored]
153137
end
154138

155-
def get_domain_sid
156-
return @domain_sid if @domain_sid.present?
157-
158-
obj = ldap_get('(objectClass=domain)', attributes: %w[name objectSID])
159-
fail_with(Failure::NotFound, 'The domain SID was not found!') unless obj&.fetch('objectsid', nil)
160-
161-
Rex::Proto::MsDtyp::MsDtypSid.read(obj['objectsid'].first)
162-
end
163-
164139
def get_pki_oids
165140
return @pki_oids if @pki_oids.present?
166141

@@ -231,9 +206,12 @@ def load_from_yaml(yaml)
231206

232207
# if the string only contains printable characters, treat it as SDDL
233208
if value !~ /[^[:print:]]/
209+
vprint_status("Parsing SDDL text: #{value}")
210+
domain_info = adds_get_domain_info(@ldap)
211+
fail_with(Failure::Unknown, 'Failed to obtain the domain SID.') unless domain_info
212+
234213
begin
235-
vprint_status("Parsing SDDL text: #{value}")
236-
descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.from_sddl_text(value, domain_sid: get_domain_sid)
214+
descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.from_sddl_text(value, domain_sid: domain_info[:sid])
237215
rescue RuntimeError => e
238216
fail_with(Failure::BadConfig, e.message)
239217
end
@@ -270,11 +248,6 @@ def load_local_template
270248
end
271249
end
272250

273-
def ms_security_descriptor_control(flags)
274-
control_values = [flags].map(&:to_ber).to_ber_sequence.to_s.to_ber
275-
[LDAP_SERVER_SD_FLAGS_OID.to_ber, control_values].to_ber_sequence
276-
end
277-
278251
def action_create
279252
dn = "CN=#{datastore['CERT_TEMPLATE']},"
280253
dn << 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,'
@@ -346,14 +319,24 @@ def action_read
346319
object_guid = Rex::Proto::MsDtyp::MsDtypGuid.read(obj['objectguid'].first)
347320
print_status(" objectGUID: #{object_guid}")
348321
end
349-
if obj['ntsecuritydescriptor'].first.present?
322+
323+
if obj[:nTSecurityDescriptor].first.present?
324+
domain_info = adds_get_domain_info(@ldap)
325+
fail_with(Failure::Unknown, 'Failed to obtain the domain SID.') unless domain_info
326+
350327
begin
351-
sd = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj['ntsecuritydescriptor'].first)
352-
sddl_text = sd.to_sddl_text(domain_sid: get_domain_sid)
328+
sd = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[:nTSecurityDescriptor].first)
329+
sddl_text = sd.to_sddl_text(domain_sid: domain_info[:sid])
353330
rescue StandardError => e
354331
elog('failed to parse a binary security descriptor to SDDL', error: e)
355332
else
356333
print_status(" nTSecurityDescriptor: #{sddl_text}")
334+
permissions = [ 'READ' ] # if we have the object, we can assume we have read permissions
335+
permissions << 'WRITE' if adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.new(:WP))
336+
permissions << 'ENROLL' if adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.certificate_enrollment)
337+
permissions << 'AUTOENROLL' if adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.certificate_autoenrollment)
338+
whoami = adds_get_current_user(@ldap)
339+
print_status(" * Permissions applied for #{whoami[:userPrincipalName].first}: #{permissions.join(', ')}")
357340
end
358341
end
359342

@@ -375,7 +358,7 @@ def action_read
375358
CT_FLAG_EXPORTABLE_KEY
376359
].each do |flag_name|
377360
if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0
378-
print_status(" * #{flag_name}")
361+
print_status(" * #{flag_name}")
379362
end
380363
end
381364
end
@@ -554,7 +537,7 @@ def action_update
554537
return true
555538
end
556539

557-
@ldap.modify(dn: obj['dn'].first, operations: operations, controls: [ms_security_descriptor_control(DACL_SECURITY_INFORMATION)])
540+
@ldap.modify(dn: obj.dn, operations: operations, controls: [adds_build_ldap_sd_control(owner: false, group: false)])
558541
validate_query_result!(@ldap.get_operation_result.table)
559542
true
560543
end

0 commit comments

Comments
 (0)