# -*- coding: binary -*- require 'json' require 'rexml/document' require 'metasploit/framework/data_service' require 'metasploit/framework/data_service/remote/http/core' module Msf module Ui module Console module CommandDispatcher class Db require 'tempfile' include Msf::Ui::Console::CommandDispatcher include Msf::Ui::Console::CommandDispatcher::Common include Msf::Ui::Console::CommandDispatcher::Db::Common include Msf::Ui::Console::CommandDispatcher::Db::Analyze include Msf::Ui::Console::CommandDispatcher::Db::Klist DB_CONFIG_PATH = 'framework/database' # # The dispatcher's name. # def name "Database Backend" end # # Returns the hash of commands supported by this dispatcher. # def commands base = { "db_connect" => "Connect to an existing data service", "db_disconnect" => "Disconnect from the current data service", "db_status" => "Show the current data service status", "db_save" => "Save the current data service connection as the default to reconnect on startup", "db_remove" => "Remove the saved data service entry" } more = { "workspace" => "Switch between database workspaces", "hosts" => "List all hosts in the database", "services" => "List all services in the database", "vulns" => "List all vulnerabilities in the database", "notes" => "List all notes in the database", "loot" => "List all loot in the database", "klist" => "List Kerberos tickets in the database", "db_import" => "Import a scan result file (filetype will be auto-detected)", "db_export" => "Export a file containing the contents of the database", "db_nmap" => "Executes nmap and records the output automatically", "db_rebuild_cache" => "Rebuilds the database-stored module cache (deprecated)", "analyze" => "Analyze database information about a specific address or address range", "db_stats" => "Show statistics for the database" } # Always include commands that only make sense when connected. # This avoids the problem of them disappearing unexpectedly if the # database dies or times out. See #1923 base.merge(more) end def deprecated_commands [ "db_autopwn", "db_driver", "db_hosts", "db_notes", "db_services", "db_vulns", ] end # # Attempts to connect to the previously configured database, and additionally keeps track of # the currently loaded data service. # def load_config(path = nil) result = Msf::DbConnector.db_connect_from_config(framework, path) if result[:error] print_error(result[:error]) end if result[:data_service_name] @current_data_service = result[:data_service_name] end end @@workspace_opts = Rex::Parser::Arguments.new( [ '-h', '--help' ] => [ false, 'Help banner.'], [ '-a', '--add' ] => [ true, 'Add a workspace.', ''], [ '-d', '--delete' ] => [ true, 'Delete a workspace.', ''], [ '-D', '--delete-all' ] => [ false, 'Delete all workspaces.'], [ '-r', '--rename' ] => [ true, 'Rename a workspace.', ' '], [ '-l', '--list' ] => [ false, 'List workspaces.'], [ '-v', '--list-verbose' ] => [ false, 'List workspaces verbosely.'], [ '-S', '--search' ] => [ true, 'Search for a workspace.', ''] ) def cmd_workspace_help print_line "Usage:" print_line " workspace List workspaces" print_line " workspace [name] Switch workspace" print_line @@workspace_opts.usage end def cmd_workspace(*args) return unless active? state = :nil list = false verbose = false names = [] search_term = nil @@workspace_opts.parse(args) do |opt, idx, val| case opt when '-h', '--help' cmd_workspace_help return when '-a', '--add' return cmd_workspace_help unless state == :nil state = :adding names << val if !val.nil? when '-d', '--del' return cmd_workspace_help unless state == :nil state = :deleting names << val if !val.nil? when '-D', '--delete-all' return cmd_workspace_help unless state == :nil state = :delete_all when '-r', '--rename' return cmd_workspace_help unless state == :nil state = :renaming names << val if !val.nil? when '-v', '--verbose' verbose = true when '-l', '--list' list = true when '-S', '--search' search_term = val else names << val if !val.nil? end end if state == :adding and names # Add workspaces wspace = nil names.each do |name| wspace = framework.db.workspaces(name: name).first if wspace print_status("Workspace '#{wspace.name}' already existed, switching to it.") else wspace = framework.db.add_workspace(name) print_status("Added workspace: #{wspace.name}") end end framework.db.workspace = wspace print_status("Workspace: #{framework.db.workspace.name}") elsif state == :deleting and names ws_ids_to_delete = [] starting_ws = framework.db.workspace names.uniq.each do |n| ws = framework.db.workspaces(name: n).first ws_ids_to_delete << ws.id if ws end if ws_ids_to_delete.count > 0 deleted = framework.db.delete_workspaces(ids: ws_ids_to_delete) process_deleted_workspaces(deleted, starting_ws) else print_status("No workspaces matching the given name(s) were found.") end elsif state == :delete_all ws_ids_to_delete = [] starting_ws = framework.db.workspace framework.db.workspaces.each do |ws| ws_ids_to_delete << ws.id end deleted = framework.db.delete_workspaces(ids: ws_ids_to_delete) process_deleted_workspaces(deleted, starting_ws) elsif state == :renaming if names.length != 2 print_error("Wrong number of arguments to rename") return end ws_to_update = framework.db.find_workspace(names.first) unless ws_to_update print_error("Workspace '#{names.first}' does not exist") return end opts = { id: ws_to_update.id, name: names.last } begin updated_ws = framework.db.update_workspace(opts) if updated_ws framework.db.workspace = updated_ws if names.first == framework.db.workspace.name print_status("Renamed workspace '#{names.first}' to '#{updated_ws.name}'") else print_error "There was a problem updating the workspace. Setting to the default workspace." framework.db.workspace = framework.db.default_workspace return end if names.first == Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME print_status("Recreated default workspace") end rescue => e print_error "Failed to rename workspace: #{e.message}" end elsif !names.empty? name = names.last # Switch workspace workspace = framework.db.find_workspace(name) if workspace framework.db.workspace = workspace print_status("Workspace: #{workspace.name}") else print_error("Workspace not found: #{name}") return end else current_workspace = framework.db.workspace unless verbose current = nil framework.db.workspaces.sort_by {|s| s.name}.each do |s| if s.name == current_workspace.name current = s.name else print_line(" #{s.name}") end end print_line("%red* #{current}%clr") unless current.nil? return end col_names = %w{current name hosts services vulns creds loots notes} tbl = Rex::Text::Table.new( 'Header' => 'Workspaces', 'Columns' => col_names, 'SortIndex' => -1, 'SearchTerm' => search_term ) framework.db.workspaces.each do |ws| tbl << [ current_workspace.name == ws.name ? '*' : '', ws.name, framework.db.hosts(workspace: ws.name).count, framework.db.services(workspace: ws.name).count, framework.db.vulns(workspace: ws.name).count, framework.db.creds(workspace: ws.name).count, framework.db.loots(workspace: ws.name).count, framework.db.notes(workspace: ws.name).count ] end print_line print_line(tbl.to_s) end end def process_deleted_workspaces(deleted_workspaces, starting_ws) deleted_workspaces.each do |ws| print_status "Deleted workspace: #{ws.name}" if ws.name == Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME framework.db.workspace = framework.db.default_workspace print_status 'Recreated the default workspace' elsif ws == starting_ws framework.db.workspace = framework.db.default_workspace print_status "Switched to workspace: #{framework.db.workspace.name}" end end end def cmd_workspace_tabs(str, words) return [] unless active? framework.db.workspaces.map(&:name) if (words & ['-a','--add']).empty? end # # Tab completion for the hosts command # # @param str [String] the string currently being typed before tab was hit # @param words [Array] the previously completed words on the command line. words is always # at least 1 when tab completion has reached this stage since the command itself has been completed def cmd_hosts_tabs(str, words) if words.length == 1 return @@hosts_opts.option_keys.select { |opt| opt.start_with?(str) } end case words[-1] when '-c', '--columns', '-C', '--columns-until-restart' return @@hosts_columns when '-o', '--output' return tab_complete_filenames(str, words) end if @@hosts_opts.arg_required?(words[-1]) return [] end return @@hosts_opts.option_keys.select { |opt| opt.start_with?(str) } end def cmd_hosts_help # This command does some lookups for the list of appropriate column # names, so instead of putting all the usage stuff here like other # help methods, just use it's "-h" so we don't have to recreating # that list cmd_hosts("-h") end # Changes the specified host data # # @param host_ranges - range of hosts to process # @param host_data - hash of host data to be updated def change_host_data(host_ranges, host_data) if !host_data || host_data.length != 1 print_error("A single key-value data hash is required to change the host data") return end attribute = host_data.keys[0] if host_ranges == [nil] print_error("In order to change the host #{attribute}, you must provide a range of hosts") return end each_host_range_chunk(host_ranges) do |host_search| next if host_search && host_search.empty? framework.db.hosts(address: host_search).each do |host| framework.db.update_host(host_data.merge(id: host.id)) framework.db.report_note(host: host.address, type: "host.#{attribute}", data: host_data[attribute]) end end end def add_host_tag(rws, tag_name) if rws == [nil] print_error("In order to add a tag, you must provide a range of hosts") return end opts = Hash.new() opts[:workspace] = framework.db.workspace opts[:tag_name] = tag_name rws.each do |rw| rw.each do |ip| opts[:address] = ip unless framework.db.add_host_tag(opts) print_error("Host #{ip} could not be found.") end end end end def find_host_tags(workspace, host_id) opts = Hash.new() opts[:workspace] = workspace opts[:id] = host_id framework.db.get_host_tags(opts) end def delete_host_tag(rws, tag_name) opts = Hash.new() opts[:workspace] = framework.db.workspace opts[:tag_name] = tag_name # This will be the case if no IP was passed in, and we are just trying to delete all # instances of a given tag within the database. if rws == [nil] wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework) wspace.hosts.each do |host| opts[:address] = host.address framework.db.delete_host_tag(opts) end else rws.each do |rw| rw.each do |ip| opts[:address] = ip unless framework.db.delete_host_tag(opts) print_error("Host #{ip} could not be found.") end end end end end @@hosts_columns = [ 'address', 'mac', 'name', 'os_name', 'os_flavor', 'os_sp', 'purpose', 'info', 'comments'] @@hosts_opts = Rex::Parser::Arguments.new( [ '-h', '--help' ] => [ false, 'Show this help information' ], [ '-a', '--add' ] => [ true, 'Add the hosts instead of searching', '' ], [ '-u', '--up' ] => [ false, 'Only show hosts which are up' ], [ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search' ], [ '-S', '--search' ] => [ true, 'Search string to filter by', '' ], [ '-i', '--info' ] => [ true, 'Change the info of a host', '' ], [ '-n', '--name' ] => [ true, 'Change the name of a host', '' ], [ '-m', '--comment' ] => [ true, 'Change the comment of a host', '' ], [ '-t', '--tag' ] => [ true, 'Add or specify a tag to a range of hosts', '' ], [ '-T', '--delete-tag' ] => [ true, 'Remove a tag from a range of hosts', '' ], [ '-d', '--delete' ] => [ true, 'Delete the hosts instead of searching', '' ], [ '-o', '--output' ] => [ true, 'Send output to a file in csv format', '' ], [ '-O', '--order' ] => [ true, 'Order rows by specified column number', '' ], [ '-c', '--columns' ] => [ true, 'Only show the given columns (see list below)', '' ], [ '-C', '--columns-until-restart' ] => [ true, 'Only show the given columns until the next restart (see list below)', '' ], ) def cmd_hosts(*args) return unless active? onlyup = false set_rhosts = false mode = [] delete_count = 0 rhosts = [] host_ranges = [] search_term = nil order_by = nil info_data = nil name_data = nil comment_data = nil tag_name = nil output = nil default_columns = [ 'address', 'arch', 'comm', 'comments', 'created_at', 'cred_count', 'detected_arch', 'exploit_attempt_count', 'host_detail_count', 'info', 'mac', 'name', 'note_count', 'os_family', 'os_flavor', 'os_lang', 'os_name', 'os_sp', 'purpose', 'scope', 'service_count', 'state', 'updated_at', 'virtual_host', 'vuln_count', 'workspace_id'] default_columns << 'tags' # Special case virtual_columns = [ 'svcs', 'vulns', 'workspace', 'tags' ] col_search = @@hosts_columns default_columns.delete_if {|v| (v[-2,2] == "id")} @@hosts_opts.parse(args) do |opt, idx, val| case opt when '-h', '--help' print_line "Usage: hosts [ options ] [addr1 addr2 ...]" print_line print @@hosts_opts.usage print_line print_line "Available columns: #{default_columns.join(", ")}" print_line return when '-a', '--add' mode << :add arg_host_range(val, host_ranges) when '-d', '--delete' mode << :delete arg_host_range(val, host_ranges) when '-u', '--up' onlyup = true when '-o' output = val output = ::File.expand_path(output) when '-R', '--rhosts' set_rhosts = true when '-S', '--search' search_term = val when '-i', '--info' mode << :new_info info_data = val when '-n', '--name' mode << :new_name name_data = val when '-m', '--comment' mode << :new_comment comment_data = val when '-t', '--tag' mode << :tag tag_name = val when '-T', '--delete-tag' mode << :delete_tag tag_name = val when '-c', '-C' list = val if(!list) print_error("Invalid column list") return end col_search = list.strip().split(",") col_search.each { |c| if not default_columns.include?(c) and not virtual_columns.include?(c) all_columns = default_columns + virtual_columns print_error("Invalid column list. Possible values are (#{all_columns.join("|")})") return end } if opt == '-C' @@hosts_columns = col_search end when '-O' if (order_by = val.to_i - 1) < 0 print_error('Please specify a column number starting from 1') return end else # Anything that wasn't an option is a host to search for unless (arg_host_range(val, host_ranges)) return end end end if col_search col_names = col_search else col_names = default_columns + virtual_columns end mode << :search if mode.empty? if mode == [:add] host_ranges.each do |range| range.each do |address| host = framework.db.find_or_create_host(:host => address) print_status("Time: #{host.created_at} Host: host=#{host.address}") end end return end cp_hsh = {} col_names.map do |col| cp_hsh[col] = { 'MaxChar' => 52 } end # If we got here, we're searching. Delete implies search tbl = Rex::Text::Table.new( { 'Header' => "Hosts", 'Columns' => col_names, 'ColProps' => cp_hsh, 'SortIndex' => order_by }) # Sentinel value meaning all host_ranges.push(nil) if host_ranges.empty? case when mode == [:new_info] change_host_data(host_ranges, info: info_data) return when mode == [:new_name] change_host_data(host_ranges, name: name_data) return when mode == [:new_comment] change_host_data(host_ranges, comments: comment_data) return when mode == [:tag] begin add_host_tag(host_ranges, tag_name) rescue => e if e.message.include?('Validation failed') print_error(e.message) else raise e end end return when mode == [:delete_tag] begin delete_host_tag(host_ranges, tag_name) rescue => e if e.message.include?('Validation failed') print_error(e.message) else raise e end end return end matched_host_ids = [] each_host_range_chunk(host_ranges) do |host_search| next if host_search && host_search.empty? framework.db.hosts(address: host_search, non_dead: onlyup, search_term: search_term).each do |host| matched_host_ids << host.id columns = col_names.map do |n| # Deal with the special cases if virtual_columns.include?(n) case n when "svcs"; host.service_count when "vulns"; host.vuln_count when "workspace"; host.workspace.name when "tags" found_tags = find_host_tags(framework.db.workspace, host.id) tag_names = found_tags.map(&:name).join(', ') tag_names end # Otherwise, it's just an attribute else host[n] || "" end end tbl << columns if set_rhosts addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address) rhosts << addr end end if mode == [:delete] result = framework.db.delete_host(ids: matched_host_ids) delete_count += result.size end end if output print_status("Wrote hosts to #{output}") ::File.open(output, "wb") { |ofd| ofd.write(tbl.to_csv) } else print_line print_line(tbl.to_s) end # Finally, handle the case where the user wants the resulting list # of hosts to go into RHOSTS. set_rhosts_from_addrs(rhosts.uniq) if set_rhosts print_status("Deleted #{delete_count} hosts") if delete_count > 0 end # # Tab completion for the services command # # @param str [String] the string currently being typed before tab was hit # @param words [Array] the previously completed words on the command line. words is always # at least 1 when tab completion has reached this stage since the command itself has been completed def cmd_services_tabs(str, words) if words.length == 1 return @@services_opts.option_keys.select { |opt| opt.start_with?(str) } end case words[-1] when '-c', '--column' return @@services_columns when '-O', '--order' return [] when '-o', '--output' return tab_complete_filenames(str, words) when '-p', '--port' return [] when '-r', '--protocol' return [] end [] end def cmd_services_help print_line "Usage: services [-h] [-u] [-a] [-r ] [-p ] [-s ] [-o ] [addr1 addr2 ...]" print_line print @@services_opts.usage print_line print_line "Available columns: #{@@services_columns.join(", ")}" print_line end @@services_columns = [ 'created_at', 'info', 'name', 'port', 'proto', 'state', 'updated_at' ] @@services_opts = Rex::Parser::Arguments.new( [ '-a', '--add' ] => [ false, 'Add the services instead of searching.' ], [ '-d', '--delete' ] => [ false, 'Delete the services instead of searching.' ], [ '-U', '--update' ] => [ false, 'Update data for existing service.' ], [ '-u', '--up' ] => [ false, 'Only show services which are up.' ], [ '-c', '--column' ] => [ true, 'Only show the given columns.', '' ], [ '-p', '--port' ] => [ true, 'Search for a list of ports.', '' ], [ '-r', '--protocol' ] => [ true, 'Protocol type of the service being added [tcp|udp].', '' ], [ '-s', '--name' ] => [ true, 'Name of the service to add.', '' ], [ '-o', '--output' ] => [ true, 'Send output to a file in csv format.', '' ], [ '-O', '--order' ] => [ true, 'Order rows by specified column number.', '' ], [ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ], [ '-S', '--search' ] => [ true, 'Search string to filter by.', '' ], [ '-h', '--help' ] => [ false, 'Show this help information.' ] ) def db_connection_info(framework) unless framework.db.connection_established? return "#{framework.db.driver} selected, no connection" end cdb = '' if framework.db.driver == 'http' cdb = framework.db.name else ::ApplicationRecord.connection_pool.with_connection do |conn| if conn.respond_to?(:current_database) cdb = conn.current_database end end end if cdb.empty? output = "Connected Database Name could not be extracted. DB Connection type: #{framework.db.driver}." else output = "Connected to #{cdb}. Connection type: #{framework.db.driver}." end output end def cmd_db_stats(*args) return unless active? print_line "Session Type: #{db_connection_info(framework)}" current_workspace = framework.db.workspace example_workspaces = ::Mdm::Workspace.order(id: :desc) ordered_workspaces = ([current_workspace] + example_workspaces).uniq.sort_by(&:id) tbl = Rex::Text::Table.new( 'Indent' => 2, 'Header' => "Database Stats", 'Columns' => [ "IsTarget", "ID", "Name", "Hosts", "Services", "Services per Host", "Vulnerabilities", "Vulns per Host", "Notes", "Creds", "Kerberos Cache" ], 'SortIndex' => 1, 'ColProps' => { 'IsTarget' => { 'Stylers' => [Msf::Ui::Console::TablePrint::RowIndicatorStyler.new], 'ColumnStylers' => [Msf::Ui::Console::TablePrint::OmitColumnHeader.new], 'Width' => 2 } } ) total_hosts = 0 total_services = 0 total_vulns = 0 total_notes = 0 total_creds = 0 total_tickets = 0 ordered_workspaces.map do |workspace| hosts = workspace.hosts.count services = workspace.services.count vulns = workspace.vulns.count notes = workspace.notes.count creds = framework.db.creds(workspace: workspace.name).count # workspace.creds.count.to_fs(:delimited) is always 0 for whatever reason kerbs = ticket_search([nil], nil, :workspace => workspace).count total_hosts += hosts total_services += services total_vulns += vulns total_notes += notes total_creds += creds total_tickets += kerbs tbl << [ current_workspace.id == workspace.id, workspace.id, workspace.name, hosts.to_fs(:delimited), services.to_fs(:delimited), hosts > 0 ? (services.to_f / hosts).truncate(2) : 0, vulns.to_fs(:delimited), hosts > 0 ? (vulns.to_f / hosts).truncate(2) : 0, notes.to_fs(:delimited), creds.to_fs(:delimited), kerbs.to_fs(:delimited) ] end # total row tbl << [ "", "Total", ordered_workspaces.length.to_fs(:delimited), total_hosts.to_fs(:delimited), total_services.to_fs(:delimited), total_hosts > 0 ? (total_services.to_f / total_hosts).truncate(2) : 0, total_vulns, total_hosts > 0 ? (total_vulns.to_f / total_hosts).truncate(2) : 0, total_notes, total_creds.to_fs(:delimited), total_tickets.to_fs(:delimited) ] print_line tbl.to_s end def cmd_services(*args) return unless active? mode = :search onlyup = false output_file = nil set_rhosts = false col_search = ['port', 'proto', 'name', 'state', 'info'] names = nil order_by = nil proto = nil host_ranges = [] port_ranges = [] rhosts = [] delete_count = 0 search_term = nil opts = {} @@services_opts.parse(args) do |opt, idx, val| case opt when '-a', '--add' mode = :add when '-d', '--delete' mode = :delete when '-U', '--update' mode = :update when '-u', '--up' onlyup = true when '-c' list = val if(!list) print_error("Invalid column list") return end col_search = list.strip().split(",") col_search.each { |c| if not @@services_columns.include? c print_error("Invalid column list. Possible values are (#{@@services_columns.join("|")})") return end } when '-p' unless (arg_port_range(val, port_ranges, true)) return end when '-r' proto = val if (!proto) print_status("Invalid protocol") return end proto = proto.strip when '-s' namelist = val if (!namelist) print_error("Invalid name list") return end names = namelist.strip().split(",") when '-o' output_file = val if (!output_file) print_error("Invalid output filename") return end output_file = ::File.expand_path(output_file) when '-O' if (order_by = val.to_i - 1) < 0 print_error('Please specify a column number starting from 1') return end when '-R', '--rhosts' set_rhosts = true when '-S', '--search' search_term = val opts[:search_term] = search_term when '-h', '--help' cmd_services_help return else # Anything that wasn't an option is a host to search for unless (arg_host_range(val, host_ranges)) return end end end ports = port_ranges.flatten.uniq if mode == :add # Can only deal with one port and one service name at a time # right now. Them's the breaks. if ports.length != 1 print_error("Exactly one port required") return end if host_ranges.empty? print_error("Host address or range required") return end host_ranges.each do |range| range.each do |addr| info = { :host => addr, :port => ports.first.to_i } info[:proto] = proto.downcase if proto info[:name] = names.first.downcase if names and names.first svc = framework.db.find_or_create_service(info) print_status("Time: #{svc.created_at} Service: host=#{svc.host.address} port=#{svc.port} proto=#{svc.proto} name=#{svc.name}") end end return end # If we got here, we're searching. Delete implies search col_names = @@services_columns if col_search col_names = col_search end tbl = Rex::Text::Table.new({ 'Header' => "Services", 'Columns' => ['host'] + col_names, 'SortIndex' => order_by }) # Sentinel value meaning all host_ranges.push(nil) if host_ranges.empty? ports = nil if ports.empty? matched_service_ids = [] each_host_range_chunk(host_ranges) do |host_search| next if host_search && host_search.empty? opts[:workspace] = framework.db.workspace opts[:hosts] = {address: host_search} if !host_search.nil? opts[:port] = ports if ports framework.db.services(opts).each do |service| unless service.state == 'open' next if onlyup end host = service.host matched_service_ids << service.id if mode == :update service.name = names.first if names service.proto = proto if proto service.port = ports.first if ports framework.db.update_service(service.as_json.symbolize_keys) end columns = [host.address] + col_names.map { |n| service[n].to_s || "" } tbl << columns if set_rhosts addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address ) rhosts << addr end end end if (mode == :delete) result = framework.db.delete_service(ids: matched_service_ids) delete_count += result.size end if (output_file == nil) print_line(tbl.to_s) else # create the output file ::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) } print_status("Wrote services to #{output_file}") end # Finally, handle the case where the user wants the resulting list # of hosts to go into RHOSTS. set_rhosts_from_addrs(rhosts.uniq) if set_rhosts print_status("Deleted #{delete_count} services") if delete_count > 0 end # # Tab completion for the vulns command # # @param str [String] the string currently being typed before tab was hit # @param words [Array] the previously completed words on the command line. words is always # at least 1 when tab completion has reached this stage since the command itself has been completed def cmd_vulns_tabs(str, words) if words.length == 1 return @@vulns_opts.option_keys.select { |opt| opt.start_with?(str) } end case words[-1] when '-o', '--output' return tab_complete_filenames(str, words) end end def cmd_vulns_help print_line "Print all vulnerabilities in the database" print_line print_line "Usage: vulns [addr range]" print_line print @@vulns_opts.usage print_line print_line "Examples:" print_line " vulns -p 1-65536 # only vulns with associated services" print_line " vulns -p 1-65536 -s http # identified as http on any port" print_line end @@vulns_opts = Rex::Parser::Arguments.new( [ '-h', '--help' ] => [ false, 'Show this help information.' ], [ '-o', '--output' ] => [ true, 'Send output to a file in csv format.', '' ], [ '-p', '--port' ] => [ true, 'List vulns matching this port spec.', '' ], [ '-s', '--service' ] => [ true, 'List vulns matching these service names.', '' ], [ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ], [ '-S', '--search' ] => [ true, 'Search string to filter by.', '' ], [ '-i', '--info' ] => [ false, 'Display vuln information.' ], [ '-d', '--delete' ] => [ false, 'Delete vulnerabilities. Not officially supported.' ] ) def cmd_vulns(*args) return unless active? default_columns = ['Timestamp', 'Host', 'Name', 'References'] host_ranges = [] port_ranges = [] svcs = [] rhosts = [] search_term = nil show_info = false set_rhosts = false output_file = nil delete_count = 0 mode = nil @@vulns_opts.parse(args) do |opt, idx, val| case opt when '-d', '--delete' # TODO: This is currently undocumented because it's not officially supported. mode = :delete when '-h', '--help' cmd_vulns_help return when '-o', '--output' output_file = val if output_file output_file = File.expand_path(output_file) else print_error("Invalid output filename") return end when '-p', '--port' unless (arg_port_range(val, port_ranges, true)) return end when '-s', '--service' service = val if (!service) print_error("Argument required for -s") return end svcs = service.split(/[\s]*,[\s]*/) when '-R', '--rhosts' set_rhosts = true when '-S', '--search' search_term = val when '-i', '--info' show_info = true else # Anything that wasn't an option is a host to search for unless (arg_host_range(val, host_ranges)) return end end end if show_info default_columns << 'Information' end # add sentinel value meaning all if empty host_ranges.push(nil) if host_ranges.empty? # normalize ports = port_ranges.flatten.uniq svcs.flatten! tbl = Rex::Text::Table.new( 'Header' => 'Vulnerabilities', 'Columns' => default_columns ) matched_vuln_ids = [] vulns = [] if host_ranges.compact.empty? vulns = framework.db.vulns({:search_term => search_term}) else each_host_range_chunk(host_ranges) do |host_search| next if host_search && host_search.empty? vulns.concat(framework.db.vulns({:hosts => { :address => host_search }, :search_term => search_term })) end end vulns.each do |vuln| reflist = vuln.refs.map {|r| r.name} if (vuln.service) # Skip this one if the user specified a port and it # doesn't match. next unless ports.empty? or ports.include? vuln.service.port # Same for service names next unless svcs.empty? or svcs.include?(vuln.service.name) else # This vuln has no service, so it can't match next unless ports.empty? and svcs.empty? end matched_vuln_ids << vuln.id row = [] row << vuln.created_at row << vuln.host.address row << vuln.name row << reflist.join(',') if show_info row << vuln.info end tbl << row if set_rhosts addr = (vuln.host.scope.to_s != "" ? vuln.host.address + '%' + vuln.host.scope : vuln.host.address) rhosts << addr end end if mode == :delete result = framework.db.delete_vuln(ids: matched_vuln_ids) delete_count = result.size end if output_file File.write(output_file, tbl.to_csv) print_status("Wrote vulnerability information to #{output_file}") else print_line print_line(tbl.to_s) end # Finally, handle the case where the user wants the resulting list # of hosts to go into RHOSTS. set_rhosts_from_addrs(rhosts.uniq) if set_rhosts print_status("Deleted #{delete_count} vulnerabilities") if delete_count > 0 end # # Tab completion for the notes command # # @param str [String] the string currently being typed before tab was hit # @param words [Array] the previously completed words on the command line. words is always # at least 1 when tab completion has reached this stage since the command itself has been completed def cmd_notes_tabs(str, words) if words.length == 1 return @@notes_opts.option_keys.select { |opt| opt.start_with?(str) } end case words[-1] when '-O', '--order' return [] when '-o', '--output' return tab_complete_filenames(str, words) end [] end def cmd_notes_help print_line "Usage: notes [-h] [-t ] [-n ] [-a] [addr range]" print_line print @@notes_opts.usage print_line print_line "Examples:" print_line " notes --add -t apps -n 'winzip' 10.1.1.34 10.1.20.41" print_line " notes -t smb.fingerprint 10.1.1.34 10.1.20.41" print_line " notes -S 'nmap.nse.(http|rtsp)'" print_line end @@notes_opts = Rex::Parser::Arguments.new( [ '-a', '--add' ] => [ false, 'Add a note to the list of addresses, instead of listing.' ], [ '-d', '--delete' ] => [ false, 'Delete the notes instead of searching.' ], [ '-n', '--note' ] => [ true, 'Set the data for a new note (only with -a).', '' ], [ '-t', '--type' ] => [ true, 'Search for a list of types, or set single type for add.', '' ], [ '-h', '--help' ] => [ false, 'Show this help information.' ], [ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ], [ '-o', '--output' ] => [ true, 'Save the notes to a csv file.', '' ], [ '-O', '--order' ] => [ true, 'Order rows by specified column number.', '' ], [ '-u', '--update' ] => [ false, 'Update a note. Not officially supported.' ] ) def cmd_notes(*args) return unless active? ::ApplicationRecord.connection_pool.with_connection { mode = :search data = nil types = nil set_rhosts = false host_ranges = [] rhosts = [] search_term = nil output_file = nil delete_count = 0 order_by = nil @@notes_opts.parse(args) do |opt, idx, val| case opt when '-a', '--add' mode = :add when '-d', '--delete' mode = :delete when '-n', '--note' data = val if(!data) print_error("Can't make a note with no data") return end when '-t', '--type' typelist = val if(!typelist) print_error("Invalid type list") return end types = typelist.strip().split(",") when '-R', '--rhosts' set_rhosts = true when '-S', '--search' search_term = val when '-o', '--output' output_file = val output_file = ::File.expand_path(output_file) when '-O' if (order_by = val.to_i - 1) < 0 print_error('Please specify a column number starting from 1') return end when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported. mode = :update when '-h', '--help' cmd_notes_help return else # Anything that wasn't an option is a host to search for unless (arg_host_range(val, host_ranges)) return end end end if mode == :add if host_ranges.compact.empty? print_error("Host address or range required") return end if types.nil? || types.size != 1 print_error("Exactly one type is required") return end if data.nil? print_error("Data required") return end type = types.first host_ranges.each { |range| range.each { |addr| note = framework.db.find_or_create_note(host: addr, type: type, data: data) break if not note print_status("Time: #{note.created_at} Note: host=#{addr} type=#{note.ntype} data=#{note.data}") } } return end if mode == :update if !types.nil? && types.size != 1 print_error("Exactly one type is required") return end if types.nil? && data.nil? print_error("Update requires data or type") return end end note_list = [] if host_ranges.compact.empty? # No host specified - collect all notes opts = {search_term: search_term} opts[:ntype] = types if mode != :update && types && !types.empty? note_list = framework.db.notes(opts) else # Collect notes of specified hosts each_host_range_chunk(host_ranges) do |host_search| next if host_search && host_search.empty? opts = {hosts: {address: host_search}, workspace: framework.db.workspace, search_term: search_term} opts[:ntype] = types if mode != :update && types && !types.empty? note_list.concat(framework.db.notes(opts)) end end # Now display them table = Rex::Text::Table.new( 'Header' => 'Notes', 'Indent' => 1, 'Columns' => ['Time', 'Host', 'Service', 'Port', 'Protocol', 'Type', 'Data'], 'SortIndex' => order_by ) matched_note_ids = [] note_list.each do |note| if mode == :update begin update_opts = {id: note.id} unless types.nil? note.ntype = types.first update_opts[:ntype] = types.first end unless data.nil? note.data = data update_opts[:data] = data end framework.db.update_note(update_opts) rescue => e elog "There was an error updating note with ID #{note.id}: #{e.message}" next end end matched_note_ids << note.id row = [] row << note.created_at if note.host host = note.host row << host.address if set_rhosts addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address) rhosts << addr end else row << '' end if note.service row << note.service.name || '' row << note.service.port || '' row << note.service.proto || '' else row << '' # For the Service field row << '' # For the Port field row << '' # For the Protocol field end row << note.ntype row << note.data.inspect table << row end if mode == :delete result = framework.db.delete_note(ids: matched_note_ids) delete_count = result.size end if output_file save_csv_notes(output_file, table) else print_line print_line(table.to_s) end # Finally, handle the case where the user wants the resulting list # of hosts to go into RHOSTS. set_rhosts_from_addrs(rhosts.uniq) if set_rhosts print_status("Deleted #{delete_count} notes") if delete_count > 0 } end def save_csv_notes(fpath, table) begin File.open(fpath, 'wb') do |f| f.write(table.to_csv) end print_status("Wrote notes to #{fpath}") rescue Errno::EACCES => e print_error("Unable to save notes. #{e.message}") end end # # Tab completion for the loot command # # @param str [String] the string currently being typed before tab was hit # @param words [Array] the previously completed words on the command line. words is always # at least 1 when tab completion has reached this stage since the command itself has been completed def cmd_loot_tabs(str, words) if words.length == 1 @@loot_opts.option_keys.select { |opt| opt.start_with?(str) } end end def cmd_loot_help print_line "Usage: loot [options]" print_line " Info: loot [-h] [addr1 addr2 ...] [-t ]" print_line " Add: loot -f [fname] -i [info] -a [addr1 addr2 ...] -t [type]" print_line " Del: loot -d [addr1 addr2 ...]" print_line print @@loot_opts.usage print_line end @@loot_opts = Rex::Parser::Arguments.new( [ '-a', '--add' ] => [ false, 'Add loot to the list of addresses, instead of listing.' ], [ '-d', '--delete' ] => [ false, 'Delete *all* loot matching host and type.' ], [ '-f', '--file' ] => [ true, 'File with contents of the loot to add.', '' ], [ '-i', '--info' ] => [ true, 'Info of the loot to add.', '' ], [ '-t', '--type' ] => [ true, 'Search for a list of types.', '' ], [ '-h', '--help' ] => [ false, 'Show this help information.' ], [ '-S', '--search' ] => [ true, 'Search string to filter by.', '' ], [ '-u', '--update' ] => [ false, 'Update loot. Not officially supported.' ] ) def cmd_loot(*args) return unless active? mode = :search host_ranges = [] types = nil delete_count = 0 search_term = nil file = nil name = nil info = nil filename = nil @@loot_opts.parse(args) do |opt, idx, val| case opt when '-a', '--add' mode = :add when '-d', '--delete' mode = :delete when '-f', '--file' filename = val if(!filename) print_error("Can't make loot with no filename") return end if (!File.exist?(filename) or !File.readable?(filename)) print_error("Can't read file") return end when '-i', '--info' info = val if(!info) print_error("Can't make loot with no info") return end when '-t', '--type' typelist = val if(!typelist) print_error("Invalid type list") return end types = typelist.strip().split(",") when '-S', '--search' search_term = val when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported. mode = :update when '-h', '--help' cmd_loot_help return else # Anything that wasn't an option is a host to search for unless (arg_host_range(val, host_ranges)) return end end end tbl = Rex::Text::Table.new({ 'Header' => "Loot", 'Columns' => [ 'host', 'service', 'type', 'name', 'content', 'info', 'path' ], # For now, don't perform any word wrapping on the loot table as it breaks the workflow of # copying paths and pasting them into applications 'WordWrap' => false, }) # Sentinel value meaning all host_ranges.push(nil) if host_ranges.empty? if mode == :add if host_ranges.compact.empty? print_error('Address list required') return end if info.nil? print_error("Info required") return end if filename.nil? print_error("Loot file required") return end if types.nil? or types.size != 1 print_error("Exactly one loot type is required") return end type = types.first name = File.basename(filename) file = File.open(filename, "rb") contents = file.read host_ranges.each do |range| range.each do |host| lootfile = framework.db.find_or_create_loot(:type => type, :host => host, :info => info, :data => contents, :path => filename, :name => name) print_status("Added loot for #{host} (#{lootfile})") end end return end matched_loot_ids = [] loots = [] if host_ranges.compact.empty? loots = loots + framework.db.loots(workspace: framework.db.workspace, search_term: search_term) else each_host_range_chunk(host_ranges) do |host_search| next if host_search && host_search.empty? loots = loots + framework.db.loots(workspace: framework.db.workspace, hosts: { address: host_search }, search_term: search_term) end end loots.each do |loot| row = [] # TODO: This is just a temp implementation of update for the time being since it did not exist before. # It should be updated to not pass all of the attributes attached to the object, only the ones being updated. if mode == :update begin loot.info = info if info if types && types.size > 1 print_error "May only pass 1 type when performing an update." next end loot.ltype = types.first if types framework.db.update_loot(loot.as_json.symbolize_keys) rescue => e elog "There was an error updating loot with ID #{loot.id}: #{e.message}" next end end row.push (loot.host && loot.host.address) ? loot.host.address : "" if (loot.service) svc = (loot.service.name ? loot.service.name : "#{loot.service.port}/#{loot.service.proto}") row.push svc else row.push "" end row.push(loot.ltype) row.push(loot.name || "") row.push(loot.content_type) row.push(loot.info || "") row.push(loot.path) tbl << row matched_loot_ids << loot.id end if (mode == :delete) result = framework.db.delete_loot(ids: matched_loot_ids) delete_count = result.size end print_line print_line(tbl.to_s) print_status("Deleted #{delete_count} loots") if delete_count > 0 end # :category: Deprecated Commands def cmd_db_hosts_help; deprecated_help(:hosts); end # :category: Deprecated Commands def cmd_db_notes_help; deprecated_help(:notes); end # :category: Deprecated Commands def cmd_db_vulns_help; deprecated_help(:vulns); end # :category: Deprecated Commands def cmd_db_services_help; deprecated_help(:services); end # :category: Deprecated Commands def cmd_db_autopwn_help; deprecated_help; end # :category: Deprecated Commands def cmd_db_driver_help; deprecated_help; end # :category: Deprecated Commands def cmd_db_hosts(*args); deprecated_cmd(:hosts, *args); end # :category: Deprecated Commands def cmd_db_notes(*args); deprecated_cmd(:notes, *args); end # :category: Deprecated Commands def cmd_db_vulns(*args); deprecated_cmd(:vulns, *args); end # :category: Deprecated Commands def cmd_db_services(*args); deprecated_cmd(:services, *args); end # :category: Deprecated Commands def cmd_db_autopwn(*args); deprecated_cmd; end # # :category: Deprecated Commands # # This one deserves a little more explanation than standard deprecation # warning, so give the user a better understanding of what's going on. # def cmd_db_driver(*args) deprecated_cmd print_line print_line "Because Metasploit no longer supports databases other than the default" print_line "PostgreSQL, there is no longer a need to set the driver. Thus db_driver" print_line "is not useful and its functionality has been removed. Usually Metasploit" print_line "will already have connected to the database; check db_status to see." print_line cmd_db_status end def cmd_db_import_tabs(str, words) tab_complete_filenames(str, words) end def cmd_db_import_help print_line "Usage: db_import [file2...]" print_line print_line "Filenames can be globs like *.xml, or **/*.xml which will search recursively" print_line "Currently supported file types include:" print_line " Acunetix" print_line " Amap Log" print_line " Amap Log -m" print_line " Appscan" print_line " Burp Session XML" print_line " Burp Issue XML" print_line " CI" print_line " Foundstone" print_line " FusionVM XML" print_line " Group Policy Preferences Credentials" print_line " IP Address List" print_line " IP360 ASPL" print_line " IP360 XML v3" print_line " Libpcap Packet Capture" print_line " Masscan XML" print_line " Metasploit PWDump Export" print_line " Metasploit XML" print_line " Metasploit Zip Export" print_line " Microsoft Baseline Security Analyzer" print_line " NeXpose Simple XML" print_line " NeXpose XML Report" print_line " Nessus NBE Report" print_line " Nessus XML (v1)" print_line " Nessus XML (v2)" print_line " NetSparker XML" print_line " Nikto XML" print_line " Nmap XML" print_line " OpenVAS Report" print_line " OpenVAS XML" print_line " Outpost24 XML" print_line " Qualys Asset XML" print_line " Qualys Scan XML" print_line " Retina XML" print_line " Spiceworks CSV Export" print_line " Wapiti XML" print_line end # # Generic import that automatically detects the file type # def cmd_db_import(*args) return unless active? ::ApplicationRecord.connection_pool.with_connection { if args.include?("-h") || ! (args && args.length > 0) cmd_db_import_help return end args.each { |glob| files = ::Dir.glob(::File.expand_path(glob)) if files.empty? print_error("No such file #{glob}") next end files.each { |filename| if (not ::File.readable?(filename)) print_error("Could not read file #{filename}") next end begin warnings = 0 framework.db.import_file(:filename => filename) do |type,data| case type when :debug print_error("DEBUG: #{data.inspect}") when :vuln inst = data[1] == 1 ? "instance" : "instances" print_status("Importing vulnerability '#{data[0]}' (#{data[1]} #{inst})") when :filetype print_status("Importing '#{data}' data") when :parser print_status("Import: Parsing with '#{data}'") when :address print_status("Importing host #{data}") when :service print_status("Importing service #{data}") when :msf_loot print_status("Importing loot #{data}") when :msf_task print_status("Importing task #{data}") when :msf_report print_status("Importing report #{data}") when :pcap_count print_status("Import: #{data} packets processed") when :record_count print_status("Import: #{data[1]} records processed") when :warning print_error data.split("\n").each do |line| print_error(line) end print_error warnings += 1 end end print_status("Successfully imported #{filename}") print_error("Please note that there were #{warnings} warnings") if warnings > 1 print_error("Please note that there was one warning") if warnings == 1 rescue Msf::DBImportError => e print_error("Failed to import #{filename}: #{$!}") elog("Failed to import #{filename}", error: e) dlog("Call stack: #{$@.join("\n")}", LEV_3) next rescue REXML::ParseException => e print_error("Failed to import #{filename} due to malformed XML:") print_error("#{e.class}: #{e}") elog("Failed to import #{filename}", error: e) dlog("Call stack: #{$@.join("\n")}", LEV_3) next end } } } end def cmd_db_export_help # Like db_hosts and db_services, this creates a list of columns, so # use its -h cmd_db_export("-h") end # # Export an XML # def cmd_db_export(*args) return unless active? ::ApplicationRecord.connection_pool.with_connection { export_formats = %W{xml pwdump} format = 'xml' output = nil while (arg = args.shift) case arg when '-h','--help' print_line "Usage:" print_line " db_export -f [filename]" print_line " Format can be one of: #{export_formats.join(", ")}" when '-f','--format' format = args.shift.to_s.downcase else output = arg end end if not output print_error("No output file was specified") return end if not export_formats.include?(format) print_error("Unsupported file format: #{format}") print_error("Unsupported file format: '#{format}'. Must be one of: #{export_formats.join(", ")}") return end print_status("Starting export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...") framework.db.run_db_export(output, format) print_status("Finished export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...") } end def find_nmap_path Rex::FileUtils.find_full_path("nmap") || Rex::FileUtils.find_full_path("nmap.exe") end # # Import Nmap data from a file # def cmd_db_nmap(*args) return unless active? ::ApplicationRecord.connection_pool.with_connection { if (args.length == 0) print_status("Usage: db_nmap [--save | [--help | -h]] [nmap options]") return end save = false arguments = [] while (arg = args.shift) case arg when '--save' save = true when '--help', '-h' cmd_db_nmap_help return else arguments << arg end end nmap = find_nmap_path unless nmap print_error("The nmap executable could not be found") return end fd = Rex::Quickfile.new(['msf-db-nmap-', '.xml'], Msf::Config.local_directory) begin # When executing native Nmap in Cygwin, expand the Cygwin path to a Win32 path if(Rex::Compat.is_cygwin and nmap =~ /cygdrive/) # Custom function needed because cygpath breaks on 8.3 dirs tout = Rex::Compat.cygwin_to_win32(fd.path) arguments.push('-oX', tout) else arguments.push('-oX', fd.path) end run_nmap(nmap, arguments) framework.db.import_nmap_xml_file(:filename => fd.path) print_status("Saved NMAP XML results to #{fd.path}") if save ensure fd.close fd.unlink unless save end } end def cmd_db_nmap_help nmap = find_nmap_path unless nmap print_error("The nmap executable could not be found") return end stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help') stdout.each_line do |out_line| next if out_line.strip.empty? print_status(out_line.strip) end stderr.each_line do |err_line| next if err_line.strip.empty? print_error(err_line.strip) end end def cmd_db_nmap_tabs(str, words) nmap = find_nmap_path unless nmap return end stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help') tabs = [] stdout.each_line do |out_line| if out_line.strip.starts_with?('-') tabs.push(out_line.strip.split(':').first) end end stderr.each_line do |err_line| next if err_line.strip.empty? print_error(err_line.strip) end return tabs end # # Database management # def db_check_driver unless framework.db.driver print_error("No database driver installed.") return false end true end # # Is everything working? # def cmd_db_status(*args) return if not db_check_driver if framework.db.connection_established? print_connection_info else print_status("#{framework.db.driver} selected, no connection") end end def cmd_db_connect_help print_line(" USAGE:") print_line(" * Postgres Data Service:") print_line(" db_connect @/") print_line(" Examples:") print_line(" db_connect user@metasploit3") print_line(" db_connect user:pass@192.168.0.2/metasploit3") print_line(" db_connect user:pass@192.168.0.2:1500/metasploit3") print_line(" db_connect -y [path/to/database.yml]") print_line(" ") print_line(" * HTTP Data Service:") print_line(" db_connect [options] ://") print_line(" Examples:") print_line(" db_connect http://localhost:8080") print_line(" db_connect http://my-super-msf-data.service.com") print_line(" db_connect -c ~/cert.pem -t 6a7a74c1a5003802c955ead1bbddd4ab1b05a7f2940b4732d34bfc555bc6e1c5d7611a497b29e8f0 https://localhost:8080") print_line(" NOTE: You must be connected to a Postgres data service in order to successfully connect to a HTTP data service.") print_line(" ") print_line(" Persisting Connections:") print_line(" db_connect --name [options]
") print_line(" Examples:") print_line(" Saving: db_connect --name LA-server http://123.123.123.45:1234") print_line(" Connecting: db_connect LA-server") print_line(" ") print_line(" OPTIONS:") print_line(" -l,--list-services List the available data services that have been previously saved.") print_line(" -y,--yaml Connect to the data service specified in the provided database.yml file.") print_line(" -n,--name Name used to store the connection. Providing an existing name will overwrite the settings for that connection.") print_line(" -c,--cert Certificate file matching the remote data server's certificate. Needed when using self-signed SSL cert.") print_line(" -t,--token The API token used to authenticate to the remote data service.") print_line(" --skip-verify Skip validating authenticity of server's certificate (NOT RECOMMENDED).") print_line("") end def cmd_db_connect(*args) return if not db_check_driver opts = {} while (arg = args.shift) case arg when '-h', '--help' cmd_db_connect_help return when '-y', '--yaml' opts[:yaml_file] = args.shift when '-c', '--cert' opts[:cert] = args.shift when '-t', '--token' opts[:api_token] = args.shift when '-l', '--list-services' list_saved_data_services return when '-n', '--name' opts[:name] = args.shift if opts[:name] =~ /\/|\[|\]/ print_error "Provided name contains an invalid character. Aborting connection." return end when '--skip-verify' opts[:skip_verify] = true else found_name = ::Msf::DbConnector.data_service_search(name: arg) if found_name opts = ::Msf::DbConnector.load_db_config(found_name) else opts[:url] = arg end end end if !opts[:url] && !opts[:yaml_file] print_error 'A URL or saved data service name is required.' print_line cmd_db_connect_help return end if opts[:url] =~ /http/ new_conn_type = 'http' else new_conn_type = framework.db.driver end # Currently only able to be connected to one DB at a time if framework.db.connection_established? # But the http connection still requires a local database to support AR, so we have to allow that # Don't allow more than one HTTP service, though if new_conn_type != 'http' || framework.db.get_services_metadata.count >= 2 print_error('Connection already established. Only one connection is allowed at a time.') print_error('Run db_disconnect first if you wish to connect to a different data service.') print_line print_line 'Current connection information:' print_connection_info return end end result = Msf::DbConnector.db_connect(framework, opts) if result[:error] print_error result[:error] return end if result[:result] print_status result[:result] end if framework.db.active name = opts[:name] if !name || name.empty? if found_name name = found_name elsif result[:data_service_name] name = result[:data_service_name] else name = Rex::Text.rand_text_alphanumeric(8) end end save_db_to_config(framework.db, name) @current_data_service = name end end def cmd_db_disconnect_help print_line "Usage:" print_line " db_disconnect Temporarily disconnects from the currently configured dataservice." print_line " db_disconnect --clear Clears the default dataservice that msfconsole will use when opened." print_line end def cmd_db_disconnect(*args) return if not db_check_driver if args[0] == '-h' || args[0] == '--help' cmd_db_disconnect_help return elsif args[0] == '-c' || args[0] == '--clear' clear_default_db return end previous_name = framework.db.name result = Msf::DbConnector.db_disconnect(framework) if result[:error] print_error "Unable to disconnect from the data service: #{@current_data_service}" print_error result[:error] elsif result[:old_data_service_name].nil? print_error 'Not currently connected to a data service.' else print_line "Successfully disconnected from the data service: #{previous_name}." @current_data_service = result[:data_service_name] if @current_data_service print_line "Now connected to: #{@current_data_service}." end end end def cmd_db_rebuild_cache(*args) print_line "This command is deprecated with Metasploit 5" end def cmd_db_save_help print_line "Usage: db_save" print_line print_line "Save the current data service connection as the default to reconnect on startup." print_line end def cmd_db_save(*args) while (arg = args.shift) case arg when '-h', '--help' cmd_db_save_help return end end if !framework.db.active || !@current_data_service print_error "Not currently connected to a data service that can be saved." return end begin Msf::Config.save(DB_CONFIG_PATH => { 'default_db' => @current_data_service }) print_line "Successfully saved data service as default: #{@current_data_service}" rescue ArgumentError => e print_error e.message end end def save_db_to_config(database, database_name) if database_name =~ /\/|\[|\]/ raise ArgumentError, 'Data service name contains an invalid character.' end config_path = "#{DB_CONFIG_PATH}/#{database_name}" config_opts = {} if !database.is_local? begin config_opts['url'] = database.endpoint if database.https_opts config_opts['cert'] = database.https_opts[:cert] if database.https_opts[:cert] config_opts['skip_verify'] = true if database.https_opts[:skip_verify] end if database.api_token config_opts['api_token'] = database.api_token end Msf::Config.save(config_path => config_opts) rescue => e print_error "There was an error saving the data service configuration: #{e.message}" end else url = Msf::DbConnector.build_postgres_url config_opts['url'] = url Msf::Config.save(config_path => config_opts) end end def cmd_db_remove_help print_line "Usage: db_remove " print_line print_line "Delete the specified saved data service." print_line end def cmd_db_remove(*args) if args[0] == '-h' || args[0] == '--help' || args[0].nil? || args[0].empty? cmd_db_remove_help return end delete_db_from_config(args[0]) end def delete_db_from_config(db_name) conf = Msf::Config.load db_path = "#{DB_CONFIG_PATH}/#{db_name}" if conf[db_path] clear_default_db if conf[DB_CONFIG_PATH]['default_db'] && conf[DB_CONFIG_PATH]['default_db'] == db_name Msf::Config.delete_group(db_path) print_line "Successfully deleted data service: #{db_name}" else print_line "Unable to locate saved data service with name #{db_name}." end end def clear_default_db conf = Msf::Config.load if conf[DB_CONFIG_PATH] && conf[DB_CONFIG_PATH]['default_db'] updated_opts = conf[DB_CONFIG_PATH] updated_opts.delete('default_db') Msf::Config.save(DB_CONFIG_PATH => updated_opts) print_line "Cleared the default data service." else print_line "No default data service was configured." end end def db_find_tools(tools) missed = [] tools.each do |name| if(! Rex::FileUtils.find_full_path(name)) missed << name end end if(not missed.empty?) print_error("This database command requires the following tools to be installed: #{missed.join(", ")}") return end true end ####### private def run_nmap(nmap, arguments, use_sudo: false) print_warning('Running Nmap with sudo') if use_sudo begin nmap_pipe = use_sudo ? ::Open3::popen3('sudo', nmap, *arguments) : ::Open3::popen3(nmap, *arguments) temp_nmap_threads = [] temp_nmap_threads << framework.threads.spawn("db_nmap-Stdout", false, nmap_pipe[1]) do |np_1| np_1.each_line do |nmap_out| next if nmap_out.strip.empty? print_status("Nmap: #{nmap_out.strip}") end end temp_nmap_threads << framework.threads.spawn("db_nmap-Stderr", false, nmap_pipe[2]) do |np_2| np_2.each_line do |nmap_err| next if nmap_err.strip.empty? print_status("Nmap: '#{nmap_err.strip}'") # Check if the stderr text includes 'root', this only happens if the scan requires root privileges if nmap_err =~ /requires? root privileges/ or nmap_err.include? 'only works if you are root' or nmap_err =~ /requires? raw socket access/ return run_nmap(nmap, arguments, use_sudo: true) unless use_sudo end end end temp_nmap_threads.map { |t| t.join rescue nil } nmap_pipe.each { |p| p.close rescue nil } rescue ::IOError end end ####### def print_connection_info cdb = '' if framework.db.driver == 'http' cdb = framework.db.name else ::ApplicationRecord.connection_pool.with_connection do |conn| if conn.respond_to?(:current_database) cdb = conn.current_database end end end output = "Connected to #{cdb}. Connection type: #{framework.db.driver}." output += " Connection name: #{@current_data_service}." if @current_data_service print_status(output) end def list_saved_data_services conf = Msf::Config.load default = nil tbl = Rex::Text::Table.new({ 'Header' => 'Data Services', 'Columns' => ['current', 'name', 'url', 'default?'], 'SortIndex' => 1 }) conf.each_pair do |k,v| if k =~ /#{DB_CONFIG_PATH}/ default = v['default_db'] if v['default_db'] name = k.split('/').last next if name == 'database' # Data service information is not stored in 'framework/database', just metadata url = v['url'] current = '' current = '*' if name == @current_data_service default_output = '' default_output = '*' if name == default line = [current, name, url, default_output] tbl << line end end print_line print_line tbl.to_s end def print_msgs(status_msg, error_msg) status_msg.each do |s| print_status(s) end error_msg.each do |e| print_error(e) end end end end end end end