Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/msf/ui/console/command_dispatcher/db.rb
32575 views
1
# -*- coding: binary -*-
2
3
require 'json'
4
require 'rexml/document'
5
require 'metasploit/framework/data_service'
6
require 'metasploit/framework/data_service/remote/http/core'
7
8
module Msf
9
module Ui
10
module Console
11
module CommandDispatcher
12
13
class Db
14
15
require 'tempfile'
16
17
include Msf::Ui::Console::CommandDispatcher
18
include Msf::Ui::Console::CommandDispatcher::Common
19
include Msf::Ui::Console::CommandDispatcher::Db::Common
20
include Msf::Ui::Console::CommandDispatcher::Db::Analyze
21
include Msf::Ui::Console::CommandDispatcher::Db::Klist
22
include Msf::Ui::Console::CommandDispatcher::Db::Certs
23
24
DB_CONFIG_PATH = 'framework/database'
25
26
#
27
# The dispatcher's name.
28
#
29
def name
30
"Database Backend"
31
end
32
33
#
34
# Returns the hash of commands supported by this dispatcher.
35
#
36
def commands
37
base = {
38
"db_connect" => "Connect to an existing data service",
39
"db_disconnect" => "Disconnect from the current data service",
40
"db_status" => "Show the current data service status",
41
"db_save" => "Save the current data service connection as the default to reconnect on startup",
42
"db_remove" => "Remove the saved data service entry"
43
}
44
45
more = {
46
"workspace" => "Switch between database workspaces",
47
"hosts" => "List all hosts in the database",
48
"services" => "List all services in the database",
49
"vulns" => "List all vulnerabilities in the database",
50
"notes" => "List all notes in the database",
51
"loot" => "List all loot in the database",
52
"klist" => "List Kerberos tickets in the database",
53
"certs" => "List Pkcs12 certificate bundles in the database",
54
"db_import" => "Import a scan result file (filetype will be auto-detected)",
55
"db_export" => "Export a file containing the contents of the database",
56
"db_nmap" => "Executes nmap and records the output automatically",
57
"db_rebuild_cache" => "Rebuilds the database-stored module cache (deprecated)",
58
"analyze" => "Analyze database information about a specific address or address range",
59
"db_stats" => "Show statistics for the database"
60
}
61
62
# Always include commands that only make sense when connected.
63
# This avoids the problem of them disappearing unexpectedly if the
64
# database dies or times out. See #1923
65
66
base.merge(more)
67
end
68
69
def deprecated_commands
70
[
71
"db_autopwn",
72
"db_driver",
73
"db_hosts",
74
"db_notes",
75
"db_services",
76
"db_vulns",
77
]
78
end
79
80
#
81
# Attempts to connect to the previously configured database, and additionally keeps track of
82
# the currently loaded data service.
83
#
84
def load_config(path = nil)
85
result = Msf::DbConnector.db_connect_from_config(framework, path)
86
87
if result[:error]
88
print_error(result[:error])
89
end
90
if result[:data_service_name]
91
@current_data_service = result[:data_service_name]
92
end
93
end
94
95
@@workspace_opts = Rex::Parser::Arguments.new(
96
[ '-h', '--help' ] => [ false, 'Help banner.'],
97
[ '-a', '--add' ] => [ true, 'Add a workspace.', '<name>'],
98
[ '-d', '--delete' ] => [ true, 'Delete a workspace.', '<name>'],
99
[ '-D', '--delete-all' ] => [ false, 'Delete all workspaces.'],
100
[ '-r', '--rename' ] => [ true, 'Rename a workspace.', '<old> <new>'],
101
[ '-l', '--list' ] => [ false, 'List workspaces.'],
102
[ '-v', '--list-verbose' ] => [ false, 'List workspaces verbosely.'],
103
[ '-S', '--search' ] => [ true, 'Search for a workspace.', '<name>']
104
)
105
106
def cmd_workspace_help
107
print_line "Usage:"
108
print_line " workspace List workspaces"
109
print_line " workspace [name] Switch workspace"
110
print_line @@workspace_opts.usage
111
end
112
113
def cmd_workspace(*args)
114
return unless active?
115
116
state = :nil
117
118
list = false
119
verbose = false
120
names = []
121
search_term = nil
122
123
@@workspace_opts.parse(args) do |opt, idx, val|
124
case opt
125
when '-h', '--help'
126
cmd_workspace_help
127
return
128
when '-a', '--add'
129
return cmd_workspace_help unless state == :nil
130
131
state = :adding
132
names << val if !val.nil?
133
when '-d', '--del'
134
return cmd_workspace_help unless state == :nil
135
136
state = :deleting
137
names << val if !val.nil?
138
when '-D', '--delete-all'
139
return cmd_workspace_help unless state == :nil
140
141
state = :delete_all
142
when '-r', '--rename'
143
return cmd_workspace_help unless state == :nil
144
145
state = :renaming
146
names << val if !val.nil?
147
when '-v', '--verbose'
148
verbose = true
149
when '-l', '--list'
150
list = true
151
when '-S', '--search'
152
search_term = val
153
else
154
names << val if !val.nil?
155
end
156
end
157
158
if state == :adding and names
159
# Add workspaces
160
wspace = nil
161
names.each do |name|
162
wspace = framework.db.workspaces(name: name).first
163
if wspace
164
print_status("Workspace '#{wspace.name}' already existed, switching to it.")
165
else
166
wspace = framework.db.add_workspace(name)
167
print_status("Added workspace: #{wspace.name}")
168
end
169
end
170
framework.db.workspace = wspace
171
print_status("Workspace: #{framework.db.workspace.name}")
172
elsif state == :deleting and names
173
ws_ids_to_delete = []
174
starting_ws = framework.db.workspace
175
names.uniq.each do |n|
176
ws = framework.db.workspaces(name: n).first
177
ws_ids_to_delete << ws.id if ws
178
end
179
if ws_ids_to_delete.count > 0
180
deleted = framework.db.delete_workspaces(ids: ws_ids_to_delete)
181
process_deleted_workspaces(deleted, starting_ws)
182
else
183
print_status("No workspaces matching the given name(s) were found.")
184
end
185
elsif state == :delete_all
186
ws_ids_to_delete = []
187
starting_ws = framework.db.workspace
188
framework.db.workspaces.each do |ws|
189
ws_ids_to_delete << ws.id
190
end
191
deleted = framework.db.delete_workspaces(ids: ws_ids_to_delete)
192
process_deleted_workspaces(deleted, starting_ws)
193
elsif state == :renaming
194
if names.length != 2
195
print_error("Wrong number of arguments to rename")
196
return
197
end
198
199
ws_to_update = framework.db.find_workspace(names.first)
200
unless ws_to_update
201
print_error("Workspace '#{names.first}' does not exist")
202
return
203
end
204
opts = {
205
id: ws_to_update.id,
206
name: names.last
207
}
208
begin
209
updated_ws = framework.db.update_workspace(opts)
210
if updated_ws
211
framework.db.workspace = updated_ws if names.first == framework.db.workspace.name
212
print_status("Renamed workspace '#{names.first}' to '#{updated_ws.name}'")
213
else
214
print_error "There was a problem updating the workspace. Setting to the default workspace."
215
framework.db.workspace = framework.db.default_workspace
216
return
217
end
218
if names.first == Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME
219
print_status("Recreated default workspace")
220
end
221
rescue => e
222
print_error "Failed to rename workspace: #{e.message}"
223
end
224
225
elsif !names.empty?
226
name = names.last
227
# Switch workspace
228
workspace = framework.db.find_workspace(name)
229
if workspace
230
framework.db.workspace = workspace
231
print_status("Workspace: #{workspace.name}")
232
else
233
print_error("Workspace not found: #{name}")
234
return
235
end
236
else
237
current_workspace = framework.db.workspace
238
239
unless verbose
240
current = nil
241
framework.db.workspaces.sort_by {|s| s.name}.each do |s|
242
if s.name == current_workspace.name
243
current = s.name
244
else
245
print_line(" #{s.name}")
246
end
247
end
248
print_line("%red* #{current}%clr") unless current.nil?
249
return
250
end
251
col_names = %w{current name hosts services vulns creds loots notes}
252
253
tbl = Rex::Text::Table.new(
254
'Header' => 'Workspaces',
255
'Columns' => col_names,
256
'SortIndex' => -1,
257
'SearchTerm' => search_term
258
)
259
260
framework.db.workspaces.each do |ws|
261
tbl << [
262
current_workspace.name == ws.name ? '*' : '',
263
ws.name,
264
framework.db.hosts(workspace: ws.name).count,
265
framework.db.services(workspace: ws.name).count,
266
framework.db.vulns(workspace: ws.name).count,
267
framework.db.creds(workspace: ws.name).count,
268
framework.db.loots(workspace: ws.name).count,
269
framework.db.notes(workspace: ws.name).count
270
]
271
end
272
273
print_line
274
print_line(tbl.to_s)
275
end
276
end
277
278
def process_deleted_workspaces(deleted_workspaces, starting_ws)
279
deleted_workspaces.each do |ws|
280
print_status "Deleted workspace: #{ws.name}"
281
if ws.name == Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME
282
framework.db.workspace = framework.db.default_workspace
283
print_status 'Recreated the default workspace'
284
elsif ws == starting_ws
285
framework.db.workspace = framework.db.default_workspace
286
print_status "Switched to workspace: #{framework.db.workspace.name}"
287
end
288
end
289
end
290
291
def cmd_workspace_tabs(str, words)
292
return [] unless active?
293
framework.db.workspaces.map(&:name) if (words & ['-a','--add']).empty?
294
end
295
296
#
297
# Tab completion for the hosts command
298
#
299
# @param str [String] the string currently being typed before tab was hit
300
# @param words [Array<String>] the previously completed words on the command line. words is always
301
# at least 1 when tab completion has reached this stage since the command itself has been completed
302
def cmd_hosts_tabs(str, words)
303
if words.length == 1
304
return @@hosts_opts.option_keys.select { |opt| opt.start_with?(str) }
305
end
306
307
case words[-1]
308
when '-c', '--columns', '-C', '--columns-until-restart'
309
return @@hosts_columns
310
when '-o', '--output'
311
return tab_complete_filenames(str, words)
312
end
313
314
if @@hosts_opts.arg_required?(words[-1])
315
return []
316
end
317
318
return @@hosts_opts.option_keys.select { |opt| opt.start_with?(str) }
319
end
320
321
def cmd_hosts_help
322
# This command does some lookups for the list of appropriate column
323
# names, so instead of putting all the usage stuff here like other
324
# help methods, just use it's "-h" so we don't have to recreating
325
# that list
326
cmd_hosts("-h")
327
end
328
329
# Changes the specified host data
330
#
331
# @param host_ranges - range of hosts to process
332
# @param host_data - hash of host data to be updated
333
def change_host_data(host_ranges, host_data)
334
if !host_data || host_data.length != 1
335
print_error("A single key-value data hash is required to change the host data")
336
return
337
end
338
attribute = host_data.keys[0]
339
340
if host_ranges == [nil]
341
print_error("In order to change the host #{attribute}, you must provide a range of hosts")
342
return
343
end
344
345
each_host_range_chunk(host_ranges) do |host_search|
346
next if host_search && host_search.empty?
347
348
framework.db.hosts(address: host_search).each do |host|
349
framework.db.update_host(host_data.merge(id: host.id))
350
framework.db.report_note(host: host.address, type: "host.#{attribute}", data: { :host_data => host_data[attribute] })
351
end
352
end
353
end
354
355
def add_host_tag(rws, tag_name)
356
if rws == [nil]
357
print_error("In order to add a tag, you must provide a range of hosts")
358
return
359
end
360
361
opts = Hash.new()
362
opts[:workspace] = framework.db.workspace
363
opts[:tag_name] = tag_name
364
365
rws.each do |rw|
366
rw.each do |ip|
367
opts[:address] = ip
368
unless framework.db.add_host_tag(opts)
369
print_error("Host #{ip} could not be found.")
370
end
371
end
372
end
373
end
374
375
def find_host_tags(workspace, host_id)
376
opts = Hash.new()
377
opts[:workspace] = workspace
378
opts[:id] = host_id
379
380
framework.db.get_host_tags(opts)
381
end
382
383
def delete_host_tag(rws, tag_name)
384
opts = Hash.new()
385
opts[:workspace] = framework.db.workspace
386
opts[:tag_name] = tag_name
387
388
# This will be the case if no IP was passed in, and we are just trying to delete all
389
# instances of a given tag within the database.
390
if rws == [nil]
391
wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
392
wspace.hosts.each do |host|
393
opts[:address] = host.address
394
framework.db.delete_host_tag(opts)
395
end
396
else
397
rws.each do |rw|
398
rw.each do |ip|
399
opts[:address] = ip
400
unless framework.db.delete_host_tag(opts)
401
print_error("Host #{ip} could not be found.")
402
end
403
end
404
end
405
end
406
end
407
408
@@hosts_columns = [ 'address', 'mac', 'name', 'os_name', 'os_flavor', 'os_sp', 'purpose', 'info', 'comments']
409
410
@@hosts_opts = Rex::Parser::Arguments.new(
411
[ '-h', '--help' ] => [ false, 'Show this help information' ],
412
[ '-a', '--add' ] => [ true, 'Add the hosts instead of searching', '<host>' ],
413
[ '-u', '--up' ] => [ false, 'Only show hosts which are up' ],
414
[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search' ],
415
[ '-S', '--search' ] => [ true, 'Search string to filter by', '<filter>' ],
416
[ '-i', '--info' ] => [ true, 'Change the info of a host', '<info>' ],
417
[ '-n', '--name' ] => [ true, 'Change the name of a host', '<name>' ],
418
[ '-m', '--comment' ] => [ true, 'Change the comment of a host', '<comment>' ],
419
[ '-t', '--tag' ] => [ true, 'Add or specify a tag to a range of hosts', '<tag>' ],
420
[ '-T', '--delete-tag' ] => [ true, 'Remove a tag from a range of hosts', '<tag>' ],
421
[ '-d', '--delete' ] => [ true, 'Delete the hosts instead of searching', '<hosts>' ],
422
[ '-o', '--output' ] => [ true, 'Send output to a file in csv format', '<filename>' ],
423
[ '-O', '--order' ] => [ true, 'Order rows by specified column number', '<column id>' ],
424
[ '-c', '--columns' ] => [ true, 'Only show the given columns (see list below)', '<columns>' ],
425
[ '-C', '--columns-until-restart' ] => [ true, 'Only show the given columns until the next restart (see list below)', '<columns>' ],
426
)
427
428
def cmd_hosts(*args)
429
return unless active?
430
onlyup = false
431
set_rhosts = false
432
mode = []
433
delete_count = 0
434
435
rhosts = []
436
host_ranges = []
437
search_term = nil
438
439
order_by = nil
440
info_data = nil
441
name_data = nil
442
comment_data = nil
443
tag_name = nil
444
445
output = nil
446
default_columns = [
447
'address',
448
'arch',
449
'comm',
450
'comments',
451
'created_at',
452
'cred_count',
453
'detected_arch',
454
'exploit_attempt_count',
455
'host_detail_count',
456
'info',
457
'mac',
458
'name',
459
'note_count',
460
'os_family',
461
'os_flavor',
462
'os_lang',
463
'os_name',
464
'os_sp',
465
'purpose',
466
'scope',
467
'service_count',
468
'state',
469
'updated_at',
470
'virtual_host',
471
'vuln_count',
472
'workspace_id']
473
474
default_columns << 'tags' # Special case
475
virtual_columns = [ 'svcs', 'vulns', 'workspace', 'tags' ]
476
477
col_search = @@hosts_columns
478
479
default_columns.delete_if {|v| (v[-2,2] == "id")}
480
@@hosts_opts.parse(args) do |opt, idx, val|
481
case opt
482
when '-h', '--help'
483
print_line "Usage: hosts [ options ] [addr1 addr2 ...]"
484
print_line
485
print @@hosts_opts.usage
486
print_line
487
print_line "Available columns: #{default_columns.join(", ")}"
488
print_line
489
return
490
when '-a', '--add'
491
mode << :add
492
arg_host_range(val, host_ranges)
493
when '-d', '--delete'
494
mode << :delete
495
arg_host_range(val, host_ranges)
496
when '-u', '--up'
497
onlyup = true
498
when '-o'
499
output = val
500
output = ::File.expand_path(output)
501
when '-R', '--rhosts'
502
set_rhosts = true
503
when '-S', '--search'
504
search_term = val
505
when '-i', '--info'
506
mode << :new_info
507
info_data = val
508
when '-n', '--name'
509
mode << :new_name
510
name_data = val
511
when '-m', '--comment'
512
mode << :new_comment
513
comment_data = val
514
when '-t', '--tag'
515
mode << :tag
516
tag_name = val
517
when '-T', '--delete-tag'
518
mode << :delete_tag
519
tag_name = val
520
when '-c', '-C'
521
list = val
522
if(!list)
523
print_error("Invalid column list")
524
return
525
end
526
col_search = list.strip().split(",")
527
col_search.each { |c|
528
if not default_columns.include?(c) and not virtual_columns.include?(c)
529
all_columns = default_columns + virtual_columns
530
print_error("Invalid column list. Possible values are (#{all_columns.join("|")})")
531
return
532
end
533
}
534
if opt == '-C'
535
@@hosts_columns = col_search
536
end
537
when '-O'
538
if (order_by = val.to_i - 1) < 0
539
print_error('Please specify a column number starting from 1')
540
return
541
end
542
else
543
# Anything that wasn't an option is a host to search for
544
unless (arg_host_range(val, host_ranges))
545
return
546
end
547
end
548
end
549
550
if col_search
551
col_names = col_search
552
else
553
col_names = default_columns + virtual_columns
554
end
555
556
mode << :search if mode.empty?
557
558
if mode == [:add]
559
host_ranges.each do |range|
560
range.each do |address|
561
host = framework.db.find_or_create_host(:host => address)
562
print_status("Time: #{host.created_at} Host: host=#{host.address}")
563
end
564
end
565
return
566
end
567
568
cp_hsh = {}
569
col_names.map do |col|
570
cp_hsh[col] = { 'MaxChar' => 52 }
571
end
572
# If we got here, we're searching. Delete implies search
573
tbl = Rex::Text::Table.new(
574
{
575
'Header' => "Hosts",
576
'Columns' => col_names,
577
'ColProps' => cp_hsh,
578
'SortIndex' => order_by
579
})
580
581
# Sentinel value meaning all
582
host_ranges.push(nil) if host_ranges.empty?
583
584
case
585
when mode == [:new_info]
586
change_host_data(host_ranges, info: info_data)
587
return
588
when mode == [:new_name]
589
change_host_data(host_ranges, name: name_data)
590
return
591
when mode == [:new_comment]
592
change_host_data(host_ranges, comments: comment_data)
593
return
594
when mode == [:tag]
595
begin
596
add_host_tag(host_ranges, tag_name)
597
rescue => e
598
if e.message.include?('Validation failed')
599
print_error(e.message)
600
else
601
raise e
602
end
603
end
604
return
605
when mode == [:delete_tag]
606
begin
607
delete_host_tag(host_ranges, tag_name)
608
rescue => e
609
if e.message.include?('Validation failed')
610
print_error(e.message)
611
else
612
raise e
613
end
614
end
615
return
616
end
617
618
matched_host_ids = []
619
each_host_range_chunk(host_ranges) do |host_search|
620
next if host_search && host_search.empty?
621
622
framework.db.hosts(address: host_search, non_dead: onlyup, search_term: search_term).each do |host|
623
matched_host_ids << host.id
624
columns = col_names.map do |n|
625
# Deal with the special cases
626
if virtual_columns.include?(n)
627
case n
628
when "svcs"; host.service_count
629
when "vulns"; host.vuln_count
630
when "workspace"; host.workspace.name
631
when "tags"
632
found_tags = find_host_tags(framework.db.workspace, host.id)
633
tag_names = found_tags.map(&:name).join(', ')
634
tag_names
635
end
636
# Otherwise, it's just an attribute
637
else
638
host[n] || ""
639
end
640
end
641
642
tbl << columns
643
if set_rhosts
644
addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address)
645
rhosts << addr
646
end
647
end
648
649
if mode == [:delete]
650
result = framework.db.delete_host(ids: matched_host_ids)
651
delete_count += result.size
652
end
653
end
654
655
if output
656
print_status("Wrote hosts to #{output}")
657
::File.open(output, "wb") { |ofd|
658
ofd.write(tbl.to_csv)
659
}
660
else
661
print_line
662
print_line(tbl.to_s)
663
end
664
665
# Finally, handle the case where the user wants the resulting list
666
# of hosts to go into RHOSTS.
667
set_rhosts_from_addrs(rhosts.uniq) if set_rhosts
668
669
print_status("Deleted #{delete_count} hosts") if delete_count > 0
670
end
671
672
#
673
# Tab completion for the services command
674
#
675
# @param str [String] the string currently being typed before tab was hit
676
# @param words [Array<String>] the previously completed words on the command line. words is always
677
# at least 1 when tab completion has reached this stage since the command itself has been completed
678
def cmd_services_tabs(str, words)
679
if words.length == 1
680
return @@services_opts.option_keys.select { |opt| opt.start_with?(str) }
681
end
682
683
case words[-1]
684
when '-c', '--column'
685
return @@services_columns
686
when '-O', '--order'
687
return []
688
when '-o', '--output'
689
return tab_complete_filenames(str, words)
690
when '-p', '--port'
691
return []
692
when '-r', '--protocol'
693
return []
694
end
695
696
[]
697
end
698
699
def cmd_services_help
700
print_line "Usage: services [-h] [-u] [-a] [-r <proto>] [-p <port1,port2>] [-s <name1,name2>] [-o <filename>] [addr1 addr2 ...]"
701
print_line
702
print @@services_opts.usage
703
print_line
704
print_line "Available columns: #{@@services_columns.join(", ")}"
705
print_line
706
end
707
708
@@services_columns = [ 'created_at', 'info', 'name', 'port', 'proto', 'state', 'updated_at' ]
709
710
@@services_opts = Rex::Parser::Arguments.new(
711
[ '-a', '--add' ] => [ false, 'Add the services instead of searching.' ],
712
[ '-d', '--delete' ] => [ false, 'Delete the services instead of searching.' ],
713
[ '-U', '--update' ] => [ false, 'Update data for existing service.' ],
714
[ '-u', '--up' ] => [ false, 'Only show services which are up.' ],
715
[ '-c', '--column' ] => [ true, 'Only show the given columns.', '<col1,col2>' ],
716
[ '-p', '--port' ] => [ true, 'Search for a list of ports.', '<ports>' ],
717
[ '-r', '--protocol' ] => [ true, 'Protocol type of the service being added [tcp|udp].', '<protocol>' ],
718
[ '-s', '--name' ] => [ true, 'Name of the service to add.', '<name>' ],
719
[ '-o', '--output' ] => [ true, 'Send output to a file in csv format.', '<filename>' ],
720
[ '-O', '--order' ] => [ true, 'Order rows by specified column number.', '<column id>' ],
721
[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],
722
[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],
723
[ '-h', '--help' ] => [ false, 'Show this help information.' ]
724
)
725
726
def db_connection_info(framework)
727
unless framework.db.connection_established?
728
return "#{framework.db.driver} selected, no connection"
729
end
730
731
cdb = ''
732
if framework.db.driver == 'http'
733
cdb = framework.db.name
734
else
735
::ApplicationRecord.connection_pool.with_connection do |conn|
736
if conn.respond_to?(:current_database)
737
cdb = conn.current_database
738
end
739
end
740
end
741
742
if cdb.empty?
743
output = "Connected Database Name could not be extracted. DB Connection type: #{framework.db.driver}."
744
else
745
output = "Connected to #{cdb}. Connection type: #{framework.db.driver}."
746
end
747
748
output
749
end
750
751
def cmd_db_stats(*args)
752
return unless active?
753
print_line "Session Type: #{db_connection_info(framework)}"
754
755
current_workspace = framework.db.workspace
756
example_workspaces = ::Mdm::Workspace.order(id: :desc)
757
ordered_workspaces = ([current_workspace] + example_workspaces).uniq.sort_by(&:id)
758
759
tbl = Rex::Text::Table.new(
760
'Indent' => 2,
761
'Header' => "Database Stats",
762
'Columns' =>
763
[
764
"IsTarget",
765
"ID",
766
"Name",
767
"Hosts",
768
"Services",
769
"Services per Host",
770
"Vulnerabilities",
771
"Vulns per Host",
772
"Notes",
773
"Creds",
774
"Kerberos Cache"
775
],
776
'SortIndex' => 1,
777
'ColProps' => {
778
'IsTarget' => {
779
'Stylers' => [Msf::Ui::Console::TablePrint::RowIndicatorStyler.new],
780
'ColumnStylers' => [Msf::Ui::Console::TablePrint::OmitColumnHeader.new],
781
'Width' => 2
782
}
783
}
784
)
785
786
total_hosts = 0
787
total_services = 0
788
total_vulns = 0
789
total_notes = 0
790
total_creds = 0
791
total_tickets = 0
792
793
ordered_workspaces.map do |workspace|
794
795
hosts = workspace.hosts.count
796
services = workspace.services.count
797
vulns = workspace.vulns.count
798
notes = workspace.notes.count
799
creds = framework.db.creds(workspace: workspace.name).count # workspace.creds.count.to_fs(:delimited) is always 0 for whatever reason
800
kerbs = ticket_search([nil], nil, :workspace => workspace).count
801
802
total_hosts += hosts
803
total_services += services
804
total_vulns += vulns
805
total_notes += notes
806
total_creds += creds
807
total_tickets += kerbs
808
809
tbl << [
810
current_workspace.id == workspace.id,
811
workspace.id,
812
workspace.name,
813
hosts.to_fs(:delimited),
814
services.to_fs(:delimited),
815
hosts > 0 ? (services.to_f / hosts).truncate(2) : 0,
816
vulns.to_fs(:delimited),
817
hosts > 0 ? (vulns.to_f / hosts).truncate(2) : 0,
818
notes.to_fs(:delimited),
819
creds.to_fs(:delimited),
820
kerbs.to_fs(:delimited)
821
]
822
end
823
824
# total row
825
tbl << [
826
"",
827
"Total",
828
ordered_workspaces.length.to_fs(:delimited),
829
total_hosts.to_fs(:delimited),
830
total_services.to_fs(:delimited),
831
total_hosts > 0 ? (total_services.to_f / total_hosts).truncate(2) : 0,
832
total_vulns,
833
total_hosts > 0 ? (total_vulns.to_f / total_hosts).truncate(2) : 0,
834
total_notes,
835
total_creds.to_fs(:delimited),
836
total_tickets.to_fs(:delimited)
837
]
838
839
print_line tbl.to_s
840
end
841
842
def cmd_services(*args)
843
return unless active?
844
mode = :search
845
onlyup = false
846
output_file = nil
847
set_rhosts = false
848
col_search = ['port', 'proto', 'name', 'state', 'info']
849
extra_columns = ['resource', 'parents']
850
851
names = nil
852
order_by = nil
853
proto = nil
854
host_ranges = []
855
port_ranges = []
856
rhosts = []
857
delete_count = 0
858
search_term = nil
859
opts = {}
860
861
@@services_opts.parse(args) do |opt, idx, val|
862
case opt
863
when '-a', '--add'
864
mode = :add
865
when '-d', '--delete'
866
mode = :delete
867
when '-U', '--update'
868
mode = :update
869
when '-u', '--up'
870
onlyup = true
871
when '-c'
872
list = val
873
if(!list)
874
print_error("Invalid column list")
875
return
876
end
877
col_search = list.strip().split(",")
878
col_search.each { |c|
879
if not @@services_columns.include? c
880
print_error("Invalid column list. Possible values are (#{@@services_columns.join("|")})")
881
return
882
end
883
}
884
when '-p'
885
unless (arg_port_range(val, port_ranges, true))
886
return
887
end
888
when '-r'
889
proto = val
890
if (!proto)
891
print_status("Invalid protocol")
892
return
893
end
894
proto = proto.strip
895
when '-s'
896
namelist = val
897
if (!namelist)
898
print_error("Invalid name list")
899
return
900
end
901
names = namelist.strip().split(",")
902
when '-o'
903
output_file = val
904
if (!output_file)
905
print_error("Invalid output filename")
906
return
907
end
908
output_file = ::File.expand_path(output_file)
909
when '-O'
910
if (order_by = val.to_i - 1) < 0
911
print_error('Please specify a column number starting from 1')
912
return
913
end
914
when '-R', '--rhosts'
915
set_rhosts = true
916
when '-S', '--search'
917
search_term = val
918
opts[:search_term] = search_term
919
when '-h', '--help'
920
cmd_services_help
921
return
922
else
923
# Anything that wasn't an option is a host to search for
924
unless (arg_host_range(val, host_ranges))
925
return
926
end
927
end
928
end
929
930
ports = port_ranges.flatten.uniq
931
932
if mode == :add
933
# Can only deal with one port and one service name at a time
934
# right now. Them's the breaks.
935
if ports.length != 1
936
print_error("Exactly one port required")
937
return
938
end
939
if host_ranges.empty?
940
print_error("Host address or range required")
941
return
942
end
943
host_ranges.each do |range|
944
range.each do |addr|
945
info = {
946
:host => addr,
947
:port => ports.first.to_i
948
}
949
info[:proto] = proto.downcase if proto
950
info[:name] = names.first.downcase if names and names.first
951
952
svc = framework.db.find_or_create_service(info)
953
print_status("Time: #{svc.created_at} Service: host=#{svc.host.address} port=#{svc.port} proto=#{svc.proto} name=#{svc.name}")
954
end
955
end
956
return
957
end
958
959
# If we got here, we're searching. Delete implies search
960
col_names = @@services_columns
961
if col_search
962
col_names = col_search
963
end
964
tbl = Rex::Text::Table.new({
965
'Header' => "Services",
966
'Columns' => extra_columns.empty? ? (['host'] + col_names) : (['host'] + col_names + extra_columns),
967
'SortIndex' => order_by
968
})
969
970
# Sentinel value meaning all
971
host_ranges.push(nil) if host_ranges.empty?
972
ports = nil if ports.empty?
973
matched_service_ids = []
974
975
each_host_range_chunk(host_ranges) do |host_search|
976
next if host_search && host_search.empty?
977
opts[:workspace] = framework.db.workspace
978
opts[:hosts] = {address: host_search} if !host_search.nil?
979
opts[:port] = ports if ports
980
opts[:name] = names if names
981
framework.db.services(opts).each do |service|
982
983
unless service.state == 'open'
984
next if onlyup
985
end
986
987
host = service.host
988
matched_service_ids << service.id
989
990
if mode == :update
991
service.name = names.first if names
992
service.proto = proto if proto
993
service.port = ports.first if ports
994
framework.db.update_service(service.as_json.symbolize_keys)
995
end
996
997
columns = [host.address] + col_names.map { |n| service[n].to_s || "" }
998
unless extra_columns.empty?
999
columns << service.resource.to_json
1000
columns << service.parents.map { |parent| "#{parent.name} (#{parent.port}/#{parent.proto})"}.join(', ')
1001
end
1002
tbl << columns
1003
if set_rhosts
1004
addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address )
1005
rhosts << addr
1006
end
1007
end
1008
end
1009
1010
if (mode == :delete)
1011
result = framework.db.delete_service(ids: matched_service_ids)
1012
delete_count += result.size
1013
end
1014
1015
if (output_file == nil)
1016
print_line(tbl.to_s)
1017
else
1018
# create the output file
1019
::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) }
1020
print_status("Wrote services to #{output_file}")
1021
end
1022
1023
# Finally, handle the case where the user wants the resulting list
1024
# of hosts to go into RHOSTS.
1025
set_rhosts_from_addrs(rhosts.uniq) if set_rhosts
1026
1027
print_status("Deleted #{delete_count} services") if delete_count > 0
1028
1029
end
1030
1031
#
1032
# Tab completion for the vulns command
1033
#
1034
# @param str [String] the string currently being typed before tab was hit
1035
# @param words [Array<String>] the previously completed words on the command line. words is always
1036
# at least 1 when tab completion has reached this stage since the command itself has been completed
1037
def cmd_vulns_tabs(str, words)
1038
if words.length == 1
1039
return @@vulns_opts.option_keys.select { |opt| opt.start_with?(str) }
1040
end
1041
case words[-1]
1042
when '-o', '--output'
1043
return tab_complete_filenames(str, words)
1044
end
1045
end
1046
1047
def cmd_vulns_help
1048
print_line "Print all vulnerabilities in the database"
1049
print_line
1050
print_line "Usage: vulns [addr range]"
1051
print_line
1052
print @@vulns_opts.usage
1053
print_line
1054
print_line "Examples:"
1055
print_line " vulns -p 1-65536 # only vulns with associated services"
1056
print_line " vulns -p 1-65536 -s http # identified as http on any port"
1057
print_line
1058
end
1059
1060
@@vulns_opts = Rex::Parser::Arguments.new(
1061
[ '-h', '--help' ] => [ false, 'Show this help information.' ],
1062
[ '-o', '--output' ] => [ true, 'Send output to a file in csv format.', '<filename>' ],
1063
[ '-p', '--port' ] => [ true, 'List vulns matching this port spec.', '<port>' ],
1064
[ '-s', '--service' ] => [ true, 'List vulns matching these service names.', '<name>' ],
1065
[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],
1066
[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],
1067
[ '-i', '--info' ] => [ false, 'Display vuln information.' ],
1068
[ '-d', '--delete' ] => [ false, 'Delete vulnerabilities. Not officially supported.' ],
1069
[ '-v', '--verbose' ] => [ false, 'Display additional information.' ]
1070
)
1071
1072
def cmd_vulns(*args)
1073
return unless active?
1074
1075
default_columns = ['Timestamp', 'Host', 'Service', 'Resource', 'Name', 'References']
1076
host_ranges = []
1077
port_ranges = []
1078
svcs = []
1079
rhosts = []
1080
1081
search_term = nil
1082
show_info = false
1083
show_vuln_attempts = false
1084
set_rhosts = false
1085
output_file = nil
1086
delete_count = 0
1087
1088
mode = nil
1089
1090
@@vulns_opts.parse(args) do |opt, idx, val|
1091
case opt
1092
when '-d', '--delete' # TODO: This is currently undocumented because it's not officially supported.
1093
mode = :delete
1094
when '-h', '--help'
1095
cmd_vulns_help
1096
return
1097
when '-o', '--output'
1098
output_file = val
1099
if output_file
1100
output_file = File.expand_path(output_file)
1101
else
1102
print_error("Invalid output filename")
1103
return
1104
end
1105
when '-p', '--port'
1106
unless (arg_port_range(val, port_ranges, true))
1107
return
1108
end
1109
when '-s', '--service'
1110
service = val
1111
if (!service)
1112
print_error("Argument required for -s")
1113
return
1114
end
1115
svcs = service.split(/[\s]*,[\s]*/)
1116
when '-R', '--rhosts'
1117
set_rhosts = true
1118
when '-S', '--search'
1119
search_term = val
1120
when '-i', '--info'
1121
show_info = true
1122
when '-v', '--verbose'
1123
show_vuln_attempts = true
1124
else
1125
# Anything that wasn't an option is a host to search for
1126
unless (arg_host_range(val, host_ranges))
1127
return
1128
end
1129
end
1130
end
1131
1132
if show_info
1133
default_columns << 'Information'
1134
end
1135
1136
# add sentinel value meaning all if empty
1137
host_ranges.push(nil) if host_ranges.empty?
1138
# normalize
1139
ports = port_ranges.flatten.uniq
1140
svcs.flatten!
1141
tbl = Rex::Text::Table.new(
1142
'Header' => 'Vulnerabilities',
1143
'Columns' => default_columns
1144
)
1145
1146
matched_vuln_ids = []
1147
vulns = []
1148
if host_ranges.compact.empty?
1149
vulns = framework.db.vulns({:search_term => search_term})
1150
else
1151
each_host_range_chunk(host_ranges) do |host_search|
1152
next if host_search && host_search.empty?
1153
1154
vulns.concat(framework.db.vulns({:hosts => { :address => host_search }, :search_term => search_term }))
1155
end
1156
end
1157
1158
vulns.each do |vuln|
1159
reflist = vuln.refs.map {|r| r.name}
1160
if (vuln.service)
1161
# Skip this one if the user specified a port and it
1162
# doesn't match.
1163
next unless ports.empty? or ports.include? vuln.service.port
1164
# Same for service names
1165
next unless svcs.empty? or svcs.include?(vuln.service.name)
1166
else
1167
# This vuln has no service, so it can't match
1168
next unless ports.empty? and svcs.empty?
1169
end
1170
1171
matched_vuln_ids << vuln.id
1172
1173
row = []
1174
row << vuln.created_at
1175
row << vuln.host.address
1176
row << (vuln.service.present? ? "#{vuln.service.name} (#{vuln.service.port}/#{vuln.service.proto})" : 'None')
1177
row << vuln.resource.to_s
1178
row << vuln.name
1179
row << reflist.join(',')
1180
if show_info
1181
row << vuln.info
1182
end
1183
tbl << row
1184
1185
if set_rhosts
1186
addr = (vuln.host.scope.to_s != "" ? vuln.host.address + '%' + vuln.host.scope : vuln.host.address)
1187
rhosts << addr
1188
end
1189
end
1190
1191
if mode == :delete
1192
result = framework.db.delete_vuln(ids: matched_vuln_ids)
1193
delete_count = result.size
1194
end
1195
1196
if output_file
1197
if show_vuln_attempts
1198
print_warning("Cannot output to a file when verbose mode is enabled. Please remove verbose flag and try again.")
1199
else
1200
File.write(output_file, tbl.to_csv)
1201
print_status("Wrote vulnerability information to #{output_file}")
1202
end
1203
else
1204
print_line
1205
if show_vuln_attempts
1206
vulns_and_attempts = _format_vulns_and_vuln_attempts(vulns)
1207
_print_vulns_and_attempts(vulns_and_attempts)
1208
else
1209
print_line(tbl.to_s)
1210
end
1211
end
1212
1213
# Finally, handle the case where the user wants the resulting list
1214
# of hosts to go into RHOSTS.
1215
set_rhosts_from_addrs(rhosts.uniq) if set_rhosts
1216
1217
print_status("Deleted #{delete_count} vulnerabilities") if delete_count > 0
1218
end
1219
1220
#
1221
# Tab completion for the notes command
1222
#
1223
# @param str [String] the string currently being typed before tab was hit
1224
# @param words [Array<String>] the previously completed words on the command line. words is always
1225
# at least 1 when tab completion has reached this stage since the command itself has been completed
1226
def cmd_notes_tabs(str, words)
1227
if words.length == 1
1228
return @@notes_opts.option_keys.select { |opt| opt.start_with?(str) }
1229
end
1230
1231
case words[-1]
1232
when '-O', '--order'
1233
return []
1234
when '-o', '--output'
1235
return tab_complete_filenames(str, words)
1236
end
1237
1238
[]
1239
end
1240
1241
def cmd_notes_help
1242
print_line "Usage: notes [-h] [-t <type1,type2>] [-n <data string>] [-a] [addr range]"
1243
print_line
1244
print @@notes_opts.usage
1245
print_line
1246
print_line "Examples:"
1247
print_line " notes --add -t apps -n 'winzip' 10.1.1.34 10.1.20.41"
1248
print_line " notes -t smb.fingerprint 10.1.1.34 10.1.20.41"
1249
print_line " notes -S 'nmap.nse.(http|rtsp)'"
1250
print_line
1251
end
1252
1253
@@notes_opts = Rex::Parser::Arguments.new(
1254
[ '-a', '--add' ] => [ false, 'Add a note to the list of addresses, instead of listing.' ],
1255
[ '-d', '--delete' ] => [ false, 'Delete the notes instead of searching.' ],
1256
[ '-h', '--help' ] => [ false, 'Show this help information.' ],
1257
[ '-n', '--note' ] => [ true, 'Set the data for a new note (only with -a).', '<note>' ],
1258
[ '-O', '--order' ] => [ true, 'Order rows by specified column number.', '<column id>' ],
1259
[ '-o', '--output' ] => [ true, 'Save the notes to a csv file.', '<filename>' ],
1260
[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],
1261
[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],
1262
[ '-t', '--type' ] => [ true, 'Search for a list of types, or set single type for add.', '<type1,type2>' ],
1263
[ '-u', '--update' ] => [ false, 'Update a note. Not officially supported.' ]
1264
)
1265
1266
def cmd_notes(*args)
1267
return unless active?
1268
::ApplicationRecord.connection_pool.with_connection {
1269
mode = :search
1270
data = nil
1271
types = nil
1272
set_rhosts = false
1273
1274
host_ranges = []
1275
rhosts = []
1276
search_term = nil
1277
output_file = nil
1278
delete_count = 0
1279
order_by = nil
1280
1281
@@notes_opts.parse(args) do |opt, idx, val|
1282
case opt
1283
when '-a', '--add'
1284
mode = :add
1285
when '-d', '--delete'
1286
mode = :delete
1287
when '-n', '--note'
1288
data = val
1289
if(!data)
1290
print_error("Can't make a note with no data")
1291
return
1292
end
1293
when '-t', '--type'
1294
typelist = val
1295
if(!typelist)
1296
print_error("Invalid type list")
1297
return
1298
end
1299
types = typelist.strip().split(",")
1300
when '-R', '--rhosts'
1301
set_rhosts = true
1302
when '-S', '--search'
1303
search_term = val
1304
when '-o', '--output'
1305
output_file = val
1306
output_file = ::File.expand_path(output_file)
1307
when '-O'
1308
if (order_by = val.to_i - 1) < 0
1309
print_error('Please specify a column number starting from 1')
1310
return
1311
end
1312
when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported.
1313
mode = :update
1314
when '-h', '--help'
1315
cmd_notes_help
1316
return
1317
else
1318
# Anything that wasn't an option is a host to search for
1319
unless (arg_host_range(val, host_ranges))
1320
return
1321
end
1322
end
1323
end
1324
1325
if mode == :add
1326
if host_ranges.compact.empty?
1327
print_error("Host address or range required")
1328
return
1329
end
1330
1331
if types.nil? || types.size != 1
1332
print_error("Exactly one type is required")
1333
return
1334
end
1335
1336
if data.nil?
1337
print_error("Data required")
1338
return
1339
end
1340
1341
type = types.first
1342
host_ranges.each { |range|
1343
range.each { |addr|
1344
note = framework.db.find_or_create_note(host: addr, type: type, data: data)
1345
break if not note
1346
print_status("Time: #{note.created_at} Note: host=#{addr} type=#{note.ntype} data=#{note.data}")
1347
}
1348
}
1349
return
1350
end
1351
1352
if mode == :update
1353
if !types.nil? && types.size != 1
1354
print_error("Exactly one type is required")
1355
return
1356
end
1357
1358
if types.nil? && data.nil?
1359
print_error("Update requires data or type")
1360
return
1361
end
1362
end
1363
1364
note_list = []
1365
if host_ranges.compact.empty?
1366
# No host specified - collect all notes
1367
opts = {search_term: search_term}
1368
opts[:ntype] = types if mode != :update && types && !types.empty?
1369
note_list = framework.db.notes(opts)
1370
else
1371
# Collect notes of specified hosts
1372
each_host_range_chunk(host_ranges) do |host_search|
1373
next if host_search && host_search.empty?
1374
1375
opts = {hosts: {address: host_search}, workspace: framework.db.workspace, search_term: search_term}
1376
opts[:ntype] = types if mode != :update && types && !types.empty?
1377
note_list.concat(framework.db.notes(opts))
1378
end
1379
end
1380
1381
# Now display them
1382
table = Rex::Text::Table.new(
1383
'Header' => 'Notes',
1384
'Indent' => 1,
1385
'Columns' => ['Time', 'Host', 'Service', 'Port', 'Protocol', 'Type', 'Data'],
1386
'SortIndex' => order_by
1387
)
1388
1389
matched_note_ids = []
1390
note_list.each do |note|
1391
if mode == :update
1392
begin
1393
update_opts = {id: note.id}
1394
unless types.nil?
1395
note.ntype = types.first
1396
update_opts[:ntype] = types.first
1397
end
1398
1399
unless data.nil?
1400
note.data = data
1401
update_opts[:data] = data
1402
end
1403
1404
framework.db.update_note(update_opts)
1405
rescue => e
1406
elog "There was an error updating note with ID #{note.id}: #{e.message}"
1407
next
1408
end
1409
end
1410
1411
matched_note_ids << note.id
1412
1413
row = []
1414
row << note.created_at
1415
1416
if note.host
1417
host = note.host
1418
row << host.address
1419
if set_rhosts
1420
addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address)
1421
rhosts << addr
1422
end
1423
else
1424
row << ''
1425
end
1426
1427
if note.service
1428
row << note.service.name || ''
1429
row << note.service.port || ''
1430
row << note.service.proto || ''
1431
else
1432
row << '' # For the Service field
1433
row << '' # For the Port field
1434
row << '' # For the Protocol field
1435
end
1436
1437
row << note.ntype
1438
row << note.data.inspect
1439
table << row
1440
end
1441
1442
if mode == :delete
1443
result = framework.db.delete_note(ids: matched_note_ids)
1444
delete_count = result.size
1445
end
1446
1447
if output_file
1448
save_csv_notes(output_file, table)
1449
else
1450
print_line
1451
print_line(table.to_s)
1452
end
1453
1454
# Finally, handle the case where the user wants the resulting list
1455
# of hosts to go into RHOSTS.
1456
set_rhosts_from_addrs(rhosts.uniq) if set_rhosts
1457
1458
print_status("Deleted #{delete_count} notes") if delete_count > 0
1459
}
1460
end
1461
1462
def save_csv_notes(fpath, table)
1463
begin
1464
File.open(fpath, 'wb') do |f|
1465
f.write(table.to_csv)
1466
end
1467
print_status("Wrote notes to #{fpath}")
1468
rescue Errno::EACCES => e
1469
print_error("Unable to save notes. #{e.message}")
1470
end
1471
end
1472
1473
#
1474
# Tab completion for the loot command
1475
#
1476
# @param str [String] the string currently being typed before tab was hit
1477
# @param words [Array<String>] the previously completed words on the command line. words is always
1478
# at least 1 when tab completion has reached this stage since the command itself has been completed
1479
def cmd_loot_tabs(str, words)
1480
if words.length == 1
1481
@@loot_opts.option_keys.select { |opt| opt.start_with?(str) }
1482
end
1483
end
1484
1485
def cmd_loot_help
1486
print_line "Usage: loot [options]"
1487
print_line " Info: loot [-h] [addr1 addr2 ...] [-t <type1,type2>]"
1488
print_line " Add: loot -f [fname] -i [info] -a [addr1 addr2 ...] -t [type]"
1489
print_line " Del: loot -d [addr1 addr2 ...]"
1490
print_line
1491
print @@loot_opts.usage
1492
print_line
1493
end
1494
1495
@@loot_opts = Rex::Parser::Arguments.new(
1496
[ '-a', '--add' ] => [ false, 'Add loot to the list of addresses, instead of listing.' ],
1497
[ '-d', '--delete' ] => [ false, 'Delete *all* loot matching host and type.' ],
1498
[ '-f', '--file' ] => [ true, 'File with contents of the loot to add.', '<filename>' ],
1499
[ '-i', '--info' ] => [ true, 'Info of the loot to add.', '<info>' ],
1500
[ '-t', '--type' ] => [ true, 'Search for a list of types.', '<type1,type2>' ],
1501
[ '-h', '--help' ] => [ false, 'Show this help information.' ],
1502
[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],
1503
[ '-u', '--update' ] => [ false, 'Update loot. Not officially supported.' ]
1504
)
1505
1506
def cmd_loot(*args)
1507
return unless active?
1508
1509
mode = :search
1510
host_ranges = []
1511
types = nil
1512
delete_count = 0
1513
search_term = nil
1514
file = nil
1515
name = nil
1516
info = nil
1517
filename = nil
1518
1519
@@loot_opts.parse(args) do |opt, idx, val|
1520
case opt
1521
when '-a', '--add'
1522
mode = :add
1523
when '-d', '--delete'
1524
mode = :delete
1525
when '-f', '--file'
1526
filename = val
1527
if(!filename)
1528
print_error("Can't make loot with no filename")
1529
return
1530
end
1531
if (!File.exist?(filename) or !File.readable?(filename))
1532
print_error("Can't read file")
1533
return
1534
end
1535
when '-i', '--info'
1536
info = val
1537
if(!info)
1538
print_error("Can't make loot with no info")
1539
return
1540
end
1541
when '-t', '--type'
1542
typelist = val
1543
if(!typelist)
1544
print_error("Invalid type list")
1545
return
1546
end
1547
types = typelist.strip().split(",")
1548
when '-S', '--search'
1549
search_term = val
1550
when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported.
1551
mode = :update
1552
when '-h', '--help'
1553
cmd_loot_help
1554
return
1555
else
1556
# Anything that wasn't an option is a host to search for
1557
unless (arg_host_range(val, host_ranges))
1558
return
1559
end
1560
end
1561
end
1562
1563
tbl = Rex::Text::Table.new({
1564
'Header' => "Loot",
1565
'Columns' => [ 'host', 'service', 'type', 'name', 'content', 'info', 'path' ],
1566
# For now, don't perform any word wrapping on the loot table as it breaks the workflow of
1567
# copying paths and pasting them into applications
1568
'WordWrap' => false,
1569
})
1570
1571
# Sentinel value meaning all
1572
host_ranges.push(nil) if host_ranges.empty?
1573
1574
if mode == :add
1575
if host_ranges.compact.empty?
1576
print_error('Address list required')
1577
return
1578
end
1579
if info.nil?
1580
print_error("Info required")
1581
return
1582
end
1583
if filename.nil?
1584
print_error("Loot file required")
1585
return
1586
end
1587
if types.nil? or types.size != 1
1588
print_error("Exactly one loot type is required")
1589
return
1590
end
1591
type = types.first
1592
name = File.basename(filename)
1593
file = File.open(filename, "rb")
1594
contents = file.read
1595
host_ranges.each do |range|
1596
range.each do |host|
1597
lootfile = framework.db.find_or_create_loot(:type => type, :host => host, :info => info, :data => contents, :path => filename, :name => name)
1598
print_status("Added loot for #{host} (#{lootfile})")
1599
end
1600
end
1601
return
1602
end
1603
1604
matched_loot_ids = []
1605
loots = []
1606
if host_ranges.compact.empty?
1607
loots = loots + framework.db.loots(workspace: framework.db.workspace, search_term: search_term)
1608
else
1609
each_host_range_chunk(host_ranges) do |host_search|
1610
next if host_search && host_search.empty?
1611
1612
loots = loots + framework.db.loots(workspace: framework.db.workspace, hosts: { address: host_search }, search_term: search_term)
1613
end
1614
end
1615
1616
loots.each do |loot|
1617
row = []
1618
# TODO: This is just a temp implementation of update for the time being since it did not exist before.
1619
# It should be updated to not pass all of the attributes attached to the object, only the ones being updated.
1620
if mode == :update
1621
begin
1622
loot.info = info if info
1623
if types && types.size > 1
1624
print_error "May only pass 1 type when performing an update."
1625
next
1626
end
1627
loot.ltype = types.first if types
1628
framework.db.update_loot(loot.as_json.symbolize_keys)
1629
rescue => e
1630
elog "There was an error updating loot with ID #{loot.id}: #{e.message}"
1631
next
1632
end
1633
end
1634
row.push (loot.host && loot.host.address) ? loot.host.address : ""
1635
if (loot.service)
1636
svc = (loot.service.name ? loot.service.name : "#{loot.service.port}/#{loot.service.proto}")
1637
row.push svc
1638
else
1639
row.push ""
1640
end
1641
row.push(loot.ltype)
1642
row.push(loot.name || "")
1643
row.push(loot.content_type)
1644
row.push(loot.info || "")
1645
row.push(loot.path)
1646
1647
tbl << row
1648
matched_loot_ids << loot.id
1649
end
1650
1651
if (mode == :delete)
1652
result = framework.db.delete_loot(ids: matched_loot_ids)
1653
delete_count = result.size
1654
end
1655
1656
print_line
1657
print_line(tbl.to_s)
1658
print_status("Deleted #{delete_count} loots") if delete_count > 0
1659
end
1660
1661
# :category: Deprecated Commands
1662
def cmd_db_hosts_help; deprecated_help(:hosts); end
1663
# :category: Deprecated Commands
1664
def cmd_db_notes_help; deprecated_help(:notes); end
1665
# :category: Deprecated Commands
1666
def cmd_db_vulns_help; deprecated_help(:vulns); end
1667
# :category: Deprecated Commands
1668
def cmd_db_services_help; deprecated_help(:services); end
1669
# :category: Deprecated Commands
1670
def cmd_db_autopwn_help; deprecated_help; end
1671
# :category: Deprecated Commands
1672
def cmd_db_driver_help; deprecated_help; end
1673
1674
# :category: Deprecated Commands
1675
def cmd_db_hosts(*args); deprecated_cmd(:hosts, *args); end
1676
# :category: Deprecated Commands
1677
def cmd_db_notes(*args); deprecated_cmd(:notes, *args); end
1678
# :category: Deprecated Commands
1679
def cmd_db_vulns(*args); deprecated_cmd(:vulns, *args); end
1680
# :category: Deprecated Commands
1681
def cmd_db_services(*args); deprecated_cmd(:services, *args); end
1682
# :category: Deprecated Commands
1683
def cmd_db_autopwn(*args); deprecated_cmd; end
1684
1685
#
1686
# :category: Deprecated Commands
1687
#
1688
# This one deserves a little more explanation than standard deprecation
1689
# warning, so give the user a better understanding of what's going on.
1690
#
1691
def cmd_db_driver(*args)
1692
deprecated_cmd
1693
print_line
1694
print_line "Because Metasploit no longer supports databases other than the default"
1695
print_line "PostgreSQL, there is no longer a need to set the driver. Thus db_driver"
1696
print_line "is not useful and its functionality has been removed. Usually Metasploit"
1697
print_line "will already have connected to the database; check db_status to see."
1698
print_line
1699
cmd_db_status
1700
end
1701
1702
def cmd_db_import_tabs(str, words)
1703
tab_complete_filenames(str, words)
1704
end
1705
1706
def cmd_db_import_help
1707
print_line "Usage: db_import <filename> [file2...]"
1708
print_line
1709
print_line "Filenames can be globs like *.xml, or **/*.xml which will search recursively"
1710
print_line "Currently supported file types include:"
1711
print_line " Acunetix"
1712
print_line " Amap Log"
1713
print_line " Amap Log -m"
1714
print_line " Appscan"
1715
print_line " Burp Session XML"
1716
print_line " Burp Issue XML"
1717
print_line " CI"
1718
print_line " Foundstone"
1719
print_line " FusionVM XML"
1720
print_line " Group Policy Preferences Credentials"
1721
print_line " IP Address List"
1722
print_line " IP360 ASPL"
1723
print_line " IP360 XML v3"
1724
print_line " Libpcap Packet Capture"
1725
print_line " Masscan XML"
1726
print_line " Metasploit PWDump Export"
1727
print_line " Metasploit XML"
1728
print_line " Metasploit Zip Export"
1729
print_line " Microsoft Baseline Security Analyzer"
1730
print_line " NeXpose Simple XML"
1731
print_line " NeXpose XML Report"
1732
print_line " Nessus NBE Report"
1733
print_line " Nessus XML (v1)"
1734
print_line " Nessus XML (v2)"
1735
print_line " NetSparker XML"
1736
print_line " Nikto XML"
1737
print_line " Nmap XML"
1738
print_line " OpenVAS Report"
1739
print_line " OpenVAS XML (optional arguments -cert -dfn)"
1740
print_line " Outpost24 XML"
1741
print_line " Qualys Asset XML"
1742
print_line " Qualys Scan XML"
1743
print_line " Retina XML"
1744
print_line " Spiceworks CSV Export"
1745
print_line " Wapiti XML"
1746
print_line
1747
end
1748
1749
#
1750
# Generic import that automatically detects the file type
1751
#
1752
def cmd_db_import(*args)
1753
return unless active?
1754
openvas_cert = false
1755
openvas_dfn = false
1756
::ApplicationRecord.connection_pool.with_connection {
1757
if args.include?("-h") || ! (args && args.length > 0)
1758
cmd_db_import_help
1759
return
1760
end
1761
if args.include?("-dfn")
1762
openvas_dfn = true
1763
end
1764
if args.include?("-cert")
1765
openvas_cert = true
1766
end
1767
options = {:openvas_dfn => openvas_dfn, :openvas_cert => openvas_cert}
1768
args.each { |glob|
1769
next if (glob.include?("-cert") || glob.include?("-dfn"))
1770
files = ::Dir.glob(::File.expand_path(glob))
1771
if files.empty?
1772
print_error("No such file #{glob}")
1773
next
1774
end
1775
files.each { |filename|
1776
if (not ::File.readable?(filename))
1777
print_error("Could not read file #{filename}")
1778
next
1779
end
1780
begin
1781
warnings = 0
1782
framework.db.import_file(:filename => filename, :options => options) do |type,data|
1783
case type
1784
when :debug
1785
print_error("DEBUG: #{data.inspect}")
1786
when :vuln
1787
inst = data[1] == 1 ? "instance" : "instances"
1788
print_status("Importing vulnerability '#{data[0]}' (#{data[1]} #{inst})")
1789
when :filetype
1790
print_status("Importing '#{data}' data")
1791
when :parser
1792
print_status("Import: Parsing with '#{data}'")
1793
when :address
1794
print_status("Importing host #{data}")
1795
when :service
1796
print_status("Importing service #{data}")
1797
when :msf_loot
1798
print_status("Importing loot #{data}")
1799
when :msf_task
1800
print_status("Importing task #{data}")
1801
when :msf_report
1802
print_status("Importing report #{data}")
1803
when :pcap_count
1804
print_status("Import: #{data} packets processed")
1805
when :record_count
1806
print_status("Import: #{data[1]} records processed")
1807
when :warning
1808
print_error
1809
data.split("\n").each do |line|
1810
print_error(line)
1811
end
1812
print_error
1813
warnings += 1
1814
end
1815
end
1816
print_status("Successfully imported #{filename}")
1817
1818
print_error("Please note that there were #{warnings} warnings") if warnings > 1
1819
print_error("Please note that there was one warning") if warnings == 1
1820
1821
rescue Msf::DBImportError => e
1822
print_error("Failed to import #{filename}: #{$!}")
1823
elog("Failed to import #{filename}", error: e)
1824
dlog("Call stack: #{$@.join("\n")}", LEV_3)
1825
next
1826
rescue REXML::ParseException => e
1827
print_error("Failed to import #{filename} due to malformed XML:")
1828
print_error("#{e.class}: #{e}")
1829
elog("Failed to import #{filename}", error: e)
1830
dlog("Call stack: #{$@.join("\n")}", LEV_3)
1831
next
1832
end
1833
}
1834
}
1835
}
1836
end
1837
1838
def cmd_db_export_help
1839
# Like db_hosts and db_services, this creates a list of columns, so
1840
# use its -h
1841
cmd_db_export("-h")
1842
end
1843
1844
#
1845
# Export an XML
1846
#
1847
def cmd_db_export(*args)
1848
return unless active?
1849
::ApplicationRecord.connection_pool.with_connection {
1850
1851
export_formats = %W{xml pwdump}
1852
format = 'xml'
1853
output = nil
1854
1855
while (arg = args.shift)
1856
case arg
1857
when '-h','--help'
1858
print_line "Usage:"
1859
print_line " db_export -f <format> [filename]"
1860
print_line " Format can be one of: #{export_formats.join(", ")}"
1861
when '-f','--format'
1862
format = args.shift.to_s.downcase
1863
else
1864
output = arg
1865
end
1866
end
1867
1868
if not output
1869
print_error("No output file was specified")
1870
return
1871
end
1872
1873
if not export_formats.include?(format)
1874
print_error("Unsupported file format: #{format}")
1875
print_error("Unsupported file format: '#{format}'. Must be one of: #{export_formats.join(", ")}")
1876
return
1877
end
1878
1879
print_status("Starting export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...")
1880
framework.db.run_db_export(output, format)
1881
print_status("Finished export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...")
1882
}
1883
end
1884
1885
def find_nmap_path
1886
Rex::FileUtils.find_full_path("nmap") || Rex::FileUtils.find_full_path("nmap.exe")
1887
end
1888
1889
#
1890
# Import Nmap data from a file
1891
#
1892
def cmd_db_nmap(*args)
1893
return unless active?
1894
::ApplicationRecord.connection_pool.with_connection {
1895
if (args.length == 0)
1896
print_status("Usage: db_nmap [--save | [--help | -h]] [nmap options]")
1897
return
1898
end
1899
1900
save = false
1901
arguments = []
1902
while (arg = args.shift)
1903
case arg
1904
when '--save'
1905
save = true
1906
when '--help', '-h'
1907
cmd_db_nmap_help
1908
return
1909
else
1910
arguments << arg
1911
end
1912
end
1913
1914
nmap = find_nmap_path
1915
unless nmap
1916
print_error("The nmap executable could not be found")
1917
return
1918
end
1919
1920
fd = Rex::Quickfile.new(['msf-db-nmap-', '.xml'], Msf::Config.local_directory)
1921
1922
begin
1923
# When executing native Nmap in Cygwin, expand the Cygwin path to a Win32 path
1924
if(Rex::Compat.is_cygwin and nmap =~ /cygdrive/)
1925
# Custom function needed because cygpath breaks on 8.3 dirs
1926
tout = Rex::Compat.cygwin_to_win32(fd.path)
1927
arguments.push('-oX', tout)
1928
else
1929
arguments.push('-oX', fd.path)
1930
end
1931
1932
run_nmap(nmap, arguments)
1933
1934
framework.db.import_nmap_xml_file(:filename => fd.path)
1935
1936
print_status("Saved NMAP XML results to #{fd.path}") if save
1937
ensure
1938
fd.close
1939
fd.unlink unless save
1940
end
1941
}
1942
end
1943
1944
def cmd_db_nmap_help
1945
nmap = find_nmap_path
1946
unless nmap
1947
print_error("The nmap executable could not be found")
1948
return
1949
end
1950
1951
stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help')
1952
1953
stdout.each_line do |out_line|
1954
next if out_line.strip.empty?
1955
print_status(out_line.strip)
1956
end
1957
1958
stderr.each_line do |err_line|
1959
next if err_line.strip.empty?
1960
print_error(err_line.strip)
1961
end
1962
end
1963
1964
def cmd_db_nmap_tabs(str, words)
1965
nmap = find_nmap_path
1966
unless nmap
1967
return
1968
end
1969
1970
stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help')
1971
tabs = []
1972
stdout.each_line do |out_line|
1973
if out_line.strip.starts_with?('-')
1974
tabs.push(out_line.strip.split(':').first)
1975
end
1976
end
1977
1978
stderr.each_line do |err_line|
1979
next if err_line.strip.empty?
1980
print_error(err_line.strip)
1981
end
1982
1983
return tabs
1984
end
1985
1986
#
1987
# Database management
1988
#
1989
def db_check_driver
1990
unless framework.db.driver
1991
print_error("No database driver installed.")
1992
return false
1993
end
1994
true
1995
end
1996
1997
#
1998
# Is everything working?
1999
#
2000
def cmd_db_status(*args)
2001
return if not db_check_driver
2002
2003
if framework.db.connection_established?
2004
print_connection_info
2005
else
2006
print_status("#{framework.db.driver} selected, no connection")
2007
end
2008
end
2009
2010
2011
def cmd_db_connect_help
2012
print_line(" USAGE:")
2013
print_line(" * Postgres Data Service:")
2014
print_line(" db_connect <user:[pass]>@<host:[port]>/<database>")
2015
print_line(" Examples:")
2016
print_line(" db_connect user@metasploit3")
2017
print_line(" db_connect user:[email protected]/metasploit3")
2018
print_line(" db_connect user:[email protected]:1500/metasploit3")
2019
print_line(" db_connect -y [path/to/database.yml]")
2020
print_line(" ")
2021
print_line(" * HTTP Data Service:")
2022
print_line(" db_connect [options] <http|https>://<host:[port]>")
2023
print_line(" Examples:")
2024
print_line(" db_connect http://localhost:8080")
2025
print_line(" db_connect http://my-super-msf-data.service.com")
2026
print_line(" db_connect -c ~/cert.pem -t 6a7a74c1a5003802c955ead1bbddd4ab1b05a7f2940b4732d34bfc555bc6e1c5d7611a497b29e8f0 https://localhost:8080")
2027
print_line(" NOTE: You must be connected to a Postgres data service in order to successfully connect to a HTTP data service.")
2028
print_line(" ")
2029
print_line(" Persisting Connections:")
2030
print_line(" db_connect --name <name to save connection as> [options] <address>")
2031
print_line(" Examples:")
2032
print_line(" Saving: db_connect --name LA-server http://123.123.123.45:1234")
2033
print_line(" Connecting: db_connect LA-server")
2034
print_line(" ")
2035
print_line(" OPTIONS:")
2036
print_line(" -l,--list-services List the available data services that have been previously saved.")
2037
print_line(" -y,--yaml Connect to the data service specified in the provided database.yml file.")
2038
print_line(" -n,--name Name used to store the connection. Providing an existing name will overwrite the settings for that connection.")
2039
print_line(" -c,--cert Certificate file matching the remote data server's certificate. Needed when using self-signed SSL cert.")
2040
print_line(" -t,--token The API token used to authenticate to the remote data service.")
2041
print_line(" --skip-verify Skip validating authenticity of server's certificate (NOT RECOMMENDED).")
2042
print_line("")
2043
end
2044
2045
def cmd_db_connect(*args)
2046
return if not db_check_driver
2047
2048
opts = {}
2049
while (arg = args.shift)
2050
case arg
2051
when '-h', '--help'
2052
cmd_db_connect_help
2053
return
2054
when '-y', '--yaml'
2055
opts[:yaml_file] = args.shift
2056
when '-c', '--cert'
2057
opts[:cert] = args.shift
2058
when '-t', '--token'
2059
opts[:api_token] = args.shift
2060
when '-l', '--list-services'
2061
list_saved_data_services
2062
return
2063
when '-n', '--name'
2064
opts[:name] = args.shift
2065
if opts[:name] =~ /\/|\[|\]/
2066
print_error "Provided name contains an invalid character. Aborting connection."
2067
return
2068
end
2069
when '--skip-verify'
2070
opts[:skip_verify] = true
2071
else
2072
found_name = ::Msf::DbConnector.data_service_search(name: arg)
2073
if found_name
2074
opts = ::Msf::DbConnector.load_db_config(found_name)
2075
else
2076
opts[:url] = arg
2077
end
2078
end
2079
end
2080
2081
if !opts[:url] && !opts[:yaml_file]
2082
print_error 'A URL or saved data service name is required.'
2083
print_line
2084
cmd_db_connect_help
2085
return
2086
end
2087
2088
if opts[:url] =~ /http/
2089
new_conn_type = 'http'
2090
else
2091
new_conn_type = framework.db.driver
2092
end
2093
2094
# Currently only able to be connected to one DB at a time
2095
if framework.db.connection_established?
2096
# But the http connection still requires a local database to support AR, so we have to allow that
2097
# Don't allow more than one HTTP service, though
2098
if new_conn_type != 'http' || framework.db.get_services_metadata.count >= 2
2099
print_error('Connection already established. Only one connection is allowed at a time.')
2100
print_error('Run db_disconnect first if you wish to connect to a different data service.')
2101
print_line
2102
print_line 'Current connection information:'
2103
print_connection_info
2104
return
2105
end
2106
end
2107
2108
result = Msf::DbConnector.db_connect(framework, opts)
2109
if result[:error]
2110
print_error result[:error]
2111
return
2112
end
2113
2114
if result[:result]
2115
print_status result[:result]
2116
end
2117
if framework.db.active
2118
name = opts[:name]
2119
if !name || name.empty?
2120
if found_name
2121
name = found_name
2122
elsif result[:data_service_name]
2123
name = result[:data_service_name]
2124
else
2125
name = Rex::Text.rand_text_alphanumeric(8)
2126
end
2127
end
2128
2129
save_db_to_config(framework.db, name)
2130
@current_data_service = name
2131
end
2132
end
2133
2134
def cmd_db_disconnect_help
2135
print_line "Usage:"
2136
print_line " db_disconnect Temporarily disconnects from the currently configured dataservice."
2137
print_line " db_disconnect --clear Clears the default dataservice that msfconsole will use when opened."
2138
print_line
2139
end
2140
2141
def cmd_db_disconnect(*args)
2142
return if not db_check_driver
2143
2144
if args[0] == '-h' || args[0] == '--help'
2145
cmd_db_disconnect_help
2146
return
2147
elsif args[0] == '-c' || args[0] == '--clear'
2148
clear_default_db
2149
return
2150
end
2151
2152
previous_name = framework.db.name
2153
result = Msf::DbConnector.db_disconnect(framework)
2154
2155
if result[:error]
2156
print_error "Unable to disconnect from the data service: #{@current_data_service}"
2157
print_error result[:error]
2158
elsif result[:old_data_service_name].nil?
2159
print_error 'Not currently connected to a data service.'
2160
else
2161
print_line "Successfully disconnected from the data service: #{previous_name}."
2162
@current_data_service = result[:data_service_name]
2163
if @current_data_service
2164
print_line "Now connected to: #{@current_data_service}."
2165
end
2166
end
2167
end
2168
2169
def cmd_db_rebuild_cache(*args)
2170
print_line "This command is deprecated with Metasploit 5"
2171
end
2172
2173
def cmd_db_save_help
2174
print_line "Usage: db_save"
2175
print_line
2176
print_line "Save the current data service connection as the default to reconnect on startup."
2177
print_line
2178
end
2179
2180
def cmd_db_save(*args)
2181
while (arg = args.shift)
2182
case arg
2183
when '-h', '--help'
2184
cmd_db_save_help
2185
return
2186
end
2187
end
2188
2189
if !framework.db.active || !@current_data_service
2190
print_error "Not currently connected to a data service that can be saved."
2191
return
2192
end
2193
2194
begin
2195
Msf::Config.save(DB_CONFIG_PATH => { 'default_db' => @current_data_service })
2196
print_line "Successfully saved data service as default: #{@current_data_service}"
2197
rescue ArgumentError => e
2198
print_error e.message
2199
end
2200
end
2201
2202
def save_db_to_config(database, database_name)
2203
if database_name =~ /\/|\[|\]/
2204
raise ArgumentError, 'Data service name contains an invalid character.'
2205
end
2206
config_path = "#{DB_CONFIG_PATH}/#{database_name}"
2207
config_opts = {}
2208
if !database.is_local?
2209
begin
2210
config_opts['url'] = database.endpoint
2211
if database.https_opts
2212
config_opts['cert'] = database.https_opts[:cert] if database.https_opts[:cert]
2213
config_opts['skip_verify'] = true if database.https_opts[:skip_verify]
2214
end
2215
if database.api_token
2216
config_opts['api_token'] = database.api_token
2217
end
2218
Msf::Config.save(config_path => config_opts)
2219
rescue => e
2220
print_error "There was an error saving the data service configuration: #{e.message}"
2221
end
2222
else
2223
url = Msf::DbConnector.build_postgres_url
2224
config_opts['url'] = url
2225
Msf::Config.save(config_path => config_opts)
2226
end
2227
end
2228
2229
def cmd_db_remove_help
2230
print_line "Usage: db_remove <name>"
2231
print_line
2232
print_line "Delete the specified saved data service."
2233
print_line
2234
end
2235
2236
def cmd_db_remove(*args)
2237
if args[0] == '-h' || args[0] == '--help' || args[0].nil? || args[0].empty?
2238
cmd_db_remove_help
2239
return
2240
end
2241
delete_db_from_config(args[0])
2242
end
2243
2244
def delete_db_from_config(db_name)
2245
conf = Msf::Config.load
2246
db_path = "#{DB_CONFIG_PATH}/#{db_name}"
2247
if conf[db_path]
2248
clear_default_db if conf[DB_CONFIG_PATH]['default_db'] && conf[DB_CONFIG_PATH]['default_db'] == db_name
2249
Msf::Config.delete_group(db_path)
2250
print_line "Successfully deleted data service: #{db_name}"
2251
else
2252
print_line "Unable to locate saved data service with name #{db_name}."
2253
end
2254
end
2255
2256
def clear_default_db
2257
conf = Msf::Config.load
2258
if conf[DB_CONFIG_PATH] && conf[DB_CONFIG_PATH]['default_db']
2259
updated_opts = conf[DB_CONFIG_PATH]
2260
updated_opts.delete('default_db')
2261
Msf::Config.save(DB_CONFIG_PATH => updated_opts)
2262
print_line "Cleared the default data service."
2263
else
2264
print_line "No default data service was configured."
2265
end
2266
end
2267
2268
def db_find_tools(tools)
2269
missed = []
2270
tools.each do |name|
2271
if(! Rex::FileUtils.find_full_path(name))
2272
missed << name
2273
end
2274
end
2275
if(not missed.empty?)
2276
print_error("This database command requires the following tools to be installed: #{missed.join(", ")}")
2277
return
2278
end
2279
true
2280
end
2281
2282
#######
2283
private
2284
2285
def run_nmap(nmap, arguments, use_sudo: false)
2286
print_warning('Running Nmap with sudo') if use_sudo
2287
begin
2288
nmap_pipe = use_sudo ? ::Open3::popen3('sudo', nmap, *arguments) : ::Open3::popen3(nmap, *arguments)
2289
temp_nmap_threads = []
2290
temp_nmap_threads << framework.threads.spawn("db_nmap-Stdout", false, nmap_pipe[1]) do |np_1|
2291
np_1.each_line do |nmap_out|
2292
next if nmap_out.strip.empty?
2293
print_status("Nmap: #{nmap_out.strip}")
2294
end
2295
end
2296
2297
temp_nmap_threads << framework.threads.spawn("db_nmap-Stderr", false, nmap_pipe[2]) do |np_2|
2298
2299
np_2.each_line do |nmap_err|
2300
next if nmap_err.strip.empty?
2301
print_status("Nmap: '#{nmap_err.strip}'")
2302
# Check if the stderr text includes 'root', this only happens if the scan requires root privileges
2303
if nmap_err =~ /requires? root privileges/ or
2304
nmap_err.include? 'only works if you are root' or nmap_err =~ /requires? raw socket access/
2305
return run_nmap(nmap, arguments, use_sudo: true) unless use_sudo
2306
end
2307
end
2308
end
2309
2310
temp_nmap_threads.map { |t| t.join rescue nil }
2311
nmap_pipe.each { |p| p.close rescue nil }
2312
rescue ::IOError
2313
end
2314
end
2315
2316
#######
2317
2318
def print_connection_info
2319
cdb = ''
2320
if framework.db.driver == 'http'
2321
cdb = framework.db.name
2322
else
2323
::ApplicationRecord.connection_pool.with_connection do |conn|
2324
if conn.respond_to?(:current_database)
2325
cdb = conn.current_database
2326
end
2327
end
2328
end
2329
output = "Connected to #{cdb}. Connection type: #{framework.db.driver}."
2330
output += " Connection name: #{@current_data_service}." if @current_data_service
2331
print_status(output)
2332
end
2333
2334
def list_saved_data_services
2335
conf = Msf::Config.load
2336
default = nil
2337
tbl = Rex::Text::Table.new({
2338
'Header' => 'Data Services',
2339
'Columns' => ['current', 'name', 'url', 'default?'],
2340
'SortIndex' => 1
2341
})
2342
2343
conf.each_pair do |k,v|
2344
if k =~ /#{DB_CONFIG_PATH}/
2345
default = v['default_db'] if v['default_db']
2346
name = k.split('/').last
2347
next if name == 'database' # Data service information is not stored in 'framework/database', just metadata
2348
url = v['url']
2349
current = ''
2350
current = '*' if name == @current_data_service
2351
default_output = ''
2352
default_output = '*' if name == default
2353
line = [current, name, url, default_output]
2354
tbl << line
2355
end
2356
end
2357
print_line
2358
print_line tbl.to_s
2359
end
2360
2361
def print_msgs(status_msg, error_msg)
2362
status_msg.each do |s|
2363
print_status(s)
2364
end
2365
2366
error_msg.each do |e|
2367
print_error(e)
2368
end
2369
end
2370
2371
def _format_vulns_and_vuln_attempts(vulns)
2372
vulns.map.with_index do |vuln, index|
2373
service_str = ''
2374
if vuln.service.present?
2375
service_str << "#{vuln.service.name} (port: #{vuln.service.port}, resource: #{vuln.service.resource.to_json})"
2376
if vuln.service.parents.any?
2377
service_str << "\nParent Services:\n".indent(5)
2378
service_str << _print_service_parents(vuln.service).indent(7)
2379
end
2380
end
2381
2382
vuln_formatted = <<~EOF.strip.indent(2)
2383
#{index}. Vuln ID: #{vuln.id}
2384
Timestamp: #{vuln.created_at}
2385
Host: #{vuln.host.address}
2386
Name: #{vuln.name}
2387
References: #{vuln.refs.map {|r| r.name}.join(',')}
2388
Information: #{_format_vuln_value(vuln.info)}
2389
Resource: #{vuln.resource.to_json}
2390
Service: #{service_str}
2391
EOF
2392
2393
vuln_attempts_formatted = vuln.vuln_attempts.map.with_index do |vuln_attempt, i|
2394
<<~EOF.strip.indent(5)
2395
#{i}. ID: #{vuln_attempt.id}
2396
Vuln ID: #{vuln_attempt.vuln_id}
2397
Timestamp: #{vuln_attempt.attempted_at}
2398
Exploit: #{vuln_attempt.exploited}
2399
Fail reason: #{_format_vuln_value(vuln_attempt.fail_reason)}
2400
Username: #{vuln_attempt.username}
2401
Module: #{vuln_attempt.module}
2402
Session ID: #{_format_vuln_value(vuln_attempt.session_id)}
2403
Loot ID: #{_format_vuln_value(vuln_attempt.loot_id)}
2404
Fail Detail: #{_format_vuln_value(vuln_attempt.fail_detail)}
2405
EOF
2406
end
2407
2408
{ :vuln => vuln_formatted, :vuln_attempts => vuln_attempts_formatted }
2409
end
2410
end
2411
2412
def _print_service_parents(service, indent_level = 0)
2413
service.parents.map do |parent_service|
2414
parent_service_str = "#{parent_service.name} (port: #{parent_service.port}, resource: #{parent_service.resource.to_json})".indent(indent_level * 2)
2415
if parent_service.parents&.any?
2416
parent_service_str << "\n#{_print_service_parents(parent_service, indent_level + 1)}"
2417
end
2418
parent_service_str
2419
end.flatten.join("\n")
2420
end
2421
2422
def _print_vulns_and_attempts(vulns_and_attempts)
2423
print_line("Vulnerabilities\n===============")
2424
vulns_and_attempts.each do |vuln_and_attempt|
2425
print_line(vuln_and_attempt[:vuln])
2426
print_line("Vuln attempts:".indent(5))
2427
vuln_and_attempt[:vuln_attempts].each do |attempt|
2428
print_line(attempt)
2429
end
2430
end
2431
end
2432
2433
def _format_vuln_value(s)
2434
s.blank? ? s.inspect : s.to_s
2435
end
2436
end
2437
2438
end end end end
2439
2440