Path: blob/master/lib/msf/ui/console/command_dispatcher/db.rb
19851 views
# -*- coding: binary -*-12require 'json'3require 'rexml/document'4require 'metasploit/framework/data_service'5require 'metasploit/framework/data_service/remote/http/core'67module Msf8module Ui9module Console10module CommandDispatcher1112class Db1314require 'tempfile'1516include Msf::Ui::Console::CommandDispatcher17include Msf::Ui::Console::CommandDispatcher::Common18include Msf::Ui::Console::CommandDispatcher::Db::Common19include Msf::Ui::Console::CommandDispatcher::Db::Analyze20include Msf::Ui::Console::CommandDispatcher::Db::Klist21include Msf::Ui::Console::CommandDispatcher::Db::Certs2223DB_CONFIG_PATH = 'framework/database'2425#26# The dispatcher's name.27#28def name29"Database Backend"30end3132#33# Returns the hash of commands supported by this dispatcher.34#35def commands36base = {37"db_connect" => "Connect to an existing data service",38"db_disconnect" => "Disconnect from the current data service",39"db_status" => "Show the current data service status",40"db_save" => "Save the current data service connection as the default to reconnect on startup",41"db_remove" => "Remove the saved data service entry"42}4344more = {45"workspace" => "Switch between database workspaces",46"hosts" => "List all hosts in the database",47"services" => "List all services in the database",48"vulns" => "List all vulnerabilities in the database",49"notes" => "List all notes in the database",50"loot" => "List all loot in the database",51"klist" => "List Kerberos tickets in the database",52"certs" => "List Pkcs12 certificate bundles in the database",53"db_import" => "Import a scan result file (filetype will be auto-detected)",54"db_export" => "Export a file containing the contents of the database",55"db_nmap" => "Executes nmap and records the output automatically",56"db_rebuild_cache" => "Rebuilds the database-stored module cache (deprecated)",57"analyze" => "Analyze database information about a specific address or address range",58"db_stats" => "Show statistics for the database"59}6061# Always include commands that only make sense when connected.62# This avoids the problem of them disappearing unexpectedly if the63# database dies or times out. See #19236465base.merge(more)66end6768def deprecated_commands69[70"db_autopwn",71"db_driver",72"db_hosts",73"db_notes",74"db_services",75"db_vulns",76]77end7879#80# Attempts to connect to the previously configured database, and additionally keeps track of81# the currently loaded data service.82#83def load_config(path = nil)84result = Msf::DbConnector.db_connect_from_config(framework, path)8586if result[:error]87print_error(result[:error])88end89if result[:data_service_name]90@current_data_service = result[:data_service_name]91end92end9394@@workspace_opts = Rex::Parser::Arguments.new(95[ '-h', '--help' ] => [ false, 'Help banner.'],96[ '-a', '--add' ] => [ true, 'Add a workspace.', '<name>'],97[ '-d', '--delete' ] => [ true, 'Delete a workspace.', '<name>'],98[ '-D', '--delete-all' ] => [ false, 'Delete all workspaces.'],99[ '-r', '--rename' ] => [ true, 'Rename a workspace.', '<old> <new>'],100[ '-l', '--list' ] => [ false, 'List workspaces.'],101[ '-v', '--list-verbose' ] => [ false, 'List workspaces verbosely.'],102[ '-S', '--search' ] => [ true, 'Search for a workspace.', '<name>']103)104105def cmd_workspace_help106print_line "Usage:"107print_line " workspace List workspaces"108print_line " workspace [name] Switch workspace"109print_line @@workspace_opts.usage110end111112def cmd_workspace(*args)113return unless active?114115state = :nil116117list = false118verbose = false119names = []120search_term = nil121122@@workspace_opts.parse(args) do |opt, idx, val|123case opt124when '-h', '--help'125cmd_workspace_help126return127when '-a', '--add'128return cmd_workspace_help unless state == :nil129130state = :adding131names << val if !val.nil?132when '-d', '--del'133return cmd_workspace_help unless state == :nil134135state = :deleting136names << val if !val.nil?137when '-D', '--delete-all'138return cmd_workspace_help unless state == :nil139140state = :delete_all141when '-r', '--rename'142return cmd_workspace_help unless state == :nil143144state = :renaming145names << val if !val.nil?146when '-v', '--verbose'147verbose = true148when '-l', '--list'149list = true150when '-S', '--search'151search_term = val152else153names << val if !val.nil?154end155end156157if state == :adding and names158# Add workspaces159wspace = nil160names.each do |name|161wspace = framework.db.workspaces(name: name).first162if wspace163print_status("Workspace '#{wspace.name}' already existed, switching to it.")164else165wspace = framework.db.add_workspace(name)166print_status("Added workspace: #{wspace.name}")167end168end169framework.db.workspace = wspace170print_status("Workspace: #{framework.db.workspace.name}")171elsif state == :deleting and names172ws_ids_to_delete = []173starting_ws = framework.db.workspace174names.uniq.each do |n|175ws = framework.db.workspaces(name: n).first176ws_ids_to_delete << ws.id if ws177end178if ws_ids_to_delete.count > 0179deleted = framework.db.delete_workspaces(ids: ws_ids_to_delete)180process_deleted_workspaces(deleted, starting_ws)181else182print_status("No workspaces matching the given name(s) were found.")183end184elsif state == :delete_all185ws_ids_to_delete = []186starting_ws = framework.db.workspace187framework.db.workspaces.each do |ws|188ws_ids_to_delete << ws.id189end190deleted = framework.db.delete_workspaces(ids: ws_ids_to_delete)191process_deleted_workspaces(deleted, starting_ws)192elsif state == :renaming193if names.length != 2194print_error("Wrong number of arguments to rename")195return196end197198ws_to_update = framework.db.find_workspace(names.first)199unless ws_to_update200print_error("Workspace '#{names.first}' does not exist")201return202end203opts = {204id: ws_to_update.id,205name: names.last206}207begin208updated_ws = framework.db.update_workspace(opts)209if updated_ws210framework.db.workspace = updated_ws if names.first == framework.db.workspace.name211print_status("Renamed workspace '#{names.first}' to '#{updated_ws.name}'")212else213print_error "There was a problem updating the workspace. Setting to the default workspace."214framework.db.workspace = framework.db.default_workspace215return216end217if names.first == Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME218print_status("Recreated default workspace")219end220rescue => e221print_error "Failed to rename workspace: #{e.message}"222end223224elsif !names.empty?225name = names.last226# Switch workspace227workspace = framework.db.find_workspace(name)228if workspace229framework.db.workspace = workspace230print_status("Workspace: #{workspace.name}")231else232print_error("Workspace not found: #{name}")233return234end235else236current_workspace = framework.db.workspace237238unless verbose239current = nil240framework.db.workspaces.sort_by {|s| s.name}.each do |s|241if s.name == current_workspace.name242current = s.name243else244print_line(" #{s.name}")245end246end247print_line("%red* #{current}%clr") unless current.nil?248return249end250col_names = %w{current name hosts services vulns creds loots notes}251252tbl = Rex::Text::Table.new(253'Header' => 'Workspaces',254'Columns' => col_names,255'SortIndex' => -1,256'SearchTerm' => search_term257)258259framework.db.workspaces.each do |ws|260tbl << [261current_workspace.name == ws.name ? '*' : '',262ws.name,263framework.db.hosts(workspace: ws.name).count,264framework.db.services(workspace: ws.name).count,265framework.db.vulns(workspace: ws.name).count,266framework.db.creds(workspace: ws.name).count,267framework.db.loots(workspace: ws.name).count,268framework.db.notes(workspace: ws.name).count269]270end271272print_line273print_line(tbl.to_s)274end275end276277def process_deleted_workspaces(deleted_workspaces, starting_ws)278deleted_workspaces.each do |ws|279print_status "Deleted workspace: #{ws.name}"280if ws.name == Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME281framework.db.workspace = framework.db.default_workspace282print_status 'Recreated the default workspace'283elsif ws == starting_ws284framework.db.workspace = framework.db.default_workspace285print_status "Switched to workspace: #{framework.db.workspace.name}"286end287end288end289290def cmd_workspace_tabs(str, words)291return [] unless active?292framework.db.workspaces.map(&:name) if (words & ['-a','--add']).empty?293end294295#296# Tab completion for the hosts command297#298# @param str [String] the string currently being typed before tab was hit299# @param words [Array<String>] the previously completed words on the command line. words is always300# at least 1 when tab completion has reached this stage since the command itself has been completed301def cmd_hosts_tabs(str, words)302if words.length == 1303return @@hosts_opts.option_keys.select { |opt| opt.start_with?(str) }304end305306case words[-1]307when '-c', '--columns', '-C', '--columns-until-restart'308return @@hosts_columns309when '-o', '--output'310return tab_complete_filenames(str, words)311end312313if @@hosts_opts.arg_required?(words[-1])314return []315end316317return @@hosts_opts.option_keys.select { |opt| opt.start_with?(str) }318end319320def cmd_hosts_help321# This command does some lookups for the list of appropriate column322# names, so instead of putting all the usage stuff here like other323# help methods, just use it's "-h" so we don't have to recreating324# that list325cmd_hosts("-h")326end327328# Changes the specified host data329#330# @param host_ranges - range of hosts to process331# @param host_data - hash of host data to be updated332def change_host_data(host_ranges, host_data)333if !host_data || host_data.length != 1334print_error("A single key-value data hash is required to change the host data")335return336end337attribute = host_data.keys[0]338339if host_ranges == [nil]340print_error("In order to change the host #{attribute}, you must provide a range of hosts")341return342end343344each_host_range_chunk(host_ranges) do |host_search|345next if host_search && host_search.empty?346347framework.db.hosts(address: host_search).each do |host|348framework.db.update_host(host_data.merge(id: host.id))349framework.db.report_note(host: host.address, type: "host.#{attribute}", data: { :host_data => host_data[attribute] })350end351end352end353354def add_host_tag(rws, tag_name)355if rws == [nil]356print_error("In order to add a tag, you must provide a range of hosts")357return358end359360opts = Hash.new()361opts[:workspace] = framework.db.workspace362opts[:tag_name] = tag_name363364rws.each do |rw|365rw.each do |ip|366opts[:address] = ip367unless framework.db.add_host_tag(opts)368print_error("Host #{ip} could not be found.")369end370end371end372end373374def find_host_tags(workspace, host_id)375opts = Hash.new()376opts[:workspace] = workspace377opts[:id] = host_id378379framework.db.get_host_tags(opts)380end381382def delete_host_tag(rws, tag_name)383opts = Hash.new()384opts[:workspace] = framework.db.workspace385opts[:tag_name] = tag_name386387# This will be the case if no IP was passed in, and we are just trying to delete all388# instances of a given tag within the database.389if rws == [nil]390wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)391wspace.hosts.each do |host|392opts[:address] = host.address393framework.db.delete_host_tag(opts)394end395else396rws.each do |rw|397rw.each do |ip|398opts[:address] = ip399unless framework.db.delete_host_tag(opts)400print_error("Host #{ip} could not be found.")401end402end403end404end405end406407@@hosts_columns = [ 'address', 'mac', 'name', 'os_name', 'os_flavor', 'os_sp', 'purpose', 'info', 'comments']408409@@hosts_opts = Rex::Parser::Arguments.new(410[ '-h', '--help' ] => [ false, 'Show this help information' ],411[ '-a', '--add' ] => [ true, 'Add the hosts instead of searching', '<host>' ],412[ '-u', '--up' ] => [ false, 'Only show hosts which are up' ],413[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search' ],414[ '-S', '--search' ] => [ true, 'Search string to filter by', '<filter>' ],415[ '-i', '--info' ] => [ true, 'Change the info of a host', '<info>' ],416[ '-n', '--name' ] => [ true, 'Change the name of a host', '<name>' ],417[ '-m', '--comment' ] => [ true, 'Change the comment of a host', '<comment>' ],418[ '-t', '--tag' ] => [ true, 'Add or specify a tag to a range of hosts', '<tag>' ],419[ '-T', '--delete-tag' ] => [ true, 'Remove a tag from a range of hosts', '<tag>' ],420[ '-d', '--delete' ] => [ true, 'Delete the hosts instead of searching', '<hosts>' ],421[ '-o', '--output' ] => [ true, 'Send output to a file in csv format', '<filename>' ],422[ '-O', '--order' ] => [ true, 'Order rows by specified column number', '<column id>' ],423[ '-c', '--columns' ] => [ true, 'Only show the given columns (see list below)', '<columns>' ],424[ '-C', '--columns-until-restart' ] => [ true, 'Only show the given columns until the next restart (see list below)', '<columns>' ],425)426427def cmd_hosts(*args)428return unless active?429onlyup = false430set_rhosts = false431mode = []432delete_count = 0433434rhosts = []435host_ranges = []436search_term = nil437438order_by = nil439info_data = nil440name_data = nil441comment_data = nil442tag_name = nil443444output = nil445default_columns = [446'address',447'arch',448'comm',449'comments',450'created_at',451'cred_count',452'detected_arch',453'exploit_attempt_count',454'host_detail_count',455'info',456'mac',457'name',458'note_count',459'os_family',460'os_flavor',461'os_lang',462'os_name',463'os_sp',464'purpose',465'scope',466'service_count',467'state',468'updated_at',469'virtual_host',470'vuln_count',471'workspace_id']472473default_columns << 'tags' # Special case474virtual_columns = [ 'svcs', 'vulns', 'workspace', 'tags' ]475476col_search = @@hosts_columns477478default_columns.delete_if {|v| (v[-2,2] == "id")}479@@hosts_opts.parse(args) do |opt, idx, val|480case opt481when '-h', '--help'482print_line "Usage: hosts [ options ] [addr1 addr2 ...]"483print_line484print @@hosts_opts.usage485print_line486print_line "Available columns: #{default_columns.join(", ")}"487print_line488return489when '-a', '--add'490mode << :add491arg_host_range(val, host_ranges)492when '-d', '--delete'493mode << :delete494arg_host_range(val, host_ranges)495when '-u', '--up'496onlyup = true497when '-o'498output = val499output = ::File.expand_path(output)500when '-R', '--rhosts'501set_rhosts = true502when '-S', '--search'503search_term = val504when '-i', '--info'505mode << :new_info506info_data = val507when '-n', '--name'508mode << :new_name509name_data = val510when '-m', '--comment'511mode << :new_comment512comment_data = val513when '-t', '--tag'514mode << :tag515tag_name = val516when '-T', '--delete-tag'517mode << :delete_tag518tag_name = val519when '-c', '-C'520list = val521if(!list)522print_error("Invalid column list")523return524end525col_search = list.strip().split(",")526col_search.each { |c|527if not default_columns.include?(c) and not virtual_columns.include?(c)528all_columns = default_columns + virtual_columns529print_error("Invalid column list. Possible values are (#{all_columns.join("|")})")530return531end532}533if opt == '-C'534@@hosts_columns = col_search535end536when '-O'537if (order_by = val.to_i - 1) < 0538print_error('Please specify a column number starting from 1')539return540end541else542# Anything that wasn't an option is a host to search for543unless (arg_host_range(val, host_ranges))544return545end546end547end548549if col_search550col_names = col_search551else552col_names = default_columns + virtual_columns553end554555mode << :search if mode.empty?556557if mode == [:add]558host_ranges.each do |range|559range.each do |address|560host = framework.db.find_or_create_host(:host => address)561print_status("Time: #{host.created_at} Host: host=#{host.address}")562end563end564return565end566567cp_hsh = {}568col_names.map do |col|569cp_hsh[col] = { 'MaxChar' => 52 }570end571# If we got here, we're searching. Delete implies search572tbl = Rex::Text::Table.new(573{574'Header' => "Hosts",575'Columns' => col_names,576'ColProps' => cp_hsh,577'SortIndex' => order_by578})579580# Sentinel value meaning all581host_ranges.push(nil) if host_ranges.empty?582583case584when mode == [:new_info]585change_host_data(host_ranges, info: info_data)586return587when mode == [:new_name]588change_host_data(host_ranges, name: name_data)589return590when mode == [:new_comment]591change_host_data(host_ranges, comments: comment_data)592return593when mode == [:tag]594begin595add_host_tag(host_ranges, tag_name)596rescue => e597if e.message.include?('Validation failed')598print_error(e.message)599else600raise e601end602end603return604when mode == [:delete_tag]605begin606delete_host_tag(host_ranges, tag_name)607rescue => e608if e.message.include?('Validation failed')609print_error(e.message)610else611raise e612end613end614return615end616617matched_host_ids = []618each_host_range_chunk(host_ranges) do |host_search|619next if host_search && host_search.empty?620621framework.db.hosts(address: host_search, non_dead: onlyup, search_term: search_term).each do |host|622matched_host_ids << host.id623columns = col_names.map do |n|624# Deal with the special cases625if virtual_columns.include?(n)626case n627when "svcs"; host.service_count628when "vulns"; host.vuln_count629when "workspace"; host.workspace.name630when "tags"631found_tags = find_host_tags(framework.db.workspace, host.id)632tag_names = found_tags.map(&:name).join(', ')633tag_names634end635# Otherwise, it's just an attribute636else637host[n] || ""638end639end640641tbl << columns642if set_rhosts643addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address)644rhosts << addr645end646end647648if mode == [:delete]649result = framework.db.delete_host(ids: matched_host_ids)650delete_count += result.size651end652end653654if output655print_status("Wrote hosts to #{output}")656::File.open(output, "wb") { |ofd|657ofd.write(tbl.to_csv)658}659else660print_line661print_line(tbl.to_s)662end663664# Finally, handle the case where the user wants the resulting list665# of hosts to go into RHOSTS.666set_rhosts_from_addrs(rhosts.uniq) if set_rhosts667668print_status("Deleted #{delete_count} hosts") if delete_count > 0669end670671#672# Tab completion for the services command673#674# @param str [String] the string currently being typed before tab was hit675# @param words [Array<String>] the previously completed words on the command line. words is always676# at least 1 when tab completion has reached this stage since the command itself has been completed677def cmd_services_tabs(str, words)678if words.length == 1679return @@services_opts.option_keys.select { |opt| opt.start_with?(str) }680end681682case words[-1]683when '-c', '--column'684return @@services_columns685when '-O', '--order'686return []687when '-o', '--output'688return tab_complete_filenames(str, words)689when '-p', '--port'690return []691when '-r', '--protocol'692return []693end694695[]696end697698def cmd_services_help699print_line "Usage: services [-h] [-u] [-a] [-r <proto>] [-p <port1,port2>] [-s <name1,name2>] [-o <filename>] [addr1 addr2 ...]"700print_line701print @@services_opts.usage702print_line703print_line "Available columns: #{@@services_columns.join(", ")}"704print_line705end706707@@services_columns = [ 'created_at', 'info', 'name', 'port', 'proto', 'state', 'updated_at' ]708709@@services_opts = Rex::Parser::Arguments.new(710[ '-a', '--add' ] => [ false, 'Add the services instead of searching.' ],711[ '-d', '--delete' ] => [ false, 'Delete the services instead of searching.' ],712[ '-U', '--update' ] => [ false, 'Update data for existing service.' ],713[ '-u', '--up' ] => [ false, 'Only show services which are up.' ],714[ '-c', '--column' ] => [ true, 'Only show the given columns.', '<col1,col2>' ],715[ '-p', '--port' ] => [ true, 'Search for a list of ports.', '<ports>' ],716[ '-r', '--protocol' ] => [ true, 'Protocol type of the service being added [tcp|udp].', '<protocol>' ],717[ '-s', '--name' ] => [ true, 'Name of the service to add.', '<name>' ],718[ '-o', '--output' ] => [ true, 'Send output to a file in csv format.', '<filename>' ],719[ '-O', '--order' ] => [ true, 'Order rows by specified column number.', '<column id>' ],720[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],721[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],722[ '-h', '--help' ] => [ false, 'Show this help information.' ]723)724725def db_connection_info(framework)726unless framework.db.connection_established?727return "#{framework.db.driver} selected, no connection"728end729730cdb = ''731if framework.db.driver == 'http'732cdb = framework.db.name733else734::ApplicationRecord.connection_pool.with_connection do |conn|735if conn.respond_to?(:current_database)736cdb = conn.current_database737end738end739end740741if cdb.empty?742output = "Connected Database Name could not be extracted. DB Connection type: #{framework.db.driver}."743else744output = "Connected to #{cdb}. Connection type: #{framework.db.driver}."745end746747output748end749750def cmd_db_stats(*args)751return unless active?752print_line "Session Type: #{db_connection_info(framework)}"753754current_workspace = framework.db.workspace755example_workspaces = ::Mdm::Workspace.order(id: :desc)756ordered_workspaces = ([current_workspace] + example_workspaces).uniq.sort_by(&:id)757758tbl = Rex::Text::Table.new(759'Indent' => 2,760'Header' => "Database Stats",761'Columns' =>762[763"IsTarget",764"ID",765"Name",766"Hosts",767"Services",768"Services per Host",769"Vulnerabilities",770"Vulns per Host",771"Notes",772"Creds",773"Kerberos Cache"774],775'SortIndex' => 1,776'ColProps' => {777'IsTarget' => {778'Stylers' => [Msf::Ui::Console::TablePrint::RowIndicatorStyler.new],779'ColumnStylers' => [Msf::Ui::Console::TablePrint::OmitColumnHeader.new],780'Width' => 2781}782}783)784785total_hosts = 0786total_services = 0787total_vulns = 0788total_notes = 0789total_creds = 0790total_tickets = 0791792ordered_workspaces.map do |workspace|793794hosts = workspace.hosts.count795services = workspace.services.count796vulns = workspace.vulns.count797notes = workspace.notes.count798creds = framework.db.creds(workspace: workspace.name).count # workspace.creds.count.to_fs(:delimited) is always 0 for whatever reason799kerbs = ticket_search([nil], nil, :workspace => workspace).count800801total_hosts += hosts802total_services += services803total_vulns += vulns804total_notes += notes805total_creds += creds806total_tickets += kerbs807808tbl << [809current_workspace.id == workspace.id,810workspace.id,811workspace.name,812hosts.to_fs(:delimited),813services.to_fs(:delimited),814hosts > 0 ? (services.to_f / hosts).truncate(2) : 0,815vulns.to_fs(:delimited),816hosts > 0 ? (vulns.to_f / hosts).truncate(2) : 0,817notes.to_fs(:delimited),818creds.to_fs(:delimited),819kerbs.to_fs(:delimited)820]821end822823# total row824tbl << [825"",826"Total",827ordered_workspaces.length.to_fs(:delimited),828total_hosts.to_fs(:delimited),829total_services.to_fs(:delimited),830total_hosts > 0 ? (total_services.to_f / total_hosts).truncate(2) : 0,831total_vulns,832total_hosts > 0 ? (total_vulns.to_f / total_hosts).truncate(2) : 0,833total_notes,834total_creds.to_fs(:delimited),835total_tickets.to_fs(:delimited)836]837838print_line tbl.to_s839end840841def cmd_services(*args)842return unless active?843mode = :search844onlyup = false845output_file = nil846set_rhosts = false847col_search = ['port', 'proto', 'name', 'state', 'info']848849names = nil850order_by = nil851proto = nil852host_ranges = []853port_ranges = []854rhosts = []855delete_count = 0856search_term = nil857opts = {}858859@@services_opts.parse(args) do |opt, idx, val|860case opt861when '-a', '--add'862mode = :add863when '-d', '--delete'864mode = :delete865when '-U', '--update'866mode = :update867when '-u', '--up'868onlyup = true869when '-c'870list = val871if(!list)872print_error("Invalid column list")873return874end875col_search = list.strip().split(",")876col_search.each { |c|877if not @@services_columns.include? c878print_error("Invalid column list. Possible values are (#{@@services_columns.join("|")})")879return880end881}882when '-p'883unless (arg_port_range(val, port_ranges, true))884return885end886when '-r'887proto = val888if (!proto)889print_status("Invalid protocol")890return891end892proto = proto.strip893when '-s'894namelist = val895if (!namelist)896print_error("Invalid name list")897return898end899names = namelist.strip().split(",")900when '-o'901output_file = val902if (!output_file)903print_error("Invalid output filename")904return905end906output_file = ::File.expand_path(output_file)907when '-O'908if (order_by = val.to_i - 1) < 0909print_error('Please specify a column number starting from 1')910return911end912when '-R', '--rhosts'913set_rhosts = true914when '-S', '--search'915search_term = val916opts[:search_term] = search_term917when '-h', '--help'918cmd_services_help919return920else921# Anything that wasn't an option is a host to search for922unless (arg_host_range(val, host_ranges))923return924end925end926end927928ports = port_ranges.flatten.uniq929930if mode == :add931# Can only deal with one port and one service name at a time932# right now. Them's the breaks.933if ports.length != 1934print_error("Exactly one port required")935return936end937if host_ranges.empty?938print_error("Host address or range required")939return940end941host_ranges.each do |range|942range.each do |addr|943info = {944:host => addr,945:port => ports.first.to_i946}947info[:proto] = proto.downcase if proto948info[:name] = names.first.downcase if names and names.first949950svc = framework.db.find_or_create_service(info)951print_status("Time: #{svc.created_at} Service: host=#{svc.host.address} port=#{svc.port} proto=#{svc.proto} name=#{svc.name}")952end953end954return955end956957# If we got here, we're searching. Delete implies search958col_names = @@services_columns959if col_search960col_names = col_search961end962tbl = Rex::Text::Table.new({963'Header' => "Services",964'Columns' => ['host'] + col_names,965'SortIndex' => order_by966})967968# Sentinel value meaning all969host_ranges.push(nil) if host_ranges.empty?970ports = nil if ports.empty?971matched_service_ids = []972973each_host_range_chunk(host_ranges) do |host_search|974next if host_search && host_search.empty?975opts[:workspace] = framework.db.workspace976opts[:hosts] = {address: host_search} if !host_search.nil?977opts[:port] = ports if ports978framework.db.services(opts).each do |service|979980unless service.state == 'open'981next if onlyup982end983984host = service.host985matched_service_ids << service.id986987if mode == :update988service.name = names.first if names989service.proto = proto if proto990service.port = ports.first if ports991framework.db.update_service(service.as_json.symbolize_keys)992end993994columns = [host.address] + col_names.map { |n| service[n].to_s || "" }995tbl << columns996if set_rhosts997addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address )998rhosts << addr999end1000end1001end10021003if (mode == :delete)1004result = framework.db.delete_service(ids: matched_service_ids)1005delete_count += result.size1006end10071008if (output_file == nil)1009print_line(tbl.to_s)1010else1011# create the output file1012::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) }1013print_status("Wrote services to #{output_file}")1014end10151016# Finally, handle the case where the user wants the resulting list1017# of hosts to go into RHOSTS.1018set_rhosts_from_addrs(rhosts.uniq) if set_rhosts10191020print_status("Deleted #{delete_count} services") if delete_count > 010211022end10231024#1025# Tab completion for the vulns command1026#1027# @param str [String] the string currently being typed before tab was hit1028# @param words [Array<String>] the previously completed words on the command line. words is always1029# at least 1 when tab completion has reached this stage since the command itself has been completed1030def cmd_vulns_tabs(str, words)1031if words.length == 11032return @@vulns_opts.option_keys.select { |opt| opt.start_with?(str) }1033end1034case words[-1]1035when '-o', '--output'1036return tab_complete_filenames(str, words)1037end1038end10391040def cmd_vulns_help1041print_line "Print all vulnerabilities in the database"1042print_line1043print_line "Usage: vulns [addr range]"1044print_line1045print @@vulns_opts.usage1046print_line1047print_line "Examples:"1048print_line " vulns -p 1-65536 # only vulns with associated services"1049print_line " vulns -p 1-65536 -s http # identified as http on any port"1050print_line1051end10521053@@vulns_opts = Rex::Parser::Arguments.new(1054[ '-h', '--help' ] => [ false, 'Show this help information.' ],1055[ '-o', '--output' ] => [ true, 'Send output to a file in csv format.', '<filename>' ],1056[ '-p', '--port' ] => [ true, 'List vulns matching this port spec.', '<port>' ],1057[ '-s', '--service' ] => [ true, 'List vulns matching these service names.', '<name>' ],1058[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],1059[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],1060[ '-i', '--info' ] => [ false, 'Display vuln information.' ],1061[ '-d', '--delete' ] => [ false, 'Delete vulnerabilities. Not officially supported.' ],1062[ '-v', '--verbose' ] => [ false, 'Display additional information.' ]1063)10641065def cmd_vulns(*args)1066return unless active?10671068default_columns = ['Timestamp', 'Host', 'Name', 'References']1069host_ranges = []1070port_ranges = []1071svcs = []1072rhosts = []10731074search_term = nil1075show_info = false1076show_vuln_attempts = false1077set_rhosts = false1078output_file = nil1079delete_count = 010801081mode = nil10821083@@vulns_opts.parse(args) do |opt, idx, val|1084case opt1085when '-d', '--delete' # TODO: This is currently undocumented because it's not officially supported.1086mode = :delete1087when '-h', '--help'1088cmd_vulns_help1089return1090when '-o', '--output'1091output_file = val1092if output_file1093output_file = File.expand_path(output_file)1094else1095print_error("Invalid output filename")1096return1097end1098when '-p', '--port'1099unless (arg_port_range(val, port_ranges, true))1100return1101end1102when '-s', '--service'1103service = val1104if (!service)1105print_error("Argument required for -s")1106return1107end1108svcs = service.split(/[\s]*,[\s]*/)1109when '-R', '--rhosts'1110set_rhosts = true1111when '-S', '--search'1112search_term = val1113when '-i', '--info'1114show_info = true1115when '-v', '--verbose'1116show_vuln_attempts = true1117else1118# Anything that wasn't an option is a host to search for1119unless (arg_host_range(val, host_ranges))1120return1121end1122end1123end11241125if show_info1126default_columns << 'Information'1127end11281129# add sentinel value meaning all if empty1130host_ranges.push(nil) if host_ranges.empty?1131# normalize1132ports = port_ranges.flatten.uniq1133svcs.flatten!1134tbl = Rex::Text::Table.new(1135'Header' => 'Vulnerabilities',1136'Columns' => default_columns1137)11381139matched_vuln_ids = []1140vulns = []1141if host_ranges.compact.empty?1142vulns = framework.db.vulns({:search_term => search_term})1143else1144each_host_range_chunk(host_ranges) do |host_search|1145next if host_search && host_search.empty?11461147vulns.concat(framework.db.vulns({:hosts => { :address => host_search }, :search_term => search_term }))1148end1149end11501151vulns.each do |vuln|1152reflist = vuln.refs.map {|r| r.name}1153if (vuln.service)1154# Skip this one if the user specified a port and it1155# doesn't match.1156next unless ports.empty? or ports.include? vuln.service.port1157# Same for service names1158next unless svcs.empty? or svcs.include?(vuln.service.name)1159else1160# This vuln has no service, so it can't match1161next unless ports.empty? and svcs.empty?1162end11631164matched_vuln_ids << vuln.id11651166row = []1167row << vuln.created_at1168row << vuln.host.address1169row << vuln.name1170row << reflist.join(',')1171if show_info1172row << vuln.info1173end1174tbl << row11751176if set_rhosts1177addr = (vuln.host.scope.to_s != "" ? vuln.host.address + '%' + vuln.host.scope : vuln.host.address)1178rhosts << addr1179end1180end11811182if mode == :delete1183result = framework.db.delete_vuln(ids: matched_vuln_ids)1184delete_count = result.size1185end11861187if output_file1188if show_vuln_attempts1189print_warning("Cannot output to a file when verbose mode is enabled. Please remove verbose flag and try again.")1190else1191File.write(output_file, tbl.to_csv)1192print_status("Wrote vulnerability information to #{output_file}")1193end1194else1195print_line1196if show_vuln_attempts1197vulns_and_attempts = _format_vulns_and_vuln_attempts(vulns)1198_print_vulns_and_attempts(vulns_and_attempts)1199else1200print_line(tbl.to_s)1201end1202end12031204# Finally, handle the case where the user wants the resulting list1205# of hosts to go into RHOSTS.1206set_rhosts_from_addrs(rhosts.uniq) if set_rhosts12071208print_status("Deleted #{delete_count} vulnerabilities") if delete_count > 01209end12101211#1212# Tab completion for the notes command1213#1214# @param str [String] the string currently being typed before tab was hit1215# @param words [Array<String>] the previously completed words on the command line. words is always1216# at least 1 when tab completion has reached this stage since the command itself has been completed1217def cmd_notes_tabs(str, words)1218if words.length == 11219return @@notes_opts.option_keys.select { |opt| opt.start_with?(str) }1220end12211222case words[-1]1223when '-O', '--order'1224return []1225when '-o', '--output'1226return tab_complete_filenames(str, words)1227end12281229[]1230end12311232def cmd_notes_help1233print_line "Usage: notes [-h] [-t <type1,type2>] [-n <data string>] [-a] [addr range]"1234print_line1235print @@notes_opts.usage1236print_line1237print_line "Examples:"1238print_line " notes --add -t apps -n 'winzip' 10.1.1.34 10.1.20.41"1239print_line " notes -t smb.fingerprint 10.1.1.34 10.1.20.41"1240print_line " notes -S 'nmap.nse.(http|rtsp)'"1241print_line1242end12431244@@notes_opts = Rex::Parser::Arguments.new(1245[ '-a', '--add' ] => [ false, 'Add a note to the list of addresses, instead of listing.' ],1246[ '-d', '--delete' ] => [ false, 'Delete the notes instead of searching.' ],1247[ '-h', '--help' ] => [ false, 'Show this help information.' ],1248[ '-n', '--note' ] => [ true, 'Set the data for a new note (only with -a).', '<note>' ],1249[ '-O', '--order' ] => [ true, 'Order rows by specified column number.', '<column id>' ],1250[ '-o', '--output' ] => [ true, 'Save the notes to a csv file.', '<filename>' ],1251[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],1252[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],1253[ '-t', '--type' ] => [ true, 'Search for a list of types, or set single type for add.', '<type1,type2>' ],1254[ '-u', '--update' ] => [ false, 'Update a note. Not officially supported.' ]1255)12561257def cmd_notes(*args)1258return unless active?1259::ApplicationRecord.connection_pool.with_connection {1260mode = :search1261data = nil1262types = nil1263set_rhosts = false12641265host_ranges = []1266rhosts = []1267search_term = nil1268output_file = nil1269delete_count = 01270order_by = nil12711272@@notes_opts.parse(args) do |opt, idx, val|1273case opt1274when '-a', '--add'1275mode = :add1276when '-d', '--delete'1277mode = :delete1278when '-n', '--note'1279data = val1280if(!data)1281print_error("Can't make a note with no data")1282return1283end1284when '-t', '--type'1285typelist = val1286if(!typelist)1287print_error("Invalid type list")1288return1289end1290types = typelist.strip().split(",")1291when '-R', '--rhosts'1292set_rhosts = true1293when '-S', '--search'1294search_term = val1295when '-o', '--output'1296output_file = val1297output_file = ::File.expand_path(output_file)1298when '-O'1299if (order_by = val.to_i - 1) < 01300print_error('Please specify a column number starting from 1')1301return1302end1303when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported.1304mode = :update1305when '-h', '--help'1306cmd_notes_help1307return1308else1309# Anything that wasn't an option is a host to search for1310unless (arg_host_range(val, host_ranges))1311return1312end1313end1314end13151316if mode == :add1317if host_ranges.compact.empty?1318print_error("Host address or range required")1319return1320end13211322if types.nil? || types.size != 11323print_error("Exactly one type is required")1324return1325end13261327if data.nil?1328print_error("Data required")1329return1330end13311332type = types.first1333host_ranges.each { |range|1334range.each { |addr|1335note = framework.db.find_or_create_note(host: addr, type: type, data: data)1336break if not note1337print_status("Time: #{note.created_at} Note: host=#{addr} type=#{note.ntype} data=#{note.data}")1338}1339}1340return1341end13421343if mode == :update1344if !types.nil? && types.size != 11345print_error("Exactly one type is required")1346return1347end13481349if types.nil? && data.nil?1350print_error("Update requires data or type")1351return1352end1353end13541355note_list = []1356if host_ranges.compact.empty?1357# No host specified - collect all notes1358opts = {search_term: search_term}1359opts[:ntype] = types if mode != :update && types && !types.empty?1360note_list = framework.db.notes(opts)1361else1362# Collect notes of specified hosts1363each_host_range_chunk(host_ranges) do |host_search|1364next if host_search && host_search.empty?13651366opts = {hosts: {address: host_search}, workspace: framework.db.workspace, search_term: search_term}1367opts[:ntype] = types if mode != :update && types && !types.empty?1368note_list.concat(framework.db.notes(opts))1369end1370end13711372# Now display them1373table = Rex::Text::Table.new(1374'Header' => 'Notes',1375'Indent' => 1,1376'Columns' => ['Time', 'Host', 'Service', 'Port', 'Protocol', 'Type', 'Data'],1377'SortIndex' => order_by1378)13791380matched_note_ids = []1381note_list.each do |note|1382if mode == :update1383begin1384update_opts = {id: note.id}1385unless types.nil?1386note.ntype = types.first1387update_opts[:ntype] = types.first1388end13891390unless data.nil?1391note.data = data1392update_opts[:data] = data1393end13941395framework.db.update_note(update_opts)1396rescue => e1397elog "There was an error updating note with ID #{note.id}: #{e.message}"1398next1399end1400end14011402matched_note_ids << note.id14031404row = []1405row << note.created_at14061407if note.host1408host = note.host1409row << host.address1410if set_rhosts1411addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address)1412rhosts << addr1413end1414else1415row << ''1416end14171418if note.service1419row << note.service.name || ''1420row << note.service.port || ''1421row << note.service.proto || ''1422else1423row << '' # For the Service field1424row << '' # For the Port field1425row << '' # For the Protocol field1426end14271428row << note.ntype1429row << note.data.inspect1430table << row1431end14321433if mode == :delete1434result = framework.db.delete_note(ids: matched_note_ids)1435delete_count = result.size1436end14371438if output_file1439save_csv_notes(output_file, table)1440else1441print_line1442print_line(table.to_s)1443end14441445# Finally, handle the case where the user wants the resulting list1446# of hosts to go into RHOSTS.1447set_rhosts_from_addrs(rhosts.uniq) if set_rhosts14481449print_status("Deleted #{delete_count} notes") if delete_count > 01450}1451end14521453def save_csv_notes(fpath, table)1454begin1455File.open(fpath, 'wb') do |f|1456f.write(table.to_csv)1457end1458print_status("Wrote notes to #{fpath}")1459rescue Errno::EACCES => e1460print_error("Unable to save notes. #{e.message}")1461end1462end14631464#1465# Tab completion for the loot command1466#1467# @param str [String] the string currently being typed before tab was hit1468# @param words [Array<String>] the previously completed words on the command line. words is always1469# at least 1 when tab completion has reached this stage since the command itself has been completed1470def cmd_loot_tabs(str, words)1471if words.length == 11472@@loot_opts.option_keys.select { |opt| opt.start_with?(str) }1473end1474end14751476def cmd_loot_help1477print_line "Usage: loot [options]"1478print_line " Info: loot [-h] [addr1 addr2 ...] [-t <type1,type2>]"1479print_line " Add: loot -f [fname] -i [info] -a [addr1 addr2 ...] -t [type]"1480print_line " Del: loot -d [addr1 addr2 ...]"1481print_line1482print @@loot_opts.usage1483print_line1484end14851486@@loot_opts = Rex::Parser::Arguments.new(1487[ '-a', '--add' ] => [ false, 'Add loot to the list of addresses, instead of listing.' ],1488[ '-d', '--delete' ] => [ false, 'Delete *all* loot matching host and type.' ],1489[ '-f', '--file' ] => [ true, 'File with contents of the loot to add.', '<filename>' ],1490[ '-i', '--info' ] => [ true, 'Info of the loot to add.', '<info>' ],1491[ '-t', '--type' ] => [ true, 'Search for a list of types.', '<type1,type2>' ],1492[ '-h', '--help' ] => [ false, 'Show this help information.' ],1493[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],1494[ '-u', '--update' ] => [ false, 'Update loot. Not officially supported.' ]1495)14961497def cmd_loot(*args)1498return unless active?14991500mode = :search1501host_ranges = []1502types = nil1503delete_count = 01504search_term = nil1505file = nil1506name = nil1507info = nil1508filename = nil15091510@@loot_opts.parse(args) do |opt, idx, val|1511case opt1512when '-a', '--add'1513mode = :add1514when '-d', '--delete'1515mode = :delete1516when '-f', '--file'1517filename = val1518if(!filename)1519print_error("Can't make loot with no filename")1520return1521end1522if (!File.exist?(filename) or !File.readable?(filename))1523print_error("Can't read file")1524return1525end1526when '-i', '--info'1527info = val1528if(!info)1529print_error("Can't make loot with no info")1530return1531end1532when '-t', '--type'1533typelist = val1534if(!typelist)1535print_error("Invalid type list")1536return1537end1538types = typelist.strip().split(",")1539when '-S', '--search'1540search_term = val1541when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported.1542mode = :update1543when '-h', '--help'1544cmd_loot_help1545return1546else1547# Anything that wasn't an option is a host to search for1548unless (arg_host_range(val, host_ranges))1549return1550end1551end1552end15531554tbl = Rex::Text::Table.new({1555'Header' => "Loot",1556'Columns' => [ 'host', 'service', 'type', 'name', 'content', 'info', 'path' ],1557# For now, don't perform any word wrapping on the loot table as it breaks the workflow of1558# copying paths and pasting them into applications1559'WordWrap' => false,1560})15611562# Sentinel value meaning all1563host_ranges.push(nil) if host_ranges.empty?15641565if mode == :add1566if host_ranges.compact.empty?1567print_error('Address list required')1568return1569end1570if info.nil?1571print_error("Info required")1572return1573end1574if filename.nil?1575print_error("Loot file required")1576return1577end1578if types.nil? or types.size != 11579print_error("Exactly one loot type is required")1580return1581end1582type = types.first1583name = File.basename(filename)1584file = File.open(filename, "rb")1585contents = file.read1586host_ranges.each do |range|1587range.each do |host|1588lootfile = framework.db.find_or_create_loot(:type => type, :host => host, :info => info, :data => contents, :path => filename, :name => name)1589print_status("Added loot for #{host} (#{lootfile})")1590end1591end1592return1593end15941595matched_loot_ids = []1596loots = []1597if host_ranges.compact.empty?1598loots = loots + framework.db.loots(workspace: framework.db.workspace, search_term: search_term)1599else1600each_host_range_chunk(host_ranges) do |host_search|1601next if host_search && host_search.empty?16021603loots = loots + framework.db.loots(workspace: framework.db.workspace, hosts: { address: host_search }, search_term: search_term)1604end1605end16061607loots.each do |loot|1608row = []1609# TODO: This is just a temp implementation of update for the time being since it did not exist before.1610# It should be updated to not pass all of the attributes attached to the object, only the ones being updated.1611if mode == :update1612begin1613loot.info = info if info1614if types && types.size > 11615print_error "May only pass 1 type when performing an update."1616next1617end1618loot.ltype = types.first if types1619framework.db.update_loot(loot.as_json.symbolize_keys)1620rescue => e1621elog "There was an error updating loot with ID #{loot.id}: #{e.message}"1622next1623end1624end1625row.push (loot.host && loot.host.address) ? loot.host.address : ""1626if (loot.service)1627svc = (loot.service.name ? loot.service.name : "#{loot.service.port}/#{loot.service.proto}")1628row.push svc1629else1630row.push ""1631end1632row.push(loot.ltype)1633row.push(loot.name || "")1634row.push(loot.content_type)1635row.push(loot.info || "")1636row.push(loot.path)16371638tbl << row1639matched_loot_ids << loot.id1640end16411642if (mode == :delete)1643result = framework.db.delete_loot(ids: matched_loot_ids)1644delete_count = result.size1645end16461647print_line1648print_line(tbl.to_s)1649print_status("Deleted #{delete_count} loots") if delete_count > 01650end16511652# :category: Deprecated Commands1653def cmd_db_hosts_help; deprecated_help(:hosts); end1654# :category: Deprecated Commands1655def cmd_db_notes_help; deprecated_help(:notes); end1656# :category: Deprecated Commands1657def cmd_db_vulns_help; deprecated_help(:vulns); end1658# :category: Deprecated Commands1659def cmd_db_services_help; deprecated_help(:services); end1660# :category: Deprecated Commands1661def cmd_db_autopwn_help; deprecated_help; end1662# :category: Deprecated Commands1663def cmd_db_driver_help; deprecated_help; end16641665# :category: Deprecated Commands1666def cmd_db_hosts(*args); deprecated_cmd(:hosts, *args); end1667# :category: Deprecated Commands1668def cmd_db_notes(*args); deprecated_cmd(:notes, *args); end1669# :category: Deprecated Commands1670def cmd_db_vulns(*args); deprecated_cmd(:vulns, *args); end1671# :category: Deprecated Commands1672def cmd_db_services(*args); deprecated_cmd(:services, *args); end1673# :category: Deprecated Commands1674def cmd_db_autopwn(*args); deprecated_cmd; end16751676#1677# :category: Deprecated Commands1678#1679# This one deserves a little more explanation than standard deprecation1680# warning, so give the user a better understanding of what's going on.1681#1682def cmd_db_driver(*args)1683deprecated_cmd1684print_line1685print_line "Because Metasploit no longer supports databases other than the default"1686print_line "PostgreSQL, there is no longer a need to set the driver. Thus db_driver"1687print_line "is not useful and its functionality has been removed. Usually Metasploit"1688print_line "will already have connected to the database; check db_status to see."1689print_line1690cmd_db_status1691end16921693def cmd_db_import_tabs(str, words)1694tab_complete_filenames(str, words)1695end16961697def cmd_db_import_help1698print_line "Usage: db_import <filename> [file2...]"1699print_line1700print_line "Filenames can be globs like *.xml, or **/*.xml which will search recursively"1701print_line "Currently supported file types include:"1702print_line " Acunetix"1703print_line " Amap Log"1704print_line " Amap Log -m"1705print_line " Appscan"1706print_line " Burp Session XML"1707print_line " Burp Issue XML"1708print_line " CI"1709print_line " Foundstone"1710print_line " FusionVM XML"1711print_line " Group Policy Preferences Credentials"1712print_line " IP Address List"1713print_line " IP360 ASPL"1714print_line " IP360 XML v3"1715print_line " Libpcap Packet Capture"1716print_line " Masscan XML"1717print_line " Metasploit PWDump Export"1718print_line " Metasploit XML"1719print_line " Metasploit Zip Export"1720print_line " Microsoft Baseline Security Analyzer"1721print_line " NeXpose Simple XML"1722print_line " NeXpose XML Report"1723print_line " Nessus NBE Report"1724print_line " Nessus XML (v1)"1725print_line " Nessus XML (v2)"1726print_line " NetSparker XML"1727print_line " Nikto XML"1728print_line " Nmap XML"1729print_line " OpenVAS Report"1730print_line " OpenVAS XML (optional arguments -cert -dfn)"1731print_line " Outpost24 XML"1732print_line " Qualys Asset XML"1733print_line " Qualys Scan XML"1734print_line " Retina XML"1735print_line " Spiceworks CSV Export"1736print_line " Wapiti XML"1737print_line1738end17391740#1741# Generic import that automatically detects the file type1742#1743def cmd_db_import(*args)1744return unless active?1745openvas_cert = false1746openvas_dfn = false1747::ApplicationRecord.connection_pool.with_connection {1748if args.include?("-h") || ! (args && args.length > 0)1749cmd_db_import_help1750return1751end1752if args.include?("-dfn")1753openvas_dfn = true1754end1755if args.include?("-cert")1756openvas_cert = true1757end1758options = {:openvas_dfn => openvas_dfn, :openvas_cert => openvas_cert}1759args.each { |glob|1760next if (glob.include?("-cert") || glob.include?("-dfn"))1761files = ::Dir.glob(::File.expand_path(glob))1762if files.empty?1763print_error("No such file #{glob}")1764next1765end1766files.each { |filename|1767if (not ::File.readable?(filename))1768print_error("Could not read file #{filename}")1769next1770end1771begin1772warnings = 01773framework.db.import_file(:filename => filename, :options => options) do |type,data|1774case type1775when :debug1776print_error("DEBUG: #{data.inspect}")1777when :vuln1778inst = data[1] == 1 ? "instance" : "instances"1779print_status("Importing vulnerability '#{data[0]}' (#{data[1]} #{inst})")1780when :filetype1781print_status("Importing '#{data}' data")1782when :parser1783print_status("Import: Parsing with '#{data}'")1784when :address1785print_status("Importing host #{data}")1786when :service1787print_status("Importing service #{data}")1788when :msf_loot1789print_status("Importing loot #{data}")1790when :msf_task1791print_status("Importing task #{data}")1792when :msf_report1793print_status("Importing report #{data}")1794when :pcap_count1795print_status("Import: #{data} packets processed")1796when :record_count1797print_status("Import: #{data[1]} records processed")1798when :warning1799print_error1800data.split("\n").each do |line|1801print_error(line)1802end1803print_error1804warnings += 11805end1806end1807print_status("Successfully imported #{filename}")18081809print_error("Please note that there were #{warnings} warnings") if warnings > 11810print_error("Please note that there was one warning") if warnings == 118111812rescue Msf::DBImportError => e1813print_error("Failed to import #{filename}: #{$!}")1814elog("Failed to import #{filename}", error: e)1815dlog("Call stack: #{$@.join("\n")}", LEV_3)1816next1817rescue REXML::ParseException => e1818print_error("Failed to import #{filename} due to malformed XML:")1819print_error("#{e.class}: #{e}")1820elog("Failed to import #{filename}", error: e)1821dlog("Call stack: #{$@.join("\n")}", LEV_3)1822next1823end1824}1825}1826}1827end18281829def cmd_db_export_help1830# Like db_hosts and db_services, this creates a list of columns, so1831# use its -h1832cmd_db_export("-h")1833end18341835#1836# Export an XML1837#1838def cmd_db_export(*args)1839return unless active?1840::ApplicationRecord.connection_pool.with_connection {18411842export_formats = %W{xml pwdump}1843format = 'xml'1844output = nil18451846while (arg = args.shift)1847case arg1848when '-h','--help'1849print_line "Usage:"1850print_line " db_export -f <format> [filename]"1851print_line " Format can be one of: #{export_formats.join(", ")}"1852when '-f','--format'1853format = args.shift.to_s.downcase1854else1855output = arg1856end1857end18581859if not output1860print_error("No output file was specified")1861return1862end18631864if not export_formats.include?(format)1865print_error("Unsupported file format: #{format}")1866print_error("Unsupported file format: '#{format}'. Must be one of: #{export_formats.join(", ")}")1867return1868end18691870print_status("Starting export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...")1871framework.db.run_db_export(output, format)1872print_status("Finished export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...")1873}1874end18751876def find_nmap_path1877Rex::FileUtils.find_full_path("nmap") || Rex::FileUtils.find_full_path("nmap.exe")1878end18791880#1881# Import Nmap data from a file1882#1883def cmd_db_nmap(*args)1884return unless active?1885::ApplicationRecord.connection_pool.with_connection {1886if (args.length == 0)1887print_status("Usage: db_nmap [--save | [--help | -h]] [nmap options]")1888return1889end18901891save = false1892arguments = []1893while (arg = args.shift)1894case arg1895when '--save'1896save = true1897when '--help', '-h'1898cmd_db_nmap_help1899return1900else1901arguments << arg1902end1903end19041905nmap = find_nmap_path1906unless nmap1907print_error("The nmap executable could not be found")1908return1909end19101911fd = Rex::Quickfile.new(['msf-db-nmap-', '.xml'], Msf::Config.local_directory)19121913begin1914# When executing native Nmap in Cygwin, expand the Cygwin path to a Win32 path1915if(Rex::Compat.is_cygwin and nmap =~ /cygdrive/)1916# Custom function needed because cygpath breaks on 8.3 dirs1917tout = Rex::Compat.cygwin_to_win32(fd.path)1918arguments.push('-oX', tout)1919else1920arguments.push('-oX', fd.path)1921end19221923run_nmap(nmap, arguments)19241925framework.db.import_nmap_xml_file(:filename => fd.path)19261927print_status("Saved NMAP XML results to #{fd.path}") if save1928ensure1929fd.close1930fd.unlink unless save1931end1932}1933end19341935def cmd_db_nmap_help1936nmap = find_nmap_path1937unless nmap1938print_error("The nmap executable could not be found")1939return1940end19411942stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help')19431944stdout.each_line do |out_line|1945next if out_line.strip.empty?1946print_status(out_line.strip)1947end19481949stderr.each_line do |err_line|1950next if err_line.strip.empty?1951print_error(err_line.strip)1952end1953end19541955def cmd_db_nmap_tabs(str, words)1956nmap = find_nmap_path1957unless nmap1958return1959end19601961stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help')1962tabs = []1963stdout.each_line do |out_line|1964if out_line.strip.starts_with?('-')1965tabs.push(out_line.strip.split(':').first)1966end1967end19681969stderr.each_line do |err_line|1970next if err_line.strip.empty?1971print_error(err_line.strip)1972end19731974return tabs1975end19761977#1978# Database management1979#1980def db_check_driver1981unless framework.db.driver1982print_error("No database driver installed.")1983return false1984end1985true1986end19871988#1989# Is everything working?1990#1991def cmd_db_status(*args)1992return if not db_check_driver19931994if framework.db.connection_established?1995print_connection_info1996else1997print_status("#{framework.db.driver} selected, no connection")1998end1999end200020012002def cmd_db_connect_help2003print_line(" USAGE:")2004print_line(" * Postgres Data Service:")2005print_line(" db_connect <user:[pass]>@<host:[port]>/<database>")2006print_line(" Examples:")2007print_line(" db_connect user@metasploit3")2008print_line(" db_connect user:[email protected]/metasploit3")2009print_line(" db_connect user:[email protected]:1500/metasploit3")2010print_line(" db_connect -y [path/to/database.yml]")2011print_line(" ")2012print_line(" * HTTP Data Service:")2013print_line(" db_connect [options] <http|https>://<host:[port]>")2014print_line(" Examples:")2015print_line(" db_connect http://localhost:8080")2016print_line(" db_connect http://my-super-msf-data.service.com")2017print_line(" db_connect -c ~/cert.pem -t 6a7a74c1a5003802c955ead1bbddd4ab1b05a7f2940b4732d34bfc555bc6e1c5d7611a497b29e8f0 https://localhost:8080")2018print_line(" NOTE: You must be connected to a Postgres data service in order to successfully connect to a HTTP data service.")2019print_line(" ")2020print_line(" Persisting Connections:")2021print_line(" db_connect --name <name to save connection as> [options] <address>")2022print_line(" Examples:")2023print_line(" Saving: db_connect --name LA-server http://123.123.123.45:1234")2024print_line(" Connecting: db_connect LA-server")2025print_line(" ")2026print_line(" OPTIONS:")2027print_line(" -l,--list-services List the available data services that have been previously saved.")2028print_line(" -y,--yaml Connect to the data service specified in the provided database.yml file.")2029print_line(" -n,--name Name used to store the connection. Providing an existing name will overwrite the settings for that connection.")2030print_line(" -c,--cert Certificate file matching the remote data server's certificate. Needed when using self-signed SSL cert.")2031print_line(" -t,--token The API token used to authenticate to the remote data service.")2032print_line(" --skip-verify Skip validating authenticity of server's certificate (NOT RECOMMENDED).")2033print_line("")2034end20352036def cmd_db_connect(*args)2037return if not db_check_driver20382039opts = {}2040while (arg = args.shift)2041case arg2042when '-h', '--help'2043cmd_db_connect_help2044return2045when '-y', '--yaml'2046opts[:yaml_file] = args.shift2047when '-c', '--cert'2048opts[:cert] = args.shift2049when '-t', '--token'2050opts[:api_token] = args.shift2051when '-l', '--list-services'2052list_saved_data_services2053return2054when '-n', '--name'2055opts[:name] = args.shift2056if opts[:name] =~ /\/|\[|\]/2057print_error "Provided name contains an invalid character. Aborting connection."2058return2059end2060when '--skip-verify'2061opts[:skip_verify] = true2062else2063found_name = ::Msf::DbConnector.data_service_search(name: arg)2064if found_name2065opts = ::Msf::DbConnector.load_db_config(found_name)2066else2067opts[:url] = arg2068end2069end2070end20712072if !opts[:url] && !opts[:yaml_file]2073print_error 'A URL or saved data service name is required.'2074print_line2075cmd_db_connect_help2076return2077end20782079if opts[:url] =~ /http/2080new_conn_type = 'http'2081else2082new_conn_type = framework.db.driver2083end20842085# Currently only able to be connected to one DB at a time2086if framework.db.connection_established?2087# But the http connection still requires a local database to support AR, so we have to allow that2088# Don't allow more than one HTTP service, though2089if new_conn_type != 'http' || framework.db.get_services_metadata.count >= 22090print_error('Connection already established. Only one connection is allowed at a time.')2091print_error('Run db_disconnect first if you wish to connect to a different data service.')2092print_line2093print_line 'Current connection information:'2094print_connection_info2095return2096end2097end20982099result = Msf::DbConnector.db_connect(framework, opts)2100if result[:error]2101print_error result[:error]2102return2103end21042105if result[:result]2106print_status result[:result]2107end2108if framework.db.active2109name = opts[:name]2110if !name || name.empty?2111if found_name2112name = found_name2113elsif result[:data_service_name]2114name = result[:data_service_name]2115else2116name = Rex::Text.rand_text_alphanumeric(8)2117end2118end21192120save_db_to_config(framework.db, name)2121@current_data_service = name2122end2123end21242125def cmd_db_disconnect_help2126print_line "Usage:"2127print_line " db_disconnect Temporarily disconnects from the currently configured dataservice."2128print_line " db_disconnect --clear Clears the default dataservice that msfconsole will use when opened."2129print_line2130end21312132def cmd_db_disconnect(*args)2133return if not db_check_driver21342135if args[0] == '-h' || args[0] == '--help'2136cmd_db_disconnect_help2137return2138elsif args[0] == '-c' || args[0] == '--clear'2139clear_default_db2140return2141end21422143previous_name = framework.db.name2144result = Msf::DbConnector.db_disconnect(framework)21452146if result[:error]2147print_error "Unable to disconnect from the data service: #{@current_data_service}"2148print_error result[:error]2149elsif result[:old_data_service_name].nil?2150print_error 'Not currently connected to a data service.'2151else2152print_line "Successfully disconnected from the data service: #{previous_name}."2153@current_data_service = result[:data_service_name]2154if @current_data_service2155print_line "Now connected to: #{@current_data_service}."2156end2157end2158end21592160def cmd_db_rebuild_cache(*args)2161print_line "This command is deprecated with Metasploit 5"2162end21632164def cmd_db_save_help2165print_line "Usage: db_save"2166print_line2167print_line "Save the current data service connection as the default to reconnect on startup."2168print_line2169end21702171def cmd_db_save(*args)2172while (arg = args.shift)2173case arg2174when '-h', '--help'2175cmd_db_save_help2176return2177end2178end21792180if !framework.db.active || !@current_data_service2181print_error "Not currently connected to a data service that can be saved."2182return2183end21842185begin2186Msf::Config.save(DB_CONFIG_PATH => { 'default_db' => @current_data_service })2187print_line "Successfully saved data service as default: #{@current_data_service}"2188rescue ArgumentError => e2189print_error e.message2190end2191end21922193def save_db_to_config(database, database_name)2194if database_name =~ /\/|\[|\]/2195raise ArgumentError, 'Data service name contains an invalid character.'2196end2197config_path = "#{DB_CONFIG_PATH}/#{database_name}"2198config_opts = {}2199if !database.is_local?2200begin2201config_opts['url'] = database.endpoint2202if database.https_opts2203config_opts['cert'] = database.https_opts[:cert] if database.https_opts[:cert]2204config_opts['skip_verify'] = true if database.https_opts[:skip_verify]2205end2206if database.api_token2207config_opts['api_token'] = database.api_token2208end2209Msf::Config.save(config_path => config_opts)2210rescue => e2211print_error "There was an error saving the data service configuration: #{e.message}"2212end2213else2214url = Msf::DbConnector.build_postgres_url2215config_opts['url'] = url2216Msf::Config.save(config_path => config_opts)2217end2218end22192220def cmd_db_remove_help2221print_line "Usage: db_remove <name>"2222print_line2223print_line "Delete the specified saved data service."2224print_line2225end22262227def cmd_db_remove(*args)2228if args[0] == '-h' || args[0] == '--help' || args[0].nil? || args[0].empty?2229cmd_db_remove_help2230return2231end2232delete_db_from_config(args[0])2233end22342235def delete_db_from_config(db_name)2236conf = Msf::Config.load2237db_path = "#{DB_CONFIG_PATH}/#{db_name}"2238if conf[db_path]2239clear_default_db if conf[DB_CONFIG_PATH]['default_db'] && conf[DB_CONFIG_PATH]['default_db'] == db_name2240Msf::Config.delete_group(db_path)2241print_line "Successfully deleted data service: #{db_name}"2242else2243print_line "Unable to locate saved data service with name #{db_name}."2244end2245end22462247def clear_default_db2248conf = Msf::Config.load2249if conf[DB_CONFIG_PATH] && conf[DB_CONFIG_PATH]['default_db']2250updated_opts = conf[DB_CONFIG_PATH]2251updated_opts.delete('default_db')2252Msf::Config.save(DB_CONFIG_PATH => updated_opts)2253print_line "Cleared the default data service."2254else2255print_line "No default data service was configured."2256end2257end22582259def db_find_tools(tools)2260missed = []2261tools.each do |name|2262if(! Rex::FileUtils.find_full_path(name))2263missed << name2264end2265end2266if(not missed.empty?)2267print_error("This database command requires the following tools to be installed: #{missed.join(", ")}")2268return2269end2270true2271end22722273#######2274private22752276def run_nmap(nmap, arguments, use_sudo: false)2277print_warning('Running Nmap with sudo') if use_sudo2278begin2279nmap_pipe = use_sudo ? ::Open3::popen3('sudo', nmap, *arguments) : ::Open3::popen3(nmap, *arguments)2280temp_nmap_threads = []2281temp_nmap_threads << framework.threads.spawn("db_nmap-Stdout", false, nmap_pipe[1]) do |np_1|2282np_1.each_line do |nmap_out|2283next if nmap_out.strip.empty?2284print_status("Nmap: #{nmap_out.strip}")2285end2286end22872288temp_nmap_threads << framework.threads.spawn("db_nmap-Stderr", false, nmap_pipe[2]) do |np_2|22892290np_2.each_line do |nmap_err|2291next if nmap_err.strip.empty?2292print_status("Nmap: '#{nmap_err.strip}'")2293# Check if the stderr text includes 'root', this only happens if the scan requires root privileges2294if nmap_err =~ /requires? root privileges/ or2295nmap_err.include? 'only works if you are root' or nmap_err =~ /requires? raw socket access/2296return run_nmap(nmap, arguments, use_sudo: true) unless use_sudo2297end2298end2299end23002301temp_nmap_threads.map { |t| t.join rescue nil }2302nmap_pipe.each { |p| p.close rescue nil }2303rescue ::IOError2304end2305end23062307#######23082309def print_connection_info2310cdb = ''2311if framework.db.driver == 'http'2312cdb = framework.db.name2313else2314::ApplicationRecord.connection_pool.with_connection do |conn|2315if conn.respond_to?(:current_database)2316cdb = conn.current_database2317end2318end2319end2320output = "Connected to #{cdb}. Connection type: #{framework.db.driver}."2321output += " Connection name: #{@current_data_service}." if @current_data_service2322print_status(output)2323end23242325def list_saved_data_services2326conf = Msf::Config.load2327default = nil2328tbl = Rex::Text::Table.new({2329'Header' => 'Data Services',2330'Columns' => ['current', 'name', 'url', 'default?'],2331'SortIndex' => 12332})23332334conf.each_pair do |k,v|2335if k =~ /#{DB_CONFIG_PATH}/2336default = v['default_db'] if v['default_db']2337name = k.split('/').last2338next if name == 'database' # Data service information is not stored in 'framework/database', just metadata2339url = v['url']2340current = ''2341current = '*' if name == @current_data_service2342default_output = ''2343default_output = '*' if name == default2344line = [current, name, url, default_output]2345tbl << line2346end2347end2348print_line2349print_line tbl.to_s2350end23512352def print_msgs(status_msg, error_msg)2353status_msg.each do |s|2354print_status(s)2355end23562357error_msg.each do |e|2358print_error(e)2359end2360end23612362def _format_vulns_and_vuln_attempts(vulns)2363vulns.map.with_index do |vuln, index|2364vuln_formatted = <<~EOF.strip.indent(2)2365#{index}. Vuln ID: #{vuln.id}2366Timestamp: #{vuln.created_at}2367Host: #{vuln.host.address}2368Name: #{vuln.name}2369References: #{vuln.refs.map {|r| r.name}.join(',')}2370Information: #{_format_vuln_value(vuln.info)}2371EOF23722373vuln_attempts_formatted = vuln.vuln_attempts.map.with_index do |vuln_attempt, i|2374<<~EOF.strip.indent(5)2375#{i}. ID: #{vuln_attempt.id}2376Vuln ID: #{vuln_attempt.vuln_id}2377Timestamp: #{vuln_attempt.attempted_at}2378Exploit: #{vuln_attempt.exploited}2379Fail reason: #{_format_vuln_value(vuln_attempt.fail_reason)}2380Username: #{vuln_attempt.username}2381Module: #{vuln_attempt.module}2382Session ID: #{_format_vuln_value(vuln_attempt.session_id)}2383Loot ID: #{_format_vuln_value(vuln_attempt.loot_id)}2384Fail Detail: #{_format_vuln_value(vuln_attempt.fail_detail)}2385EOF2386end23872388{ :vuln => vuln_formatted, :vuln_attempts => vuln_attempts_formatted }2389end2390end23912392def _print_vulns_and_attempts(vulns_and_attempts)2393print_line("Vulnerabilities\n===============")2394vulns_and_attempts.each do |vuln_and_attempt|2395print_line(vuln_and_attempt[:vuln])2396print_line("Vuln attempts:".indent(5))2397vuln_and_attempt[:vuln_attempts].each do |attempt|2398print_line(attempt)2399end2400end2401end24022403def _format_vuln_value(s)2404s.blank? ? s.inspect : s.to_s2405end2406end24072408end end end end240924102411