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/msf/ui/console/command_dispatcher/creds.rb
Views: 11788
1
# -*- coding: binary -*-
2
3
require 'rexml/document'
4
require 'metasploit/framework/password_crackers/hashcat/formatter'
5
require 'metasploit/framework/password_crackers/jtr/formatter'
6
7
module Msf
8
module Ui
9
module Console
10
module CommandDispatcher
11
12
class Creds
13
require 'tempfile'
14
15
include Msf::Ui::Console::CommandDispatcher
16
include Metasploit::Credential::Creation
17
include Msf::Ui::Console::CommandDispatcher::Common
18
19
#
20
# The dispatcher's name.
21
#
22
def name
23
"Credentials Backend"
24
end
25
26
#
27
# Returns the hash of commands supported by this dispatcher.
28
#
29
def commands
30
{
31
"creds" => "List all credentials in the database"
32
}
33
end
34
35
def allowed_cred_types
36
%w(password ntlm hash KrbEncKey) + Metasploit::Credential::NonreplayableHash::VALID_JTR_FORMATS
37
end
38
39
#
40
# Returns true if the db is connected, prints an error and returns
41
# false if not.
42
#
43
# All commands that require an active database should call this before
44
# doing anything.
45
# TODO: abstract the db methods to a mixin that can be used by both dispatchers
46
#
47
def active?
48
if not framework.db.active
49
print_error("Database not connected")
50
return false
51
end
52
true
53
end
54
55
#
56
# Miscellaneous option helpers
57
#
58
59
#
60
# Can return return active or all, on a certain host or range, on a
61
# certain port or range, and/or on a service name.
62
#
63
def cmd_creds(*args)
64
return unless active?
65
66
# Short-circuit help
67
if args.delete("-h") || args.delete("--help")
68
cmd_creds_help
69
return
70
end
71
72
subcommand = args.shift
73
74
case subcommand
75
when 'help'
76
cmd_creds_help
77
when 'add'
78
creds_add(*args)
79
else
80
# then it's not actually a subcommand
81
args.unshift(subcommand) if subcommand
82
creds_search(*args)
83
end
84
85
end
86
87
#
88
# TODO: this needs to be cleaned up to use the new syntax
89
#
90
def cmd_creds_help
91
print_line
92
print_line "With no sub-command, list credentials. If an address range is"
93
print_line "given, show only credentials with logins on hosts within that"
94
print_line "range."
95
96
print_line
97
print_line "Usage - Listing credentials:"
98
print_line " creds [filter options] [address range]"
99
print_line
100
print_line "Usage - Adding credentials:"
101
print_line " creds add uses the following named parameters."
102
{
103
user: 'Public, usually a username',
104
password: 'Private, private_type Password.',
105
ntlm: 'Private, private_type NTLM Hash.',
106
postgres: 'Private, private_type postgres MD5',
107
pkcs12: 'Private, private_type pkcs12 archive file, must be a file path.',
108
'ssh-key' => 'Private, private_type SSH key, must be a file path.',
109
hash: 'Private, private_type Nonreplayable hash',
110
jtr: 'Private, private_type John the Ripper hash type.',
111
realm: 'Realm, ',
112
'realm-type'=>"Realm, realm_type (#{Metasploit::Model::Realm::Key::SHORT_NAMES.keys.join(' ')}), defaults to domain."
113
}.each_pair do |keyword, description|
114
print_line " #{keyword.to_s.ljust 10}: #{description}"
115
end
116
print_line
117
print_line "Examples: Adding"
118
print_line " # Add a user, password and realm"
119
print_line " creds add user:admin password:notpassword realm:workgroup"
120
print_line " # Add a user and password"
121
print_line " creds add user:guest password:'guest password'"
122
print_line " # Add a password"
123
print_line " creds add password:'password without username'"
124
print_line " # Add a user with an NTLMHash"
125
print_line " creds add user:admin ntlm:E2FC15074BF7751DD408E6B105741864:A1074A69B1BDE45403AB680504BBDD1A"
126
print_line " # Add a NTLMHash"
127
print_line " creds add ntlm:E2FC15074BF7751DD408E6B105741864:A1074A69B1BDE45403AB680504BBDD1A"
128
print_line " # Add a Postgres MD5"
129
print_line " creds add user:postgres postgres:md5be86a79bf2043622d58d5453c47d4860"
130
print_line " # Add a user with a PKCS12 file archive"
131
print_line " creds add user:alice pkcs12:/path/to/certificate.pfx"
132
print_line " # Add a user with an SSH key"
133
print_line " creds add user:sshadmin ssh-key:/path/to/id_rsa"
134
print_line " # Add a user and a NonReplayableHash"
135
print_line " creds add user:other hash:d19c32489b870735b5f587d76b934283 jtr:md5"
136
print_line " # Add a NonReplayableHash"
137
print_line " creds add hash:d19c32489b870735b5f587d76b934283"
138
139
print_line
140
print_line "General options"
141
print_line " -h,--help Show this help information"
142
print_line " -o <file> Send output to a file in csv/jtr (john the ripper) format."
143
print_line " If file name ends in '.jtr', that format will be used."
144
print_line " If file name ends in '.hcat', the hashcat format will be used."
145
print_line " csv by default."
146
print_line " -d,--delete Delete one or more credentials"
147
print_line
148
print_line "Filter options for listing"
149
print_line " -P,--password <text> List passwords that match this text"
150
print_line " -p,--port <portspec> List creds with logins on services matching this port spec"
151
print_line " -s <svc names> List creds matching comma-separated service names"
152
print_line " -u,--user <text> List users that match this text"
153
print_line " -t,--type <type> List creds of the specified type: password, ntlm, hash or any valid JtR format"
154
print_line " -O,--origins <IP> List creds that match these origins"
155
print_line " -r,--realm <realm> List creds that match this realm"
156
print_line " -R,--rhosts Set RHOSTS from the results of the search"
157
print_line " -v,--verbose Don't truncate long password hashes"
158
159
print_line
160
print_line "Examples, John the Ripper hash types:"
161
print_line " Operating Systems (starts with)"
162
print_line " Blowfish ($2a$) : bf"
163
print_line " BSDi (_) : bsdi"
164
print_line " DES : des,crypt"
165
print_line " MD5 ($1$) : md5"
166
print_line " SHA256 ($5$) : sha256,crypt"
167
print_line " SHA512 ($6$) : sha512,crypt"
168
print_line " Databases"
169
print_line " MSSQL : mssql"
170
print_line " MSSQL 2005 : mssql05"
171
print_line " MSSQL 2012/2014 : mssql12"
172
print_line " MySQL < 4.1 : mysql"
173
print_line " MySQL >= 4.1 : mysql-sha1"
174
print_line " Oracle : des,oracle"
175
print_line " Oracle 11 : raw-sha1,oracle11"
176
print_line " Oracle 11 (H type): dynamic_1506"
177
print_line " Oracle 12c : oracle12c"
178
print_line " Postgres : postgres,raw-md5"
179
180
print_line
181
print_line "Examples, listing:"
182
print_line " creds # Default, returns all credentials"
183
print_line " creds 1.2.3.4/24 # Return credentials with logins in this range"
184
print_line " creds -O 1.2.3.4/24 # Return credentials with origins in this range"
185
print_line " creds -p 22-25,445 # nmap port specification"
186
print_line " creds -s ssh,smb # All creds associated with a login on SSH or SMB services"
187
print_line " creds -t ntlm # All NTLM creds"
188
print_line
189
190
print_line "Example, deleting:"
191
print_line " # Delete all SMB credentials"
192
print_line " creds -d -s smb"
193
print_line
194
end
195
196
# @param private_type [Symbol] See `Metasploit::Credential::Creation#create_credential`
197
# @param username [String]
198
# @param password [String]
199
# @param realm [String]
200
# @param realm_type [String] A key in `Metasploit::Model::Realm::Key::SHORT_NAMES`
201
def creds_add(*args)
202
params = args.inject({}) do |hsh, n|
203
opt = n.split(':') # Splitting the string on colons.
204
hsh[opt[0]] = opt[1..-1].join(':') # everything before the first : is the key, reasembling everything after the colon. why ntlm hashes
205
hsh
206
end
207
208
begin
209
params.assert_valid_keys('user','password','realm','realm-type','ntlm','ssh-key','hash','address','port','protocol', 'service-name', 'jtr', 'pkcs12', 'postgres')
210
rescue ArgumentError => e
211
print_error(e.message)
212
end
213
214
# Verify we only have one type of private
215
if params.slice('password','ntlm','ssh-key','hash', 'pkcs12', 'postgres').length > 1
216
private_keys = params.slice('password','ntlm','ssh-key','hash', 'pkcs12', 'postgres').keys
217
print_error("You can only specify a single Private type. Private types given: #{private_keys.join(', ')}")
218
return
219
end
220
221
login_keys = params.slice('address','port','protocol','service-name')
222
if login_keys.any? and login_keys.length < 3
223
missing_login_keys = ['host','port','proto','service-name'] - login_keys.keys
224
print_error("Creating a login requires a address, a port, and a protocol. Missing params: #{missing_login_keys}")
225
return
226
end
227
228
data = {
229
workspace_id: framework.db.workspace.id,
230
origin_type: :import,
231
filename: 'msfconsole'
232
}
233
234
data[:username] = params['user'] if params.key? 'user'
235
236
if params.key? 'realm'
237
if params.key? 'realm-type'
238
if Metasploit::Model::Realm::Key::SHORT_NAMES.key? params['realm-type']
239
data[:realm_key] = Metasploit::Model::Realm::Key::SHORT_NAMES[params['realm-type']]
240
else
241
valid = Metasploit::Model::Realm::Key::SHORT_NAMES.keys.map{|n|"'#{n}'"}.join(", ")
242
print_error("Invalid realm type: #{params['realm_type']}. Valid Values: #{valid}")
243
end
244
else
245
data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
246
end
247
data[:realm_value] = params['realm']
248
end
249
250
if params.key? 'password'
251
data[:private_type] = :password
252
data[:private_data] = params['password']
253
end
254
255
if params.key? 'ntlm'
256
data[:private_type] = :ntlm_hash
257
data[:private_data] = params['ntlm']
258
end
259
260
if params.key? 'ssh-key'
261
begin
262
key_data = File.read(params['ssh-key'])
263
rescue ::Errno::EACCES, ::Errno::ENOENT => e
264
print_error("Failed to add ssh key: #{e}")
265
end
266
data[:private_type] = :ssh_key
267
data[:private_data] = key_data
268
end
269
270
if params.key? 'pkcs12'
271
begin
272
# pkcs12 is a binary format, but for persisting we Base64 encode it
273
pkcs12_data = Base64.strict_encode64(File.binread(params['pkcs12']))
274
rescue ::Errno::EACCES, ::Errno::ENOENT => e
275
print_error("Failed to add pkcs12 archive: #{e}")
276
end
277
data[:private_type] = :pkcs12
278
data[:private_data] = pkcs12_data
279
end
280
281
if params.key? 'hash'
282
data[:private_type] = :nonreplayable_hash
283
data[:private_data] = params['hash']
284
data[:jtr_format] = params['jtr'] if params.key? 'jtr'
285
end
286
287
if params.key? 'postgres'
288
data[:private_type] = :postgres_md5
289
if params['postgres'].downcase.start_with?('md5')
290
data[:private_data] = params['postgres']
291
data[:jtr_format] = 'postgres'
292
else
293
print_error("Postgres MD5 hashes should start with 'md5'")
294
end
295
end
296
297
begin
298
if login_keys.any?
299
data[:address] = params['address']
300
data[:port] = params['port']
301
data[:protocol] = params['protocol']
302
data[:service_name] = params['service-name']
303
framework.db.create_credential_and_login(data)
304
else
305
framework.db.create_credential(data)
306
end
307
rescue ActiveRecord::RecordInvalid => e
308
print_error("Failed to add #{data['private_type']}: #{e}")
309
end
310
end
311
312
def service_from_origin(core)
313
# Depending on the origin of the cred, there may or may not be a way to retrieve the associated service
314
case core.origin
315
when Metasploit::Credential::Origin::Service
316
return core.origin.service
317
end
318
end
319
320
def build_service_info(service)
321
if service.name.present?
322
info = "#{service.port}/#{service.proto} (#{service.name})"
323
else
324
info = "#{service.port}/#{service.proto}"
325
end
326
info
327
end
328
329
def creds_search(*args)
330
host_ranges = []
331
origin_ranges = []
332
port_ranges = []
333
svcs = []
334
rhosts = []
335
opts = {}
336
337
set_rhosts = false
338
truncate = true
339
340
cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type', 'JtR Format', 'cracked_password' ]
341
delete_count = 0
342
search_term = nil
343
344
while (arg = args.shift)
345
case arg
346
when '-o'
347
output_file = args.shift
348
if (!output_file)
349
print_error('Invalid output filename')
350
return
351
end
352
output_file = ::File.expand_path(output_file)
353
truncate = false
354
when '-p', '--port'
355
unless (arg_port_range(args.shift, port_ranges, true))
356
return
357
end
358
when '-t', '--type'
359
ptype = args.shift
360
opts[:ptype] = ptype
361
if (!ptype)
362
print_error('Argument required for -t')
363
return
364
end
365
when '-s', '--service'
366
service = args.shift
367
if (!service)
368
print_error('Argument required for -s')
369
return
370
end
371
svcs = service.split(/[\s]*,[\s]*/)
372
opts[:svcs] = svcs
373
when '-P', '--password'
374
if !(opts[:pass] = args.shift)
375
print_error('Argument required for -P')
376
return
377
end
378
when '-u', '--user'
379
if !(opts[:user] = args.shift)
380
print_error('Argument required for -u')
381
return
382
end
383
when '-d', '--delete'
384
mode = :delete
385
when '-R', '--rhosts'
386
set_rhosts = true
387
when '-O', '--origins'
388
hosts = args.shift
389
opts[:hosts] = hosts
390
if !hosts
391
print_error('Argument required for -O')
392
return
393
end
394
arg_host_range(hosts, origin_ranges)
395
when '-S', '--search-term'
396
search_term = args.shift
397
opts[:search_term] = search_term
398
when '-v', '--verbose'
399
truncate = false
400
when '-r', '--realm'
401
opts[:realm] = args.shift
402
else
403
# Anything that wasn't an option is a host to search for
404
unless (arg_host_range(arg, host_ranges))
405
return
406
end
407
end
408
end
409
410
# If we get here, we're searching. Delete implies search
411
412
if ptype
413
type = case ptype.downcase
414
when 'password'
415
Metasploit::Credential::Password
416
when 'hash'
417
Metasploit::Credential::PasswordHash
418
when 'ntlm'
419
Metasploit::Credential::NTLMHash
420
when 'KrbEncKey'.downcase
421
Metasploit::Credential::KrbEncKey
422
when *Metasploit::Credential::NonreplayableHash::VALID_JTR_FORMATS
423
opts[:jtr_format] = ptype
424
Metasploit::Credential::NonreplayableHash
425
else
426
print_error("Unrecognized credential type #{ptype} -- must be one of #{allowed_cred_types.join(',')}")
427
return
428
end
429
end
430
431
opts[:type] = type if type
432
433
# normalize
434
ports = port_ranges.flatten.uniq
435
opts[:ports] = ports unless ports.empty?
436
svcs.flatten!
437
tbl_opts = {
438
'Header' => "Credentials",
439
# For now, don't perform any word wrapping on the cred table as it breaks the workflow of
440
# copying credentials and pasting them into applications
441
'WordWrap' => false,
442
'Columns' => cred_table_columns,
443
'SearchTerm' => search_term
444
}
445
446
opts[:workspace] = framework.db.workspace
447
cred_cores = framework.db.creds(opts).to_a
448
cred_cores.sort_by!(&:id)
449
matched_cred_ids = []
450
cracked_cred_ids = []
451
452
if output_file&.ends_with?('.hcat')
453
output_file = ::File.open(output_file, 'wb')
454
output_formatter = Metasploit::Framework::PasswordCracker::Hashcat::Formatter.method(:hash_to_hashcat)
455
elsif output_file&.ends_with?('.jtr')
456
output_file = ::File.open(output_file, 'wb')
457
output_formatter = Metasploit::Framework::PasswordCracker::JtR::Formatter.method(:hash_to_jtr)
458
else
459
output_file = ::File.open(output_file, 'wb') unless output_file.blank?
460
tbl = Rex::Text::Table.new(tbl_opts)
461
end
462
463
filter_cred_cores(cred_cores, opts, origin_ranges, host_ranges) do |core, service, origin, cracked_password_core|
464
matched_cred_ids << core.id
465
cracked_cred_ids << cracked_password_core.id if cracked_password_core.present?
466
467
if output_file && output_formatter
468
formatted = output_formatter.call(core)
469
output_file.puts(formatted) unless formatted.blank?
470
end
471
472
unless tbl.nil?
473
public_val = core.public ? core.public.username : ''
474
if core.private
475
# Show the human readable description by default, unless the user ran with `--verbose` and wants to see the cred data
476
private_val = truncate ? core.private.to_s : core.private.data
477
else
478
private_val = ''
479
end
480
if truncate && private_val.to_s.length > 88
481
private_val = "#{private_val[0,76]} (TRUNCATED)"
482
end
483
realm_val = core.realm ? core.realm.value : ''
484
human_val = core.private ? core.private.class.model_name.human : ''
485
if human_val == ''
486
jtr_val = '' #11433, private can be nil
487
else
488
jtr_val = core.private.jtr_format ? core.private.jtr_format : ''
489
end
490
491
if service.nil?
492
host = ''
493
service_info = ''
494
else
495
host = service.host.address
496
rhosts << host unless host.blank?
497
service_info = build_service_info(service)
498
end
499
cracked_password_val = cracked_password_core&.private&.data.to_s
500
tbl << [
501
host,
502
origin,
503
service_info,
504
public_val,
505
private_val,
506
realm_val,
507
human_val, #private type
508
jtr_val,
509
cracked_password_val
510
]
511
end
512
end
513
514
if output_file.nil?
515
print_line(tbl.to_s)
516
else
517
output_file.write(tbl.to_csv) if output_formatter.nil?
518
output_file.close
519
print_status("Wrote creds to #{output_file.path}")
520
end
521
522
if mode == :delete
523
result = framework.db.delete_credentials(ids: matched_cred_ids.concat(cracked_cred_ids).uniq)
524
delete_count = result.size
525
end
526
527
# Finally, handle the case where the user wants the resulting list
528
# of hosts to go into RHOSTS.
529
set_rhosts_from_addrs(rhosts.uniq) if set_rhosts
530
print_status("Deleted #{delete_count} creds") if delete_count > 0
531
end
532
533
def cmd_creds_tabs(str, words)
534
case words.length
535
when 1
536
# subcommands
537
tabs = [ 'add-ntlm', 'add-password', 'add-hash', 'add-ssh-key', ]
538
when 2
539
tabs = if words[1] == 'add-ssh-key'
540
tab_complete_filenames(str, words)
541
else
542
[]
543
end
544
#when 5
545
# tabs = Metasploit::Model::Realm::Key::SHORT_NAMES.keys
546
else
547
tabs = []
548
end
549
return tabs
550
end
551
552
protected
553
554
# @param [Array<Metasploit::Credential::Core>] cores The list of cores to filter
555
# @param [Hash] opts
556
# @param [Array<Rex::Socket::RangeWalker>] origin_ranges
557
# @param [Array<Rex::Socket::RangeWalker>] host_ranges
558
# @yieldparam [Metasploit::Credential::Core] core
559
# @yieldparam [Mdm::Service] service
560
# @yieldparam [Metasploit::Credential::Origin] origin
561
# @yieldparam [Metasploit::Credential::Origin::CrackedPassword] cracked_password_core
562
def filter_cred_cores(cores, opts, origin_ranges, host_ranges)
563
# Some creds may have been cracked that exist outside of the filtered cores list, let's resolve them all to show the cracked value
564
cores_by_id = cores.each_with_object({}) { |core, hash| hash[core.id] = core }
565
# Map of any originating core ids that have been cracked; The value is cracked core value
566
cracked_core_id_to_cracked_value = cores.each_with_object({}) do |core, hash|
567
next unless core.origin.kind_of?(Metasploit::Credential::Origin::CrackedPassword)
568
hash[core.origin.metasploit_credential_core_id] = core
569
end
570
571
cores.each do |core|
572
# Skip the cracked password if it's planned to be shown on the originating core row in a separate column
573
is_duplicate_cracked_password_row = core.origin.kind_of?(Metasploit::Credential::Origin::CrackedPassword) &&
574
cracked_core_id_to_cracked_value.key?(core.origin.metasploit_credential_core_id) &&
575
# The core might exist outside of the currently available cores to render
576
cores_by_id.key?(core.origin.metasploit_credential_core_id)
577
next if is_duplicate_cracked_password_row
578
579
# Exclude non-blank username creds if that's what we're after
580
if opts[:user] == '' && core.public && !(core.public.username.blank?)
581
next
582
end
583
584
# Exclude non-blank password creds if that's what we're after
585
if opts[:pass] == '' && core.private && !(core.private.data.blank?)
586
next
587
end
588
589
origin = ''
590
if core.origin.kind_of?(Metasploit::Credential::Origin::Service)
591
service = framework.db.services(id: core.origin.service_id).first
592
origin = service.host.address
593
elsif core.origin.kind_of?(Metasploit::Credential::Origin::Session)
594
session = framework.db.sessions(id: core.origin.session_id).first
595
origin = session.host.address
596
end
597
598
if origin_ranges.present? && !origin_ranges.any? { |range| range.include?(origin) }
599
next
600
end
601
602
cracked_password_core = cracked_core_id_to_cracked_value.fetch(core.id, nil)
603
if core.logins.empty?
604
service = service_from_origin(core)
605
next if service.nil? && host_ranges.present? # If we're filtering by login IP and we're here there's no associated login, so skip
606
607
yield core, service, origin, cracked_password_core
608
else
609
core.logins.each do |login|
610
service = framework.db.services(id: login.service_id).first
611
# If none of this Core's associated Logins is for a host within
612
# the user-supplied RangeWalker, then we don't have any reason to
613
# print it out. However, we treat the absence of ranges as meaning
614
# all hosts.
615
if host_ranges.present? && !host_ranges.any? { |range| range.include?(service.host.address) }
616
next
617
end
618
619
yield core, service, origin, cracked_password_core
620
end
621
end
622
end
623
end
624
625
end
626
627
end end end end
628
629