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
19851 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
850
names = nil
851
order_by = nil
852
proto = nil
853
host_ranges = []
854
port_ranges = []
855
rhosts = []
856
delete_count = 0
857
search_term = nil
858
opts = {}
859
860
@@services_opts.parse(args) do |opt, idx, val|
861
case opt
862
when '-a', '--add'
863
mode = :add
864
when '-d', '--delete'
865
mode = :delete
866
when '-U', '--update'
867
mode = :update
868
when '-u', '--up'
869
onlyup = true
870
when '-c'
871
list = val
872
if(!list)
873
print_error("Invalid column list")
874
return
875
end
876
col_search = list.strip().split(",")
877
col_search.each { |c|
878
if not @@services_columns.include? c
879
print_error("Invalid column list. Possible values are (#{@@services_columns.join("|")})")
880
return
881
end
882
}
883
when '-p'
884
unless (arg_port_range(val, port_ranges, true))
885
return
886
end
887
when '-r'
888
proto = val
889
if (!proto)
890
print_status("Invalid protocol")
891
return
892
end
893
proto = proto.strip
894
when '-s'
895
namelist = val
896
if (!namelist)
897
print_error("Invalid name list")
898
return
899
end
900
names = namelist.strip().split(",")
901
when '-o'
902
output_file = val
903
if (!output_file)
904
print_error("Invalid output filename")
905
return
906
end
907
output_file = ::File.expand_path(output_file)
908
when '-O'
909
if (order_by = val.to_i - 1) < 0
910
print_error('Please specify a column number starting from 1')
911
return
912
end
913
when '-R', '--rhosts'
914
set_rhosts = true
915
when '-S', '--search'
916
search_term = val
917
opts[:search_term] = search_term
918
when '-h', '--help'
919
cmd_services_help
920
return
921
else
922
# Anything that wasn't an option is a host to search for
923
unless (arg_host_range(val, host_ranges))
924
return
925
end
926
end
927
end
928
929
ports = port_ranges.flatten.uniq
930
931
if mode == :add
932
# Can only deal with one port and one service name at a time
933
# right now. Them's the breaks.
934
if ports.length != 1
935
print_error("Exactly one port required")
936
return
937
end
938
if host_ranges.empty?
939
print_error("Host address or range required")
940
return
941
end
942
host_ranges.each do |range|
943
range.each do |addr|
944
info = {
945
:host => addr,
946
:port => ports.first.to_i
947
}
948
info[:proto] = proto.downcase if proto
949
info[:name] = names.first.downcase if names and names.first
950
951
svc = framework.db.find_or_create_service(info)
952
print_status("Time: #{svc.created_at} Service: host=#{svc.host.address} port=#{svc.port} proto=#{svc.proto} name=#{svc.name}")
953
end
954
end
955
return
956
end
957
958
# If we got here, we're searching. Delete implies search
959
col_names = @@services_columns
960
if col_search
961
col_names = col_search
962
end
963
tbl = Rex::Text::Table.new({
964
'Header' => "Services",
965
'Columns' => ['host'] + col_names,
966
'SortIndex' => order_by
967
})
968
969
# Sentinel value meaning all
970
host_ranges.push(nil) if host_ranges.empty?
971
ports = nil if ports.empty?
972
matched_service_ids = []
973
974
each_host_range_chunk(host_ranges) do |host_search|
975
next if host_search && host_search.empty?
976
opts[:workspace] = framework.db.workspace
977
opts[:hosts] = {address: host_search} if !host_search.nil?
978
opts[:port] = ports if ports
979
framework.db.services(opts).each do |service|
980
981
unless service.state == 'open'
982
next if onlyup
983
end
984
985
host = service.host
986
matched_service_ids << service.id
987
988
if mode == :update
989
service.name = names.first if names
990
service.proto = proto if proto
991
service.port = ports.first if ports
992
framework.db.update_service(service.as_json.symbolize_keys)
993
end
994
995
columns = [host.address] + col_names.map { |n| service[n].to_s || "" }
996
tbl << columns
997
if set_rhosts
998
addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address )
999
rhosts << addr
1000
end
1001
end
1002
end
1003
1004
if (mode == :delete)
1005
result = framework.db.delete_service(ids: matched_service_ids)
1006
delete_count += result.size
1007
end
1008
1009
if (output_file == nil)
1010
print_line(tbl.to_s)
1011
else
1012
# create the output file
1013
::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) }
1014
print_status("Wrote services to #{output_file}")
1015
end
1016
1017
# Finally, handle the case where the user wants the resulting list
1018
# of hosts to go into RHOSTS.
1019
set_rhosts_from_addrs(rhosts.uniq) if set_rhosts
1020
1021
print_status("Deleted #{delete_count} services") if delete_count > 0
1022
1023
end
1024
1025
#
1026
# Tab completion for the vulns command
1027
#
1028
# @param str [String] the string currently being typed before tab was hit
1029
# @param words [Array<String>] the previously completed words on the command line. words is always
1030
# at least 1 when tab completion has reached this stage since the command itself has been completed
1031
def cmd_vulns_tabs(str, words)
1032
if words.length == 1
1033
return @@vulns_opts.option_keys.select { |opt| opt.start_with?(str) }
1034
end
1035
case words[-1]
1036
when '-o', '--output'
1037
return tab_complete_filenames(str, words)
1038
end
1039
end
1040
1041
def cmd_vulns_help
1042
print_line "Print all vulnerabilities in the database"
1043
print_line
1044
print_line "Usage: vulns [addr range]"
1045
print_line
1046
print @@vulns_opts.usage
1047
print_line
1048
print_line "Examples:"
1049
print_line " vulns -p 1-65536 # only vulns with associated services"
1050
print_line " vulns -p 1-65536 -s http # identified as http on any port"
1051
print_line
1052
end
1053
1054
@@vulns_opts = Rex::Parser::Arguments.new(
1055
[ '-h', '--help' ] => [ false, 'Show this help information.' ],
1056
[ '-o', '--output' ] => [ true, 'Send output to a file in csv format.', '<filename>' ],
1057
[ '-p', '--port' ] => [ true, 'List vulns matching this port spec.', '<port>' ],
1058
[ '-s', '--service' ] => [ true, 'List vulns matching these service names.', '<name>' ],
1059
[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],
1060
[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],
1061
[ '-i', '--info' ] => [ false, 'Display vuln information.' ],
1062
[ '-d', '--delete' ] => [ false, 'Delete vulnerabilities. Not officially supported.' ],
1063
[ '-v', '--verbose' ] => [ false, 'Display additional information.' ]
1064
)
1065
1066
def cmd_vulns(*args)
1067
return unless active?
1068
1069
default_columns = ['Timestamp', 'Host', 'Name', 'References']
1070
host_ranges = []
1071
port_ranges = []
1072
svcs = []
1073
rhosts = []
1074
1075
search_term = nil
1076
show_info = false
1077
show_vuln_attempts = false
1078
set_rhosts = false
1079
output_file = nil
1080
delete_count = 0
1081
1082
mode = nil
1083
1084
@@vulns_opts.parse(args) do |opt, idx, val|
1085
case opt
1086
when '-d', '--delete' # TODO: This is currently undocumented because it's not officially supported.
1087
mode = :delete
1088
when '-h', '--help'
1089
cmd_vulns_help
1090
return
1091
when '-o', '--output'
1092
output_file = val
1093
if output_file
1094
output_file = File.expand_path(output_file)
1095
else
1096
print_error("Invalid output filename")
1097
return
1098
end
1099
when '-p', '--port'
1100
unless (arg_port_range(val, port_ranges, true))
1101
return
1102
end
1103
when '-s', '--service'
1104
service = val
1105
if (!service)
1106
print_error("Argument required for -s")
1107
return
1108
end
1109
svcs = service.split(/[\s]*,[\s]*/)
1110
when '-R', '--rhosts'
1111
set_rhosts = true
1112
when '-S', '--search'
1113
search_term = val
1114
when '-i', '--info'
1115
show_info = true
1116
when '-v', '--verbose'
1117
show_vuln_attempts = true
1118
else
1119
# Anything that wasn't an option is a host to search for
1120
unless (arg_host_range(val, host_ranges))
1121
return
1122
end
1123
end
1124
end
1125
1126
if show_info
1127
default_columns << 'Information'
1128
end
1129
1130
# add sentinel value meaning all if empty
1131
host_ranges.push(nil) if host_ranges.empty?
1132
# normalize
1133
ports = port_ranges.flatten.uniq
1134
svcs.flatten!
1135
tbl = Rex::Text::Table.new(
1136
'Header' => 'Vulnerabilities',
1137
'Columns' => default_columns
1138
)
1139
1140
matched_vuln_ids = []
1141
vulns = []
1142
if host_ranges.compact.empty?
1143
vulns = framework.db.vulns({:search_term => search_term})
1144
else
1145
each_host_range_chunk(host_ranges) do |host_search|
1146
next if host_search && host_search.empty?
1147
1148
vulns.concat(framework.db.vulns({:hosts => { :address => host_search }, :search_term => search_term }))
1149
end
1150
end
1151
1152
vulns.each do |vuln|
1153
reflist = vuln.refs.map {|r| r.name}
1154
if (vuln.service)
1155
# Skip this one if the user specified a port and it
1156
# doesn't match.
1157
next unless ports.empty? or ports.include? vuln.service.port
1158
# Same for service names
1159
next unless svcs.empty? or svcs.include?(vuln.service.name)
1160
else
1161
# This vuln has no service, so it can't match
1162
next unless ports.empty? and svcs.empty?
1163
end
1164
1165
matched_vuln_ids << vuln.id
1166
1167
row = []
1168
row << vuln.created_at
1169
row << vuln.host.address
1170
row << vuln.name
1171
row << reflist.join(',')
1172
if show_info
1173
row << vuln.info
1174
end
1175
tbl << row
1176
1177
if set_rhosts
1178
addr = (vuln.host.scope.to_s != "" ? vuln.host.address + '%' + vuln.host.scope : vuln.host.address)
1179
rhosts << addr
1180
end
1181
end
1182
1183
if mode == :delete
1184
result = framework.db.delete_vuln(ids: matched_vuln_ids)
1185
delete_count = result.size
1186
end
1187
1188
if output_file
1189
if show_vuln_attempts
1190
print_warning("Cannot output to a file when verbose mode is enabled. Please remove verbose flag and try again.")
1191
else
1192
File.write(output_file, tbl.to_csv)
1193
print_status("Wrote vulnerability information to #{output_file}")
1194
end
1195
else
1196
print_line
1197
if show_vuln_attempts
1198
vulns_and_attempts = _format_vulns_and_vuln_attempts(vulns)
1199
_print_vulns_and_attempts(vulns_and_attempts)
1200
else
1201
print_line(tbl.to_s)
1202
end
1203
end
1204
1205
# Finally, handle the case where the user wants the resulting list
1206
# of hosts to go into RHOSTS.
1207
set_rhosts_from_addrs(rhosts.uniq) if set_rhosts
1208
1209
print_status("Deleted #{delete_count} vulnerabilities") if delete_count > 0
1210
end
1211
1212
#
1213
# Tab completion for the notes command
1214
#
1215
# @param str [String] the string currently being typed before tab was hit
1216
# @param words [Array<String>] the previously completed words on the command line. words is always
1217
# at least 1 when tab completion has reached this stage since the command itself has been completed
1218
def cmd_notes_tabs(str, words)
1219
if words.length == 1
1220
return @@notes_opts.option_keys.select { |opt| opt.start_with?(str) }
1221
end
1222
1223
case words[-1]
1224
when '-O', '--order'
1225
return []
1226
when '-o', '--output'
1227
return tab_complete_filenames(str, words)
1228
end
1229
1230
[]
1231
end
1232
1233
def cmd_notes_help
1234
print_line "Usage: notes [-h] [-t <type1,type2>] [-n <data string>] [-a] [addr range]"
1235
print_line
1236
print @@notes_opts.usage
1237
print_line
1238
print_line "Examples:"
1239
print_line " notes --add -t apps -n 'winzip' 10.1.1.34 10.1.20.41"
1240
print_line " notes -t smb.fingerprint 10.1.1.34 10.1.20.41"
1241
print_line " notes -S 'nmap.nse.(http|rtsp)'"
1242
print_line
1243
end
1244
1245
@@notes_opts = Rex::Parser::Arguments.new(
1246
[ '-a', '--add' ] => [ false, 'Add a note to the list of addresses, instead of listing.' ],
1247
[ '-d', '--delete' ] => [ false, 'Delete the notes instead of searching.' ],
1248
[ '-h', '--help' ] => [ false, 'Show this help information.' ],
1249
[ '-n', '--note' ] => [ true, 'Set the data for a new note (only with -a).', '<note>' ],
1250
[ '-O', '--order' ] => [ true, 'Order rows by specified column number.', '<column id>' ],
1251
[ '-o', '--output' ] => [ true, 'Save the notes to a csv file.', '<filename>' ],
1252
[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],
1253
[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],
1254
[ '-t', '--type' ] => [ true, 'Search for a list of types, or set single type for add.', '<type1,type2>' ],
1255
[ '-u', '--update' ] => [ false, 'Update a note. Not officially supported.' ]
1256
)
1257
1258
def cmd_notes(*args)
1259
return unless active?
1260
::ApplicationRecord.connection_pool.with_connection {
1261
mode = :search
1262
data = nil
1263
types = nil
1264
set_rhosts = false
1265
1266
host_ranges = []
1267
rhosts = []
1268
search_term = nil
1269
output_file = nil
1270
delete_count = 0
1271
order_by = nil
1272
1273
@@notes_opts.parse(args) do |opt, idx, val|
1274
case opt
1275
when '-a', '--add'
1276
mode = :add
1277
when '-d', '--delete'
1278
mode = :delete
1279
when '-n', '--note'
1280
data = val
1281
if(!data)
1282
print_error("Can't make a note with no data")
1283
return
1284
end
1285
when '-t', '--type'
1286
typelist = val
1287
if(!typelist)
1288
print_error("Invalid type list")
1289
return
1290
end
1291
types = typelist.strip().split(",")
1292
when '-R', '--rhosts'
1293
set_rhosts = true
1294
when '-S', '--search'
1295
search_term = val
1296
when '-o', '--output'
1297
output_file = val
1298
output_file = ::File.expand_path(output_file)
1299
when '-O'
1300
if (order_by = val.to_i - 1) < 0
1301
print_error('Please specify a column number starting from 1')
1302
return
1303
end
1304
when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported.
1305
mode = :update
1306
when '-h', '--help'
1307
cmd_notes_help
1308
return
1309
else
1310
# Anything that wasn't an option is a host to search for
1311
unless (arg_host_range(val, host_ranges))
1312
return
1313
end
1314
end
1315
end
1316
1317
if mode == :add
1318
if host_ranges.compact.empty?
1319
print_error("Host address or range required")
1320
return
1321
end
1322
1323
if types.nil? || types.size != 1
1324
print_error("Exactly one type is required")
1325
return
1326
end
1327
1328
if data.nil?
1329
print_error("Data required")
1330
return
1331
end
1332
1333
type = types.first
1334
host_ranges.each { |range|
1335
range.each { |addr|
1336
note = framework.db.find_or_create_note(host: addr, type: type, data: data)
1337
break if not note
1338
print_status("Time: #{note.created_at} Note: host=#{addr} type=#{note.ntype} data=#{note.data}")
1339
}
1340
}
1341
return
1342
end
1343
1344
if mode == :update
1345
if !types.nil? && types.size != 1
1346
print_error("Exactly one type is required")
1347
return
1348
end
1349
1350
if types.nil? && data.nil?
1351
print_error("Update requires data or type")
1352
return
1353
end
1354
end
1355
1356
note_list = []
1357
if host_ranges.compact.empty?
1358
# No host specified - collect all notes
1359
opts = {search_term: search_term}
1360
opts[:ntype] = types if mode != :update && types && !types.empty?
1361
note_list = framework.db.notes(opts)
1362
else
1363
# Collect notes of specified hosts
1364
each_host_range_chunk(host_ranges) do |host_search|
1365
next if host_search && host_search.empty?
1366
1367
opts = {hosts: {address: host_search}, workspace: framework.db.workspace, search_term: search_term}
1368
opts[:ntype] = types if mode != :update && types && !types.empty?
1369
note_list.concat(framework.db.notes(opts))
1370
end
1371
end
1372
1373
# Now display them
1374
table = Rex::Text::Table.new(
1375
'Header' => 'Notes',
1376
'Indent' => 1,
1377
'Columns' => ['Time', 'Host', 'Service', 'Port', 'Protocol', 'Type', 'Data'],
1378
'SortIndex' => order_by
1379
)
1380
1381
matched_note_ids = []
1382
note_list.each do |note|
1383
if mode == :update
1384
begin
1385
update_opts = {id: note.id}
1386
unless types.nil?
1387
note.ntype = types.first
1388
update_opts[:ntype] = types.first
1389
end
1390
1391
unless data.nil?
1392
note.data = data
1393
update_opts[:data] = data
1394
end
1395
1396
framework.db.update_note(update_opts)
1397
rescue => e
1398
elog "There was an error updating note with ID #{note.id}: #{e.message}"
1399
next
1400
end
1401
end
1402
1403
matched_note_ids << note.id
1404
1405
row = []
1406
row << note.created_at
1407
1408
if note.host
1409
host = note.host
1410
row << host.address
1411
if set_rhosts
1412
addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address)
1413
rhosts << addr
1414
end
1415
else
1416
row << ''
1417
end
1418
1419
if note.service
1420
row << note.service.name || ''
1421
row << note.service.port || ''
1422
row << note.service.proto || ''
1423
else
1424
row << '' # For the Service field
1425
row << '' # For the Port field
1426
row << '' # For the Protocol field
1427
end
1428
1429
row << note.ntype
1430
row << note.data.inspect
1431
table << row
1432
end
1433
1434
if mode == :delete
1435
result = framework.db.delete_note(ids: matched_note_ids)
1436
delete_count = result.size
1437
end
1438
1439
if output_file
1440
save_csv_notes(output_file, table)
1441
else
1442
print_line
1443
print_line(table.to_s)
1444
end
1445
1446
# Finally, handle the case where the user wants the resulting list
1447
# of hosts to go into RHOSTS.
1448
set_rhosts_from_addrs(rhosts.uniq) if set_rhosts
1449
1450
print_status("Deleted #{delete_count} notes") if delete_count > 0
1451
}
1452
end
1453
1454
def save_csv_notes(fpath, table)
1455
begin
1456
File.open(fpath, 'wb') do |f|
1457
f.write(table.to_csv)
1458
end
1459
print_status("Wrote notes to #{fpath}")
1460
rescue Errno::EACCES => e
1461
print_error("Unable to save notes. #{e.message}")
1462
end
1463
end
1464
1465
#
1466
# Tab completion for the loot command
1467
#
1468
# @param str [String] the string currently being typed before tab was hit
1469
# @param words [Array<String>] the previously completed words on the command line. words is always
1470
# at least 1 when tab completion has reached this stage since the command itself has been completed
1471
def cmd_loot_tabs(str, words)
1472
if words.length == 1
1473
@@loot_opts.option_keys.select { |opt| opt.start_with?(str) }
1474
end
1475
end
1476
1477
def cmd_loot_help
1478
print_line "Usage: loot [options]"
1479
print_line " Info: loot [-h] [addr1 addr2 ...] [-t <type1,type2>]"
1480
print_line " Add: loot -f [fname] -i [info] -a [addr1 addr2 ...] -t [type]"
1481
print_line " Del: loot -d [addr1 addr2 ...]"
1482
print_line
1483
print @@loot_opts.usage
1484
print_line
1485
end
1486
1487
@@loot_opts = Rex::Parser::Arguments.new(
1488
[ '-a', '--add' ] => [ false, 'Add loot to the list of addresses, instead of listing.' ],
1489
[ '-d', '--delete' ] => [ false, 'Delete *all* loot matching host and type.' ],
1490
[ '-f', '--file' ] => [ true, 'File with contents of the loot to add.', '<filename>' ],
1491
[ '-i', '--info' ] => [ true, 'Info of the loot to add.', '<info>' ],
1492
[ '-t', '--type' ] => [ true, 'Search for a list of types.', '<type1,type2>' ],
1493
[ '-h', '--help' ] => [ false, 'Show this help information.' ],
1494
[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],
1495
[ '-u', '--update' ] => [ false, 'Update loot. Not officially supported.' ]
1496
)
1497
1498
def cmd_loot(*args)
1499
return unless active?
1500
1501
mode = :search
1502
host_ranges = []
1503
types = nil
1504
delete_count = 0
1505
search_term = nil
1506
file = nil
1507
name = nil
1508
info = nil
1509
filename = nil
1510
1511
@@loot_opts.parse(args) do |opt, idx, val|
1512
case opt
1513
when '-a', '--add'
1514
mode = :add
1515
when '-d', '--delete'
1516
mode = :delete
1517
when '-f', '--file'
1518
filename = val
1519
if(!filename)
1520
print_error("Can't make loot with no filename")
1521
return
1522
end
1523
if (!File.exist?(filename) or !File.readable?(filename))
1524
print_error("Can't read file")
1525
return
1526
end
1527
when '-i', '--info'
1528
info = val
1529
if(!info)
1530
print_error("Can't make loot with no info")
1531
return
1532
end
1533
when '-t', '--type'
1534
typelist = val
1535
if(!typelist)
1536
print_error("Invalid type list")
1537
return
1538
end
1539
types = typelist.strip().split(",")
1540
when '-S', '--search'
1541
search_term = val
1542
when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported.
1543
mode = :update
1544
when '-h', '--help'
1545
cmd_loot_help
1546
return
1547
else
1548
# Anything that wasn't an option is a host to search for
1549
unless (arg_host_range(val, host_ranges))
1550
return
1551
end
1552
end
1553
end
1554
1555
tbl = Rex::Text::Table.new({
1556
'Header' => "Loot",
1557
'Columns' => [ 'host', 'service', 'type', 'name', 'content', 'info', 'path' ],
1558
# For now, don't perform any word wrapping on the loot table as it breaks the workflow of
1559
# copying paths and pasting them into applications
1560
'WordWrap' => false,
1561
})
1562
1563
# Sentinel value meaning all
1564
host_ranges.push(nil) if host_ranges.empty?
1565
1566
if mode == :add
1567
if host_ranges.compact.empty?
1568
print_error('Address list required')
1569
return
1570
end
1571
if info.nil?
1572
print_error("Info required")
1573
return
1574
end
1575
if filename.nil?
1576
print_error("Loot file required")
1577
return
1578
end
1579
if types.nil? or types.size != 1
1580
print_error("Exactly one loot type is required")
1581
return
1582
end
1583
type = types.first
1584
name = File.basename(filename)
1585
file = File.open(filename, "rb")
1586
contents = file.read
1587
host_ranges.each do |range|
1588
range.each do |host|
1589
lootfile = framework.db.find_or_create_loot(:type => type, :host => host, :info => info, :data => contents, :path => filename, :name => name)
1590
print_status("Added loot for #{host} (#{lootfile})")
1591
end
1592
end
1593
return
1594
end
1595
1596
matched_loot_ids = []
1597
loots = []
1598
if host_ranges.compact.empty?
1599
loots = loots + framework.db.loots(workspace: framework.db.workspace, search_term: search_term)
1600
else
1601
each_host_range_chunk(host_ranges) do |host_search|
1602
next if host_search && host_search.empty?
1603
1604
loots = loots + framework.db.loots(workspace: framework.db.workspace, hosts: { address: host_search }, search_term: search_term)
1605
end
1606
end
1607
1608
loots.each do |loot|
1609
row = []
1610
# TODO: This is just a temp implementation of update for the time being since it did not exist before.
1611
# It should be updated to not pass all of the attributes attached to the object, only the ones being updated.
1612
if mode == :update
1613
begin
1614
loot.info = info if info
1615
if types && types.size > 1
1616
print_error "May only pass 1 type when performing an update."
1617
next
1618
end
1619
loot.ltype = types.first if types
1620
framework.db.update_loot(loot.as_json.symbolize_keys)
1621
rescue => e
1622
elog "There was an error updating loot with ID #{loot.id}: #{e.message}"
1623
next
1624
end
1625
end
1626
row.push (loot.host && loot.host.address) ? loot.host.address : ""
1627
if (loot.service)
1628
svc = (loot.service.name ? loot.service.name : "#{loot.service.port}/#{loot.service.proto}")
1629
row.push svc
1630
else
1631
row.push ""
1632
end
1633
row.push(loot.ltype)
1634
row.push(loot.name || "")
1635
row.push(loot.content_type)
1636
row.push(loot.info || "")
1637
row.push(loot.path)
1638
1639
tbl << row
1640
matched_loot_ids << loot.id
1641
end
1642
1643
if (mode == :delete)
1644
result = framework.db.delete_loot(ids: matched_loot_ids)
1645
delete_count = result.size
1646
end
1647
1648
print_line
1649
print_line(tbl.to_s)
1650
print_status("Deleted #{delete_count} loots") if delete_count > 0
1651
end
1652
1653
# :category: Deprecated Commands
1654
def cmd_db_hosts_help; deprecated_help(:hosts); end
1655
# :category: Deprecated Commands
1656
def cmd_db_notes_help; deprecated_help(:notes); end
1657
# :category: Deprecated Commands
1658
def cmd_db_vulns_help; deprecated_help(:vulns); end
1659
# :category: Deprecated Commands
1660
def cmd_db_services_help; deprecated_help(:services); end
1661
# :category: Deprecated Commands
1662
def cmd_db_autopwn_help; deprecated_help; end
1663
# :category: Deprecated Commands
1664
def cmd_db_driver_help; deprecated_help; end
1665
1666
# :category: Deprecated Commands
1667
def cmd_db_hosts(*args); deprecated_cmd(:hosts, *args); end
1668
# :category: Deprecated Commands
1669
def cmd_db_notes(*args); deprecated_cmd(:notes, *args); end
1670
# :category: Deprecated Commands
1671
def cmd_db_vulns(*args); deprecated_cmd(:vulns, *args); end
1672
# :category: Deprecated Commands
1673
def cmd_db_services(*args); deprecated_cmd(:services, *args); end
1674
# :category: Deprecated Commands
1675
def cmd_db_autopwn(*args); deprecated_cmd; end
1676
1677
#
1678
# :category: Deprecated Commands
1679
#
1680
# This one deserves a little more explanation than standard deprecation
1681
# warning, so give the user a better understanding of what's going on.
1682
#
1683
def cmd_db_driver(*args)
1684
deprecated_cmd
1685
print_line
1686
print_line "Because Metasploit no longer supports databases other than the default"
1687
print_line "PostgreSQL, there is no longer a need to set the driver. Thus db_driver"
1688
print_line "is not useful and its functionality has been removed. Usually Metasploit"
1689
print_line "will already have connected to the database; check db_status to see."
1690
print_line
1691
cmd_db_status
1692
end
1693
1694
def cmd_db_import_tabs(str, words)
1695
tab_complete_filenames(str, words)
1696
end
1697
1698
def cmd_db_import_help
1699
print_line "Usage: db_import <filename> [file2...]"
1700
print_line
1701
print_line "Filenames can be globs like *.xml, or **/*.xml which will search recursively"
1702
print_line "Currently supported file types include:"
1703
print_line " Acunetix"
1704
print_line " Amap Log"
1705
print_line " Amap Log -m"
1706
print_line " Appscan"
1707
print_line " Burp Session XML"
1708
print_line " Burp Issue XML"
1709
print_line " CI"
1710
print_line " Foundstone"
1711
print_line " FusionVM XML"
1712
print_line " Group Policy Preferences Credentials"
1713
print_line " IP Address List"
1714
print_line " IP360 ASPL"
1715
print_line " IP360 XML v3"
1716
print_line " Libpcap Packet Capture"
1717
print_line " Masscan XML"
1718
print_line " Metasploit PWDump Export"
1719
print_line " Metasploit XML"
1720
print_line " Metasploit Zip Export"
1721
print_line " Microsoft Baseline Security Analyzer"
1722
print_line " NeXpose Simple XML"
1723
print_line " NeXpose XML Report"
1724
print_line " Nessus NBE Report"
1725
print_line " Nessus XML (v1)"
1726
print_line " Nessus XML (v2)"
1727
print_line " NetSparker XML"
1728
print_line " Nikto XML"
1729
print_line " Nmap XML"
1730
print_line " OpenVAS Report"
1731
print_line " OpenVAS XML (optional arguments -cert -dfn)"
1732
print_line " Outpost24 XML"
1733
print_line " Qualys Asset XML"
1734
print_line " Qualys Scan XML"
1735
print_line " Retina XML"
1736
print_line " Spiceworks CSV Export"
1737
print_line " Wapiti XML"
1738
print_line
1739
end
1740
1741
#
1742
# Generic import that automatically detects the file type
1743
#
1744
def cmd_db_import(*args)
1745
return unless active?
1746
openvas_cert = false
1747
openvas_dfn = false
1748
::ApplicationRecord.connection_pool.with_connection {
1749
if args.include?("-h") || ! (args && args.length > 0)
1750
cmd_db_import_help
1751
return
1752
end
1753
if args.include?("-dfn")
1754
openvas_dfn = true
1755
end
1756
if args.include?("-cert")
1757
openvas_cert = true
1758
end
1759
options = {:openvas_dfn => openvas_dfn, :openvas_cert => openvas_cert}
1760
args.each { |glob|
1761
next if (glob.include?("-cert") || glob.include?("-dfn"))
1762
files = ::Dir.glob(::File.expand_path(glob))
1763
if files.empty?
1764
print_error("No such file #{glob}")
1765
next
1766
end
1767
files.each { |filename|
1768
if (not ::File.readable?(filename))
1769
print_error("Could not read file #{filename}")
1770
next
1771
end
1772
begin
1773
warnings = 0
1774
framework.db.import_file(:filename => filename, :options => options) do |type,data|
1775
case type
1776
when :debug
1777
print_error("DEBUG: #{data.inspect}")
1778
when :vuln
1779
inst = data[1] == 1 ? "instance" : "instances"
1780
print_status("Importing vulnerability '#{data[0]}' (#{data[1]} #{inst})")
1781
when :filetype
1782
print_status("Importing '#{data}' data")
1783
when :parser
1784
print_status("Import: Parsing with '#{data}'")
1785
when :address
1786
print_status("Importing host #{data}")
1787
when :service
1788
print_status("Importing service #{data}")
1789
when :msf_loot
1790
print_status("Importing loot #{data}")
1791
when :msf_task
1792
print_status("Importing task #{data}")
1793
when :msf_report
1794
print_status("Importing report #{data}")
1795
when :pcap_count
1796
print_status("Import: #{data} packets processed")
1797
when :record_count
1798
print_status("Import: #{data[1]} records processed")
1799
when :warning
1800
print_error
1801
data.split("\n").each do |line|
1802
print_error(line)
1803
end
1804
print_error
1805
warnings += 1
1806
end
1807
end
1808
print_status("Successfully imported #{filename}")
1809
1810
print_error("Please note that there were #{warnings} warnings") if warnings > 1
1811
print_error("Please note that there was one warning") if warnings == 1
1812
1813
rescue Msf::DBImportError => e
1814
print_error("Failed to import #{filename}: #{$!}")
1815
elog("Failed to import #{filename}", error: e)
1816
dlog("Call stack: #{$@.join("\n")}", LEV_3)
1817
next
1818
rescue REXML::ParseException => e
1819
print_error("Failed to import #{filename} due to malformed XML:")
1820
print_error("#{e.class}: #{e}")
1821
elog("Failed to import #{filename}", error: e)
1822
dlog("Call stack: #{$@.join("\n")}", LEV_3)
1823
next
1824
end
1825
}
1826
}
1827
}
1828
end
1829
1830
def cmd_db_export_help
1831
# Like db_hosts and db_services, this creates a list of columns, so
1832
# use its -h
1833
cmd_db_export("-h")
1834
end
1835
1836
#
1837
# Export an XML
1838
#
1839
def cmd_db_export(*args)
1840
return unless active?
1841
::ApplicationRecord.connection_pool.with_connection {
1842
1843
export_formats = %W{xml pwdump}
1844
format = 'xml'
1845
output = nil
1846
1847
while (arg = args.shift)
1848
case arg
1849
when '-h','--help'
1850
print_line "Usage:"
1851
print_line " db_export -f <format> [filename]"
1852
print_line " Format can be one of: #{export_formats.join(", ")}"
1853
when '-f','--format'
1854
format = args.shift.to_s.downcase
1855
else
1856
output = arg
1857
end
1858
end
1859
1860
if not output
1861
print_error("No output file was specified")
1862
return
1863
end
1864
1865
if not export_formats.include?(format)
1866
print_error("Unsupported file format: #{format}")
1867
print_error("Unsupported file format: '#{format}'. Must be one of: #{export_formats.join(", ")}")
1868
return
1869
end
1870
1871
print_status("Starting export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...")
1872
framework.db.run_db_export(output, format)
1873
print_status("Finished export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...")
1874
}
1875
end
1876
1877
def find_nmap_path
1878
Rex::FileUtils.find_full_path("nmap") || Rex::FileUtils.find_full_path("nmap.exe")
1879
end
1880
1881
#
1882
# Import Nmap data from a file
1883
#
1884
def cmd_db_nmap(*args)
1885
return unless active?
1886
::ApplicationRecord.connection_pool.with_connection {
1887
if (args.length == 0)
1888
print_status("Usage: db_nmap [--save | [--help | -h]] [nmap options]")
1889
return
1890
end
1891
1892
save = false
1893
arguments = []
1894
while (arg = args.shift)
1895
case arg
1896
when '--save'
1897
save = true
1898
when '--help', '-h'
1899
cmd_db_nmap_help
1900
return
1901
else
1902
arguments << arg
1903
end
1904
end
1905
1906
nmap = find_nmap_path
1907
unless nmap
1908
print_error("The nmap executable could not be found")
1909
return
1910
end
1911
1912
fd = Rex::Quickfile.new(['msf-db-nmap-', '.xml'], Msf::Config.local_directory)
1913
1914
begin
1915
# When executing native Nmap in Cygwin, expand the Cygwin path to a Win32 path
1916
if(Rex::Compat.is_cygwin and nmap =~ /cygdrive/)
1917
# Custom function needed because cygpath breaks on 8.3 dirs
1918
tout = Rex::Compat.cygwin_to_win32(fd.path)
1919
arguments.push('-oX', tout)
1920
else
1921
arguments.push('-oX', fd.path)
1922
end
1923
1924
run_nmap(nmap, arguments)
1925
1926
framework.db.import_nmap_xml_file(:filename => fd.path)
1927
1928
print_status("Saved NMAP XML results to #{fd.path}") if save
1929
ensure
1930
fd.close
1931
fd.unlink unless save
1932
end
1933
}
1934
end
1935
1936
def cmd_db_nmap_help
1937
nmap = find_nmap_path
1938
unless nmap
1939
print_error("The nmap executable could not be found")
1940
return
1941
end
1942
1943
stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help')
1944
1945
stdout.each_line do |out_line|
1946
next if out_line.strip.empty?
1947
print_status(out_line.strip)
1948
end
1949
1950
stderr.each_line do |err_line|
1951
next if err_line.strip.empty?
1952
print_error(err_line.strip)
1953
end
1954
end
1955
1956
def cmd_db_nmap_tabs(str, words)
1957
nmap = find_nmap_path
1958
unless nmap
1959
return
1960
end
1961
1962
stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help')
1963
tabs = []
1964
stdout.each_line do |out_line|
1965
if out_line.strip.starts_with?('-')
1966
tabs.push(out_line.strip.split(':').first)
1967
end
1968
end
1969
1970
stderr.each_line do |err_line|
1971
next if err_line.strip.empty?
1972
print_error(err_line.strip)
1973
end
1974
1975
return tabs
1976
end
1977
1978
#
1979
# Database management
1980
#
1981
def db_check_driver
1982
unless framework.db.driver
1983
print_error("No database driver installed.")
1984
return false
1985
end
1986
true
1987
end
1988
1989
#
1990
# Is everything working?
1991
#
1992
def cmd_db_status(*args)
1993
return if not db_check_driver
1994
1995
if framework.db.connection_established?
1996
print_connection_info
1997
else
1998
print_status("#{framework.db.driver} selected, no connection")
1999
end
2000
end
2001
2002
2003
def cmd_db_connect_help
2004
print_line(" USAGE:")
2005
print_line(" * Postgres Data Service:")
2006
print_line(" db_connect <user:[pass]>@<host:[port]>/<database>")
2007
print_line(" Examples:")
2008
print_line(" db_connect user@metasploit3")
2009
print_line(" db_connect user:[email protected]/metasploit3")
2010
print_line(" db_connect user:[email protected]:1500/metasploit3")
2011
print_line(" db_connect -y [path/to/database.yml]")
2012
print_line(" ")
2013
print_line(" * HTTP Data Service:")
2014
print_line(" db_connect [options] <http|https>://<host:[port]>")
2015
print_line(" Examples:")
2016
print_line(" db_connect http://localhost:8080")
2017
print_line(" db_connect http://my-super-msf-data.service.com")
2018
print_line(" db_connect -c ~/cert.pem -t 6a7a74c1a5003802c955ead1bbddd4ab1b05a7f2940b4732d34bfc555bc6e1c5d7611a497b29e8f0 https://localhost:8080")
2019
print_line(" NOTE: You must be connected to a Postgres data service in order to successfully connect to a HTTP data service.")
2020
print_line(" ")
2021
print_line(" Persisting Connections:")
2022
print_line(" db_connect --name <name to save connection as> [options] <address>")
2023
print_line(" Examples:")
2024
print_line(" Saving: db_connect --name LA-server http://123.123.123.45:1234")
2025
print_line(" Connecting: db_connect LA-server")
2026
print_line(" ")
2027
print_line(" OPTIONS:")
2028
print_line(" -l,--list-services List the available data services that have been previously saved.")
2029
print_line(" -y,--yaml Connect to the data service specified in the provided database.yml file.")
2030
print_line(" -n,--name Name used to store the connection. Providing an existing name will overwrite the settings for that connection.")
2031
print_line(" -c,--cert Certificate file matching the remote data server's certificate. Needed when using self-signed SSL cert.")
2032
print_line(" -t,--token The API token used to authenticate to the remote data service.")
2033
print_line(" --skip-verify Skip validating authenticity of server's certificate (NOT RECOMMENDED).")
2034
print_line("")
2035
end
2036
2037
def cmd_db_connect(*args)
2038
return if not db_check_driver
2039
2040
opts = {}
2041
while (arg = args.shift)
2042
case arg
2043
when '-h', '--help'
2044
cmd_db_connect_help
2045
return
2046
when '-y', '--yaml'
2047
opts[:yaml_file] = args.shift
2048
when '-c', '--cert'
2049
opts[:cert] = args.shift
2050
when '-t', '--token'
2051
opts[:api_token] = args.shift
2052
when '-l', '--list-services'
2053
list_saved_data_services
2054
return
2055
when '-n', '--name'
2056
opts[:name] = args.shift
2057
if opts[:name] =~ /\/|\[|\]/
2058
print_error "Provided name contains an invalid character. Aborting connection."
2059
return
2060
end
2061
when '--skip-verify'
2062
opts[:skip_verify] = true
2063
else
2064
found_name = ::Msf::DbConnector.data_service_search(name: arg)
2065
if found_name
2066
opts = ::Msf::DbConnector.load_db_config(found_name)
2067
else
2068
opts[:url] = arg
2069
end
2070
end
2071
end
2072
2073
if !opts[:url] && !opts[:yaml_file]
2074
print_error 'A URL or saved data service name is required.'
2075
print_line
2076
cmd_db_connect_help
2077
return
2078
end
2079
2080
if opts[:url] =~ /http/
2081
new_conn_type = 'http'
2082
else
2083
new_conn_type = framework.db.driver
2084
end
2085
2086
# Currently only able to be connected to one DB at a time
2087
if framework.db.connection_established?
2088
# But the http connection still requires a local database to support AR, so we have to allow that
2089
# Don't allow more than one HTTP service, though
2090
if new_conn_type != 'http' || framework.db.get_services_metadata.count >= 2
2091
print_error('Connection already established. Only one connection is allowed at a time.')
2092
print_error('Run db_disconnect first if you wish to connect to a different data service.')
2093
print_line
2094
print_line 'Current connection information:'
2095
print_connection_info
2096
return
2097
end
2098
end
2099
2100
result = Msf::DbConnector.db_connect(framework, opts)
2101
if result[:error]
2102
print_error result[:error]
2103
return
2104
end
2105
2106
if result[:result]
2107
print_status result[:result]
2108
end
2109
if framework.db.active
2110
name = opts[:name]
2111
if !name || name.empty?
2112
if found_name
2113
name = found_name
2114
elsif result[:data_service_name]
2115
name = result[:data_service_name]
2116
else
2117
name = Rex::Text.rand_text_alphanumeric(8)
2118
end
2119
end
2120
2121
save_db_to_config(framework.db, name)
2122
@current_data_service = name
2123
end
2124
end
2125
2126
def cmd_db_disconnect_help
2127
print_line "Usage:"
2128
print_line " db_disconnect Temporarily disconnects from the currently configured dataservice."
2129
print_line " db_disconnect --clear Clears the default dataservice that msfconsole will use when opened."
2130
print_line
2131
end
2132
2133
def cmd_db_disconnect(*args)
2134
return if not db_check_driver
2135
2136
if args[0] == '-h' || args[0] == '--help'
2137
cmd_db_disconnect_help
2138
return
2139
elsif args[0] == '-c' || args[0] == '--clear'
2140
clear_default_db
2141
return
2142
end
2143
2144
previous_name = framework.db.name
2145
result = Msf::DbConnector.db_disconnect(framework)
2146
2147
if result[:error]
2148
print_error "Unable to disconnect from the data service: #{@current_data_service}"
2149
print_error result[:error]
2150
elsif result[:old_data_service_name].nil?
2151
print_error 'Not currently connected to a data service.'
2152
else
2153
print_line "Successfully disconnected from the data service: #{previous_name}."
2154
@current_data_service = result[:data_service_name]
2155
if @current_data_service
2156
print_line "Now connected to: #{@current_data_service}."
2157
end
2158
end
2159
end
2160
2161
def cmd_db_rebuild_cache(*args)
2162
print_line "This command is deprecated with Metasploit 5"
2163
end
2164
2165
def cmd_db_save_help
2166
print_line "Usage: db_save"
2167
print_line
2168
print_line "Save the current data service connection as the default to reconnect on startup."
2169
print_line
2170
end
2171
2172
def cmd_db_save(*args)
2173
while (arg = args.shift)
2174
case arg
2175
when '-h', '--help'
2176
cmd_db_save_help
2177
return
2178
end
2179
end
2180
2181
if !framework.db.active || !@current_data_service
2182
print_error "Not currently connected to a data service that can be saved."
2183
return
2184
end
2185
2186
begin
2187
Msf::Config.save(DB_CONFIG_PATH => { 'default_db' => @current_data_service })
2188
print_line "Successfully saved data service as default: #{@current_data_service}"
2189
rescue ArgumentError => e
2190
print_error e.message
2191
end
2192
end
2193
2194
def save_db_to_config(database, database_name)
2195
if database_name =~ /\/|\[|\]/
2196
raise ArgumentError, 'Data service name contains an invalid character.'
2197
end
2198
config_path = "#{DB_CONFIG_PATH}/#{database_name}"
2199
config_opts = {}
2200
if !database.is_local?
2201
begin
2202
config_opts['url'] = database.endpoint
2203
if database.https_opts
2204
config_opts['cert'] = database.https_opts[:cert] if database.https_opts[:cert]
2205
config_opts['skip_verify'] = true if database.https_opts[:skip_verify]
2206
end
2207
if database.api_token
2208
config_opts['api_token'] = database.api_token
2209
end
2210
Msf::Config.save(config_path => config_opts)
2211
rescue => e
2212
print_error "There was an error saving the data service configuration: #{e.message}"
2213
end
2214
else
2215
url = Msf::DbConnector.build_postgres_url
2216
config_opts['url'] = url
2217
Msf::Config.save(config_path => config_opts)
2218
end
2219
end
2220
2221
def cmd_db_remove_help
2222
print_line "Usage: db_remove <name>"
2223
print_line
2224
print_line "Delete the specified saved data service."
2225
print_line
2226
end
2227
2228
def cmd_db_remove(*args)
2229
if args[0] == '-h' || args[0] == '--help' || args[0].nil? || args[0].empty?
2230
cmd_db_remove_help
2231
return
2232
end
2233
delete_db_from_config(args[0])
2234
end
2235
2236
def delete_db_from_config(db_name)
2237
conf = Msf::Config.load
2238
db_path = "#{DB_CONFIG_PATH}/#{db_name}"
2239
if conf[db_path]
2240
clear_default_db if conf[DB_CONFIG_PATH]['default_db'] && conf[DB_CONFIG_PATH]['default_db'] == db_name
2241
Msf::Config.delete_group(db_path)
2242
print_line "Successfully deleted data service: #{db_name}"
2243
else
2244
print_line "Unable to locate saved data service with name #{db_name}."
2245
end
2246
end
2247
2248
def clear_default_db
2249
conf = Msf::Config.load
2250
if conf[DB_CONFIG_PATH] && conf[DB_CONFIG_PATH]['default_db']
2251
updated_opts = conf[DB_CONFIG_PATH]
2252
updated_opts.delete('default_db')
2253
Msf::Config.save(DB_CONFIG_PATH => updated_opts)
2254
print_line "Cleared the default data service."
2255
else
2256
print_line "No default data service was configured."
2257
end
2258
end
2259
2260
def db_find_tools(tools)
2261
missed = []
2262
tools.each do |name|
2263
if(! Rex::FileUtils.find_full_path(name))
2264
missed << name
2265
end
2266
end
2267
if(not missed.empty?)
2268
print_error("This database command requires the following tools to be installed: #{missed.join(", ")}")
2269
return
2270
end
2271
true
2272
end
2273
2274
#######
2275
private
2276
2277
def run_nmap(nmap, arguments, use_sudo: false)
2278
print_warning('Running Nmap with sudo') if use_sudo
2279
begin
2280
nmap_pipe = use_sudo ? ::Open3::popen3('sudo', nmap, *arguments) : ::Open3::popen3(nmap, *arguments)
2281
temp_nmap_threads = []
2282
temp_nmap_threads << framework.threads.spawn("db_nmap-Stdout", false, nmap_pipe[1]) do |np_1|
2283
np_1.each_line do |nmap_out|
2284
next if nmap_out.strip.empty?
2285
print_status("Nmap: #{nmap_out.strip}")
2286
end
2287
end
2288
2289
temp_nmap_threads << framework.threads.spawn("db_nmap-Stderr", false, nmap_pipe[2]) do |np_2|
2290
2291
np_2.each_line do |nmap_err|
2292
next if nmap_err.strip.empty?
2293
print_status("Nmap: '#{nmap_err.strip}'")
2294
# Check if the stderr text includes 'root', this only happens if the scan requires root privileges
2295
if nmap_err =~ /requires? root privileges/ or
2296
nmap_err.include? 'only works if you are root' or nmap_err =~ /requires? raw socket access/
2297
return run_nmap(nmap, arguments, use_sudo: true) unless use_sudo
2298
end
2299
end
2300
end
2301
2302
temp_nmap_threads.map { |t| t.join rescue nil }
2303
nmap_pipe.each { |p| p.close rescue nil }
2304
rescue ::IOError
2305
end
2306
end
2307
2308
#######
2309
2310
def print_connection_info
2311
cdb = ''
2312
if framework.db.driver == 'http'
2313
cdb = framework.db.name
2314
else
2315
::ApplicationRecord.connection_pool.with_connection do |conn|
2316
if conn.respond_to?(:current_database)
2317
cdb = conn.current_database
2318
end
2319
end
2320
end
2321
output = "Connected to #{cdb}. Connection type: #{framework.db.driver}."
2322
output += " Connection name: #{@current_data_service}." if @current_data_service
2323
print_status(output)
2324
end
2325
2326
def list_saved_data_services
2327
conf = Msf::Config.load
2328
default = nil
2329
tbl = Rex::Text::Table.new({
2330
'Header' => 'Data Services',
2331
'Columns' => ['current', 'name', 'url', 'default?'],
2332
'SortIndex' => 1
2333
})
2334
2335
conf.each_pair do |k,v|
2336
if k =~ /#{DB_CONFIG_PATH}/
2337
default = v['default_db'] if v['default_db']
2338
name = k.split('/').last
2339
next if name == 'database' # Data service information is not stored in 'framework/database', just metadata
2340
url = v['url']
2341
current = ''
2342
current = '*' if name == @current_data_service
2343
default_output = ''
2344
default_output = '*' if name == default
2345
line = [current, name, url, default_output]
2346
tbl << line
2347
end
2348
end
2349
print_line
2350
print_line tbl.to_s
2351
end
2352
2353
def print_msgs(status_msg, error_msg)
2354
status_msg.each do |s|
2355
print_status(s)
2356
end
2357
2358
error_msg.each do |e|
2359
print_error(e)
2360
end
2361
end
2362
2363
def _format_vulns_and_vuln_attempts(vulns)
2364
vulns.map.with_index do |vuln, index|
2365
vuln_formatted = <<~EOF.strip.indent(2)
2366
#{index}. Vuln ID: #{vuln.id}
2367
Timestamp: #{vuln.created_at}
2368
Host: #{vuln.host.address}
2369
Name: #{vuln.name}
2370
References: #{vuln.refs.map {|r| r.name}.join(',')}
2371
Information: #{_format_vuln_value(vuln.info)}
2372
EOF
2373
2374
vuln_attempts_formatted = vuln.vuln_attempts.map.with_index do |vuln_attempt, i|
2375
<<~EOF.strip.indent(5)
2376
#{i}. ID: #{vuln_attempt.id}
2377
Vuln ID: #{vuln_attempt.vuln_id}
2378
Timestamp: #{vuln_attempt.attempted_at}
2379
Exploit: #{vuln_attempt.exploited}
2380
Fail reason: #{_format_vuln_value(vuln_attempt.fail_reason)}
2381
Username: #{vuln_attempt.username}
2382
Module: #{vuln_attempt.module}
2383
Session ID: #{_format_vuln_value(vuln_attempt.session_id)}
2384
Loot ID: #{_format_vuln_value(vuln_attempt.loot_id)}
2385
Fail Detail: #{_format_vuln_value(vuln_attempt.fail_detail)}
2386
EOF
2387
end
2388
2389
{ :vuln => vuln_formatted, :vuln_attempts => vuln_attempts_formatted }
2390
end
2391
end
2392
2393
def _print_vulns_and_attempts(vulns_and_attempts)
2394
print_line("Vulnerabilities\n===============")
2395
vulns_and_attempts.each do |vuln_and_attempt|
2396
print_line(vuln_and_attempt[:vuln])
2397
print_line("Vuln attempts:".indent(5))
2398
vuln_and_attempt[:vuln_attempts].each do |attempt|
2399
print_line(attempt)
2400
end
2401
end
2402
end
2403
2404
def _format_vuln_value(s)
2405
s.blank? ? s.inspect : s.to_s
2406
end
2407
end
2408
2409
end end end end
2410
2411