Path: blob/master/lib/msf/ui/console/command_dispatcher/db.rb
32575 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']848extra_columns = ['resource', 'parents']849850names = nil851order_by = nil852proto = nil853host_ranges = []854port_ranges = []855rhosts = []856delete_count = 0857search_term = nil858opts = {}859860@@services_opts.parse(args) do |opt, idx, val|861case opt862when '-a', '--add'863mode = :add864when '-d', '--delete'865mode = :delete866when '-U', '--update'867mode = :update868when '-u', '--up'869onlyup = true870when '-c'871list = val872if(!list)873print_error("Invalid column list")874return875end876col_search = list.strip().split(",")877col_search.each { |c|878if not @@services_columns.include? c879print_error("Invalid column list. Possible values are (#{@@services_columns.join("|")})")880return881end882}883when '-p'884unless (arg_port_range(val, port_ranges, true))885return886end887when '-r'888proto = val889if (!proto)890print_status("Invalid protocol")891return892end893proto = proto.strip894when '-s'895namelist = val896if (!namelist)897print_error("Invalid name list")898return899end900names = namelist.strip().split(",")901when '-o'902output_file = val903if (!output_file)904print_error("Invalid output filename")905return906end907output_file = ::File.expand_path(output_file)908when '-O'909if (order_by = val.to_i - 1) < 0910print_error('Please specify a column number starting from 1')911return912end913when '-R', '--rhosts'914set_rhosts = true915when '-S', '--search'916search_term = val917opts[:search_term] = search_term918when '-h', '--help'919cmd_services_help920return921else922# Anything that wasn't an option is a host to search for923unless (arg_host_range(val, host_ranges))924return925end926end927end928929ports = port_ranges.flatten.uniq930931if mode == :add932# Can only deal with one port and one service name at a time933# right now. Them's the breaks.934if ports.length != 1935print_error("Exactly one port required")936return937end938if host_ranges.empty?939print_error("Host address or range required")940return941end942host_ranges.each do |range|943range.each do |addr|944info = {945:host => addr,946:port => ports.first.to_i947}948info[:proto] = proto.downcase if proto949info[:name] = names.first.downcase if names and names.first950951svc = framework.db.find_or_create_service(info)952print_status("Time: #{svc.created_at} Service: host=#{svc.host.address} port=#{svc.port} proto=#{svc.proto} name=#{svc.name}")953end954end955return956end957958# If we got here, we're searching. Delete implies search959col_names = @@services_columns960if col_search961col_names = col_search962end963tbl = Rex::Text::Table.new({964'Header' => "Services",965'Columns' => extra_columns.empty? ? (['host'] + col_names) : (['host'] + col_names + extra_columns),966'SortIndex' => order_by967})968969# Sentinel value meaning all970host_ranges.push(nil) if host_ranges.empty?971ports = nil if ports.empty?972matched_service_ids = []973974each_host_range_chunk(host_ranges) do |host_search|975next if host_search && host_search.empty?976opts[:workspace] = framework.db.workspace977opts[:hosts] = {address: host_search} if !host_search.nil?978opts[:port] = ports if ports979opts[:name] = names if names980framework.db.services(opts).each do |service|981982unless service.state == 'open'983next if onlyup984end985986host = service.host987matched_service_ids << service.id988989if mode == :update990service.name = names.first if names991service.proto = proto if proto992service.port = ports.first if ports993framework.db.update_service(service.as_json.symbolize_keys)994end995996columns = [host.address] + col_names.map { |n| service[n].to_s || "" }997unless extra_columns.empty?998columns << service.resource.to_json999columns << service.parents.map { |parent| "#{parent.name} (#{parent.port}/#{parent.proto})"}.join(', ')1000end1001tbl << columns1002if set_rhosts1003addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address )1004rhosts << addr1005end1006end1007end10081009if (mode == :delete)1010result = framework.db.delete_service(ids: matched_service_ids)1011delete_count += result.size1012end10131014if (output_file == nil)1015print_line(tbl.to_s)1016else1017# create the output file1018::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) }1019print_status("Wrote services to #{output_file}")1020end10211022# Finally, handle the case where the user wants the resulting list1023# of hosts to go into RHOSTS.1024set_rhosts_from_addrs(rhosts.uniq) if set_rhosts10251026print_status("Deleted #{delete_count} services") if delete_count > 010271028end10291030#1031# Tab completion for the vulns command1032#1033# @param str [String] the string currently being typed before tab was hit1034# @param words [Array<String>] the previously completed words on the command line. words is always1035# at least 1 when tab completion has reached this stage since the command itself has been completed1036def cmd_vulns_tabs(str, words)1037if words.length == 11038return @@vulns_opts.option_keys.select { |opt| opt.start_with?(str) }1039end1040case words[-1]1041when '-o', '--output'1042return tab_complete_filenames(str, words)1043end1044end10451046def cmd_vulns_help1047print_line "Print all vulnerabilities in the database"1048print_line1049print_line "Usage: vulns [addr range]"1050print_line1051print @@vulns_opts.usage1052print_line1053print_line "Examples:"1054print_line " vulns -p 1-65536 # only vulns with associated services"1055print_line " vulns -p 1-65536 -s http # identified as http on any port"1056print_line1057end10581059@@vulns_opts = Rex::Parser::Arguments.new(1060[ '-h', '--help' ] => [ false, 'Show this help information.' ],1061[ '-o', '--output' ] => [ true, 'Send output to a file in csv format.', '<filename>' ],1062[ '-p', '--port' ] => [ true, 'List vulns matching this port spec.', '<port>' ],1063[ '-s', '--service' ] => [ true, 'List vulns matching these service names.', '<name>' ],1064[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],1065[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],1066[ '-i', '--info' ] => [ false, 'Display vuln information.' ],1067[ '-d', '--delete' ] => [ false, 'Delete vulnerabilities. Not officially supported.' ],1068[ '-v', '--verbose' ] => [ false, 'Display additional information.' ]1069)10701071def cmd_vulns(*args)1072return unless active?10731074default_columns = ['Timestamp', 'Host', 'Service', 'Resource', 'Name', 'References']1075host_ranges = []1076port_ranges = []1077svcs = []1078rhosts = []10791080search_term = nil1081show_info = false1082show_vuln_attempts = false1083set_rhosts = false1084output_file = nil1085delete_count = 010861087mode = nil10881089@@vulns_opts.parse(args) do |opt, idx, val|1090case opt1091when '-d', '--delete' # TODO: This is currently undocumented because it's not officially supported.1092mode = :delete1093when '-h', '--help'1094cmd_vulns_help1095return1096when '-o', '--output'1097output_file = val1098if output_file1099output_file = File.expand_path(output_file)1100else1101print_error("Invalid output filename")1102return1103end1104when '-p', '--port'1105unless (arg_port_range(val, port_ranges, true))1106return1107end1108when '-s', '--service'1109service = val1110if (!service)1111print_error("Argument required for -s")1112return1113end1114svcs = service.split(/[\s]*,[\s]*/)1115when '-R', '--rhosts'1116set_rhosts = true1117when '-S', '--search'1118search_term = val1119when '-i', '--info'1120show_info = true1121when '-v', '--verbose'1122show_vuln_attempts = true1123else1124# Anything that wasn't an option is a host to search for1125unless (arg_host_range(val, host_ranges))1126return1127end1128end1129end11301131if show_info1132default_columns << 'Information'1133end11341135# add sentinel value meaning all if empty1136host_ranges.push(nil) if host_ranges.empty?1137# normalize1138ports = port_ranges.flatten.uniq1139svcs.flatten!1140tbl = Rex::Text::Table.new(1141'Header' => 'Vulnerabilities',1142'Columns' => default_columns1143)11441145matched_vuln_ids = []1146vulns = []1147if host_ranges.compact.empty?1148vulns = framework.db.vulns({:search_term => search_term})1149else1150each_host_range_chunk(host_ranges) do |host_search|1151next if host_search && host_search.empty?11521153vulns.concat(framework.db.vulns({:hosts => { :address => host_search }, :search_term => search_term }))1154end1155end11561157vulns.each do |vuln|1158reflist = vuln.refs.map {|r| r.name}1159if (vuln.service)1160# Skip this one if the user specified a port and it1161# doesn't match.1162next unless ports.empty? or ports.include? vuln.service.port1163# Same for service names1164next unless svcs.empty? or svcs.include?(vuln.service.name)1165else1166# This vuln has no service, so it can't match1167next unless ports.empty? and svcs.empty?1168end11691170matched_vuln_ids << vuln.id11711172row = []1173row << vuln.created_at1174row << vuln.host.address1175row << (vuln.service.present? ? "#{vuln.service.name} (#{vuln.service.port}/#{vuln.service.proto})" : 'None')1176row << vuln.resource.to_s1177row << vuln.name1178row << reflist.join(',')1179if show_info1180row << vuln.info1181end1182tbl << row11831184if set_rhosts1185addr = (vuln.host.scope.to_s != "" ? vuln.host.address + '%' + vuln.host.scope : vuln.host.address)1186rhosts << addr1187end1188end11891190if mode == :delete1191result = framework.db.delete_vuln(ids: matched_vuln_ids)1192delete_count = result.size1193end11941195if output_file1196if show_vuln_attempts1197print_warning("Cannot output to a file when verbose mode is enabled. Please remove verbose flag and try again.")1198else1199File.write(output_file, tbl.to_csv)1200print_status("Wrote vulnerability information to #{output_file}")1201end1202else1203print_line1204if show_vuln_attempts1205vulns_and_attempts = _format_vulns_and_vuln_attempts(vulns)1206_print_vulns_and_attempts(vulns_and_attempts)1207else1208print_line(tbl.to_s)1209end1210end12111212# Finally, handle the case where the user wants the resulting list1213# of hosts to go into RHOSTS.1214set_rhosts_from_addrs(rhosts.uniq) if set_rhosts12151216print_status("Deleted #{delete_count} vulnerabilities") if delete_count > 01217end12181219#1220# Tab completion for the notes command1221#1222# @param str [String] the string currently being typed before tab was hit1223# @param words [Array<String>] the previously completed words on the command line. words is always1224# at least 1 when tab completion has reached this stage since the command itself has been completed1225def cmd_notes_tabs(str, words)1226if words.length == 11227return @@notes_opts.option_keys.select { |opt| opt.start_with?(str) }1228end12291230case words[-1]1231when '-O', '--order'1232return []1233when '-o', '--output'1234return tab_complete_filenames(str, words)1235end12361237[]1238end12391240def cmd_notes_help1241print_line "Usage: notes [-h] [-t <type1,type2>] [-n <data string>] [-a] [addr range]"1242print_line1243print @@notes_opts.usage1244print_line1245print_line "Examples:"1246print_line " notes --add -t apps -n 'winzip' 10.1.1.34 10.1.20.41"1247print_line " notes -t smb.fingerprint 10.1.1.34 10.1.20.41"1248print_line " notes -S 'nmap.nse.(http|rtsp)'"1249print_line1250end12511252@@notes_opts = Rex::Parser::Arguments.new(1253[ '-a', '--add' ] => [ false, 'Add a note to the list of addresses, instead of listing.' ],1254[ '-d', '--delete' ] => [ false, 'Delete the notes instead of searching.' ],1255[ '-h', '--help' ] => [ false, 'Show this help information.' ],1256[ '-n', '--note' ] => [ true, 'Set the data for a new note (only with -a).', '<note>' ],1257[ '-O', '--order' ] => [ true, 'Order rows by specified column number.', '<column id>' ],1258[ '-o', '--output' ] => [ true, 'Save the notes to a csv file.', '<filename>' ],1259[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],1260[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],1261[ '-t', '--type' ] => [ true, 'Search for a list of types, or set single type for add.', '<type1,type2>' ],1262[ '-u', '--update' ] => [ false, 'Update a note. Not officially supported.' ]1263)12641265def cmd_notes(*args)1266return unless active?1267::ApplicationRecord.connection_pool.with_connection {1268mode = :search1269data = nil1270types = nil1271set_rhosts = false12721273host_ranges = []1274rhosts = []1275search_term = nil1276output_file = nil1277delete_count = 01278order_by = nil12791280@@notes_opts.parse(args) do |opt, idx, val|1281case opt1282when '-a', '--add'1283mode = :add1284when '-d', '--delete'1285mode = :delete1286when '-n', '--note'1287data = val1288if(!data)1289print_error("Can't make a note with no data")1290return1291end1292when '-t', '--type'1293typelist = val1294if(!typelist)1295print_error("Invalid type list")1296return1297end1298types = typelist.strip().split(",")1299when '-R', '--rhosts'1300set_rhosts = true1301when '-S', '--search'1302search_term = val1303when '-o', '--output'1304output_file = val1305output_file = ::File.expand_path(output_file)1306when '-O'1307if (order_by = val.to_i - 1) < 01308print_error('Please specify a column number starting from 1')1309return1310end1311when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported.1312mode = :update1313when '-h', '--help'1314cmd_notes_help1315return1316else1317# Anything that wasn't an option is a host to search for1318unless (arg_host_range(val, host_ranges))1319return1320end1321end1322end13231324if mode == :add1325if host_ranges.compact.empty?1326print_error("Host address or range required")1327return1328end13291330if types.nil? || types.size != 11331print_error("Exactly one type is required")1332return1333end13341335if data.nil?1336print_error("Data required")1337return1338end13391340type = types.first1341host_ranges.each { |range|1342range.each { |addr|1343note = framework.db.find_or_create_note(host: addr, type: type, data: data)1344break if not note1345print_status("Time: #{note.created_at} Note: host=#{addr} type=#{note.ntype} data=#{note.data}")1346}1347}1348return1349end13501351if mode == :update1352if !types.nil? && types.size != 11353print_error("Exactly one type is required")1354return1355end13561357if types.nil? && data.nil?1358print_error("Update requires data or type")1359return1360end1361end13621363note_list = []1364if host_ranges.compact.empty?1365# No host specified - collect all notes1366opts = {search_term: search_term}1367opts[:ntype] = types if mode != :update && types && !types.empty?1368note_list = framework.db.notes(opts)1369else1370# Collect notes of specified hosts1371each_host_range_chunk(host_ranges) do |host_search|1372next if host_search && host_search.empty?13731374opts = {hosts: {address: host_search}, workspace: framework.db.workspace, search_term: search_term}1375opts[:ntype] = types if mode != :update && types && !types.empty?1376note_list.concat(framework.db.notes(opts))1377end1378end13791380# Now display them1381table = Rex::Text::Table.new(1382'Header' => 'Notes',1383'Indent' => 1,1384'Columns' => ['Time', 'Host', 'Service', 'Port', 'Protocol', 'Type', 'Data'],1385'SortIndex' => order_by1386)13871388matched_note_ids = []1389note_list.each do |note|1390if mode == :update1391begin1392update_opts = {id: note.id}1393unless types.nil?1394note.ntype = types.first1395update_opts[:ntype] = types.first1396end13971398unless data.nil?1399note.data = data1400update_opts[:data] = data1401end14021403framework.db.update_note(update_opts)1404rescue => e1405elog "There was an error updating note with ID #{note.id}: #{e.message}"1406next1407end1408end14091410matched_note_ids << note.id14111412row = []1413row << note.created_at14141415if note.host1416host = note.host1417row << host.address1418if set_rhosts1419addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address)1420rhosts << addr1421end1422else1423row << ''1424end14251426if note.service1427row << note.service.name || ''1428row << note.service.port || ''1429row << note.service.proto || ''1430else1431row << '' # For the Service field1432row << '' # For the Port field1433row << '' # For the Protocol field1434end14351436row << note.ntype1437row << note.data.inspect1438table << row1439end14401441if mode == :delete1442result = framework.db.delete_note(ids: matched_note_ids)1443delete_count = result.size1444end14451446if output_file1447save_csv_notes(output_file, table)1448else1449print_line1450print_line(table.to_s)1451end14521453# Finally, handle the case where the user wants the resulting list1454# of hosts to go into RHOSTS.1455set_rhosts_from_addrs(rhosts.uniq) if set_rhosts14561457print_status("Deleted #{delete_count} notes") if delete_count > 01458}1459end14601461def save_csv_notes(fpath, table)1462begin1463File.open(fpath, 'wb') do |f|1464f.write(table.to_csv)1465end1466print_status("Wrote notes to #{fpath}")1467rescue Errno::EACCES => e1468print_error("Unable to save notes. #{e.message}")1469end1470end14711472#1473# Tab completion for the loot command1474#1475# @param str [String] the string currently being typed before tab was hit1476# @param words [Array<String>] the previously completed words on the command line. words is always1477# at least 1 when tab completion has reached this stage since the command itself has been completed1478def cmd_loot_tabs(str, words)1479if words.length == 11480@@loot_opts.option_keys.select { |opt| opt.start_with?(str) }1481end1482end14831484def cmd_loot_help1485print_line "Usage: loot [options]"1486print_line " Info: loot [-h] [addr1 addr2 ...] [-t <type1,type2>]"1487print_line " Add: loot -f [fname] -i [info] -a [addr1 addr2 ...] -t [type]"1488print_line " Del: loot -d [addr1 addr2 ...]"1489print_line1490print @@loot_opts.usage1491print_line1492end14931494@@loot_opts = Rex::Parser::Arguments.new(1495[ '-a', '--add' ] => [ false, 'Add loot to the list of addresses, instead of listing.' ],1496[ '-d', '--delete' ] => [ false, 'Delete *all* loot matching host and type.' ],1497[ '-f', '--file' ] => [ true, 'File with contents of the loot to add.', '<filename>' ],1498[ '-i', '--info' ] => [ true, 'Info of the loot to add.', '<info>' ],1499[ '-t', '--type' ] => [ true, 'Search for a list of types.', '<type1,type2>' ],1500[ '-h', '--help' ] => [ false, 'Show this help information.' ],1501[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],1502[ '-u', '--update' ] => [ false, 'Update loot. Not officially supported.' ]1503)15041505def cmd_loot(*args)1506return unless active?15071508mode = :search1509host_ranges = []1510types = nil1511delete_count = 01512search_term = nil1513file = nil1514name = nil1515info = nil1516filename = nil15171518@@loot_opts.parse(args) do |opt, idx, val|1519case opt1520when '-a', '--add'1521mode = :add1522when '-d', '--delete'1523mode = :delete1524when '-f', '--file'1525filename = val1526if(!filename)1527print_error("Can't make loot with no filename")1528return1529end1530if (!File.exist?(filename) or !File.readable?(filename))1531print_error("Can't read file")1532return1533end1534when '-i', '--info'1535info = val1536if(!info)1537print_error("Can't make loot with no info")1538return1539end1540when '-t', '--type'1541typelist = val1542if(!typelist)1543print_error("Invalid type list")1544return1545end1546types = typelist.strip().split(",")1547when '-S', '--search'1548search_term = val1549when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported.1550mode = :update1551when '-h', '--help'1552cmd_loot_help1553return1554else1555# Anything that wasn't an option is a host to search for1556unless (arg_host_range(val, host_ranges))1557return1558end1559end1560end15611562tbl = Rex::Text::Table.new({1563'Header' => "Loot",1564'Columns' => [ 'host', 'service', 'type', 'name', 'content', 'info', 'path' ],1565# For now, don't perform any word wrapping on the loot table as it breaks the workflow of1566# copying paths and pasting them into applications1567'WordWrap' => false,1568})15691570# Sentinel value meaning all1571host_ranges.push(nil) if host_ranges.empty?15721573if mode == :add1574if host_ranges.compact.empty?1575print_error('Address list required')1576return1577end1578if info.nil?1579print_error("Info required")1580return1581end1582if filename.nil?1583print_error("Loot file required")1584return1585end1586if types.nil? or types.size != 11587print_error("Exactly one loot type is required")1588return1589end1590type = types.first1591name = File.basename(filename)1592file = File.open(filename, "rb")1593contents = file.read1594host_ranges.each do |range|1595range.each do |host|1596lootfile = framework.db.find_or_create_loot(:type => type, :host => host, :info => info, :data => contents, :path => filename, :name => name)1597print_status("Added loot for #{host} (#{lootfile})")1598end1599end1600return1601end16021603matched_loot_ids = []1604loots = []1605if host_ranges.compact.empty?1606loots = loots + framework.db.loots(workspace: framework.db.workspace, search_term: search_term)1607else1608each_host_range_chunk(host_ranges) do |host_search|1609next if host_search && host_search.empty?16101611loots = loots + framework.db.loots(workspace: framework.db.workspace, hosts: { address: host_search }, search_term: search_term)1612end1613end16141615loots.each do |loot|1616row = []1617# TODO: This is just a temp implementation of update for the time being since it did not exist before.1618# It should be updated to not pass all of the attributes attached to the object, only the ones being updated.1619if mode == :update1620begin1621loot.info = info if info1622if types && types.size > 11623print_error "May only pass 1 type when performing an update."1624next1625end1626loot.ltype = types.first if types1627framework.db.update_loot(loot.as_json.symbolize_keys)1628rescue => e1629elog "There was an error updating loot with ID #{loot.id}: #{e.message}"1630next1631end1632end1633row.push (loot.host && loot.host.address) ? loot.host.address : ""1634if (loot.service)1635svc = (loot.service.name ? loot.service.name : "#{loot.service.port}/#{loot.service.proto}")1636row.push svc1637else1638row.push ""1639end1640row.push(loot.ltype)1641row.push(loot.name || "")1642row.push(loot.content_type)1643row.push(loot.info || "")1644row.push(loot.path)16451646tbl << row1647matched_loot_ids << loot.id1648end16491650if (mode == :delete)1651result = framework.db.delete_loot(ids: matched_loot_ids)1652delete_count = result.size1653end16541655print_line1656print_line(tbl.to_s)1657print_status("Deleted #{delete_count} loots") if delete_count > 01658end16591660# :category: Deprecated Commands1661def cmd_db_hosts_help; deprecated_help(:hosts); end1662# :category: Deprecated Commands1663def cmd_db_notes_help; deprecated_help(:notes); end1664# :category: Deprecated Commands1665def cmd_db_vulns_help; deprecated_help(:vulns); end1666# :category: Deprecated Commands1667def cmd_db_services_help; deprecated_help(:services); end1668# :category: Deprecated Commands1669def cmd_db_autopwn_help; deprecated_help; end1670# :category: Deprecated Commands1671def cmd_db_driver_help; deprecated_help; end16721673# :category: Deprecated Commands1674def cmd_db_hosts(*args); deprecated_cmd(:hosts, *args); end1675# :category: Deprecated Commands1676def cmd_db_notes(*args); deprecated_cmd(:notes, *args); end1677# :category: Deprecated Commands1678def cmd_db_vulns(*args); deprecated_cmd(:vulns, *args); end1679# :category: Deprecated Commands1680def cmd_db_services(*args); deprecated_cmd(:services, *args); end1681# :category: Deprecated Commands1682def cmd_db_autopwn(*args); deprecated_cmd; end16831684#1685# :category: Deprecated Commands1686#1687# This one deserves a little more explanation than standard deprecation1688# warning, so give the user a better understanding of what's going on.1689#1690def cmd_db_driver(*args)1691deprecated_cmd1692print_line1693print_line "Because Metasploit no longer supports databases other than the default"1694print_line "PostgreSQL, there is no longer a need to set the driver. Thus db_driver"1695print_line "is not useful and its functionality has been removed. Usually Metasploit"1696print_line "will already have connected to the database; check db_status to see."1697print_line1698cmd_db_status1699end17001701def cmd_db_import_tabs(str, words)1702tab_complete_filenames(str, words)1703end17041705def cmd_db_import_help1706print_line "Usage: db_import <filename> [file2...]"1707print_line1708print_line "Filenames can be globs like *.xml, or **/*.xml which will search recursively"1709print_line "Currently supported file types include:"1710print_line " Acunetix"1711print_line " Amap Log"1712print_line " Amap Log -m"1713print_line " Appscan"1714print_line " Burp Session XML"1715print_line " Burp Issue XML"1716print_line " CI"1717print_line " Foundstone"1718print_line " FusionVM XML"1719print_line " Group Policy Preferences Credentials"1720print_line " IP Address List"1721print_line " IP360 ASPL"1722print_line " IP360 XML v3"1723print_line " Libpcap Packet Capture"1724print_line " Masscan XML"1725print_line " Metasploit PWDump Export"1726print_line " Metasploit XML"1727print_line " Metasploit Zip Export"1728print_line " Microsoft Baseline Security Analyzer"1729print_line " NeXpose Simple XML"1730print_line " NeXpose XML Report"1731print_line " Nessus NBE Report"1732print_line " Nessus XML (v1)"1733print_line " Nessus XML (v2)"1734print_line " NetSparker XML"1735print_line " Nikto XML"1736print_line " Nmap XML"1737print_line " OpenVAS Report"1738print_line " OpenVAS XML (optional arguments -cert -dfn)"1739print_line " Outpost24 XML"1740print_line " Qualys Asset XML"1741print_line " Qualys Scan XML"1742print_line " Retina XML"1743print_line " Spiceworks CSV Export"1744print_line " Wapiti XML"1745print_line1746end17471748#1749# Generic import that automatically detects the file type1750#1751def cmd_db_import(*args)1752return unless active?1753openvas_cert = false1754openvas_dfn = false1755::ApplicationRecord.connection_pool.with_connection {1756if args.include?("-h") || ! (args && args.length > 0)1757cmd_db_import_help1758return1759end1760if args.include?("-dfn")1761openvas_dfn = true1762end1763if args.include?("-cert")1764openvas_cert = true1765end1766options = {:openvas_dfn => openvas_dfn, :openvas_cert => openvas_cert}1767args.each { |glob|1768next if (glob.include?("-cert") || glob.include?("-dfn"))1769files = ::Dir.glob(::File.expand_path(glob))1770if files.empty?1771print_error("No such file #{glob}")1772next1773end1774files.each { |filename|1775if (not ::File.readable?(filename))1776print_error("Could not read file #{filename}")1777next1778end1779begin1780warnings = 01781framework.db.import_file(:filename => filename, :options => options) do |type,data|1782case type1783when :debug1784print_error("DEBUG: #{data.inspect}")1785when :vuln1786inst = data[1] == 1 ? "instance" : "instances"1787print_status("Importing vulnerability '#{data[0]}' (#{data[1]} #{inst})")1788when :filetype1789print_status("Importing '#{data}' data")1790when :parser1791print_status("Import: Parsing with '#{data}'")1792when :address1793print_status("Importing host #{data}")1794when :service1795print_status("Importing service #{data}")1796when :msf_loot1797print_status("Importing loot #{data}")1798when :msf_task1799print_status("Importing task #{data}")1800when :msf_report1801print_status("Importing report #{data}")1802when :pcap_count1803print_status("Import: #{data} packets processed")1804when :record_count1805print_status("Import: #{data[1]} records processed")1806when :warning1807print_error1808data.split("\n").each do |line|1809print_error(line)1810end1811print_error1812warnings += 11813end1814end1815print_status("Successfully imported #{filename}")18161817print_error("Please note that there were #{warnings} warnings") if warnings > 11818print_error("Please note that there was one warning") if warnings == 118191820rescue Msf::DBImportError => e1821print_error("Failed to import #{filename}: #{$!}")1822elog("Failed to import #{filename}", error: e)1823dlog("Call stack: #{$@.join("\n")}", LEV_3)1824next1825rescue REXML::ParseException => e1826print_error("Failed to import #{filename} due to malformed XML:")1827print_error("#{e.class}: #{e}")1828elog("Failed to import #{filename}", error: e)1829dlog("Call stack: #{$@.join("\n")}", LEV_3)1830next1831end1832}1833}1834}1835end18361837def cmd_db_export_help1838# Like db_hosts and db_services, this creates a list of columns, so1839# use its -h1840cmd_db_export("-h")1841end18421843#1844# Export an XML1845#1846def cmd_db_export(*args)1847return unless active?1848::ApplicationRecord.connection_pool.with_connection {18491850export_formats = %W{xml pwdump}1851format = 'xml'1852output = nil18531854while (arg = args.shift)1855case arg1856when '-h','--help'1857print_line "Usage:"1858print_line " db_export -f <format> [filename]"1859print_line " Format can be one of: #{export_formats.join(", ")}"1860when '-f','--format'1861format = args.shift.to_s.downcase1862else1863output = arg1864end1865end18661867if not output1868print_error("No output file was specified")1869return1870end18711872if not export_formats.include?(format)1873print_error("Unsupported file format: #{format}")1874print_error("Unsupported file format: '#{format}'. Must be one of: #{export_formats.join(", ")}")1875return1876end18771878print_status("Starting export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...")1879framework.db.run_db_export(output, format)1880print_status("Finished export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...")1881}1882end18831884def find_nmap_path1885Rex::FileUtils.find_full_path("nmap") || Rex::FileUtils.find_full_path("nmap.exe")1886end18871888#1889# Import Nmap data from a file1890#1891def cmd_db_nmap(*args)1892return unless active?1893::ApplicationRecord.connection_pool.with_connection {1894if (args.length == 0)1895print_status("Usage: db_nmap [--save | [--help | -h]] [nmap options]")1896return1897end18981899save = false1900arguments = []1901while (arg = args.shift)1902case arg1903when '--save'1904save = true1905when '--help', '-h'1906cmd_db_nmap_help1907return1908else1909arguments << arg1910end1911end19121913nmap = find_nmap_path1914unless nmap1915print_error("The nmap executable could not be found")1916return1917end19181919fd = Rex::Quickfile.new(['msf-db-nmap-', '.xml'], Msf::Config.local_directory)19201921begin1922# When executing native Nmap in Cygwin, expand the Cygwin path to a Win32 path1923if(Rex::Compat.is_cygwin and nmap =~ /cygdrive/)1924# Custom function needed because cygpath breaks on 8.3 dirs1925tout = Rex::Compat.cygwin_to_win32(fd.path)1926arguments.push('-oX', tout)1927else1928arguments.push('-oX', fd.path)1929end19301931run_nmap(nmap, arguments)19321933framework.db.import_nmap_xml_file(:filename => fd.path)19341935print_status("Saved NMAP XML results to #{fd.path}") if save1936ensure1937fd.close1938fd.unlink unless save1939end1940}1941end19421943def cmd_db_nmap_help1944nmap = find_nmap_path1945unless nmap1946print_error("The nmap executable could not be found")1947return1948end19491950stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help')19511952stdout.each_line do |out_line|1953next if out_line.strip.empty?1954print_status(out_line.strip)1955end19561957stderr.each_line do |err_line|1958next if err_line.strip.empty?1959print_error(err_line.strip)1960end1961end19621963def cmd_db_nmap_tabs(str, words)1964nmap = find_nmap_path1965unless nmap1966return1967end19681969stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help')1970tabs = []1971stdout.each_line do |out_line|1972if out_line.strip.starts_with?('-')1973tabs.push(out_line.strip.split(':').first)1974end1975end19761977stderr.each_line do |err_line|1978next if err_line.strip.empty?1979print_error(err_line.strip)1980end19811982return tabs1983end19841985#1986# Database management1987#1988def db_check_driver1989unless framework.db.driver1990print_error("No database driver installed.")1991return false1992end1993true1994end19951996#1997# Is everything working?1998#1999def cmd_db_status(*args)2000return if not db_check_driver20012002if framework.db.connection_established?2003print_connection_info2004else2005print_status("#{framework.db.driver} selected, no connection")2006end2007end200820092010def cmd_db_connect_help2011print_line(" USAGE:")2012print_line(" * Postgres Data Service:")2013print_line(" db_connect <user:[pass]>@<host:[port]>/<database>")2014print_line(" Examples:")2015print_line(" db_connect user@metasploit3")2016print_line(" db_connect user:[email protected]/metasploit3")2017print_line(" db_connect user:[email protected]:1500/metasploit3")2018print_line(" db_connect -y [path/to/database.yml]")2019print_line(" ")2020print_line(" * HTTP Data Service:")2021print_line(" db_connect [options] <http|https>://<host:[port]>")2022print_line(" Examples:")2023print_line(" db_connect http://localhost:8080")2024print_line(" db_connect http://my-super-msf-data.service.com")2025print_line(" db_connect -c ~/cert.pem -t 6a7a74c1a5003802c955ead1bbddd4ab1b05a7f2940b4732d34bfc555bc6e1c5d7611a497b29e8f0 https://localhost:8080")2026print_line(" NOTE: You must be connected to a Postgres data service in order to successfully connect to a HTTP data service.")2027print_line(" ")2028print_line(" Persisting Connections:")2029print_line(" db_connect --name <name to save connection as> [options] <address>")2030print_line(" Examples:")2031print_line(" Saving: db_connect --name LA-server http://123.123.123.45:1234")2032print_line(" Connecting: db_connect LA-server")2033print_line(" ")2034print_line(" OPTIONS:")2035print_line(" -l,--list-services List the available data services that have been previously saved.")2036print_line(" -y,--yaml Connect to the data service specified in the provided database.yml file.")2037print_line(" -n,--name Name used to store the connection. Providing an existing name will overwrite the settings for that connection.")2038print_line(" -c,--cert Certificate file matching the remote data server's certificate. Needed when using self-signed SSL cert.")2039print_line(" -t,--token The API token used to authenticate to the remote data service.")2040print_line(" --skip-verify Skip validating authenticity of server's certificate (NOT RECOMMENDED).")2041print_line("")2042end20432044def cmd_db_connect(*args)2045return if not db_check_driver20462047opts = {}2048while (arg = args.shift)2049case arg2050when '-h', '--help'2051cmd_db_connect_help2052return2053when '-y', '--yaml'2054opts[:yaml_file] = args.shift2055when '-c', '--cert'2056opts[:cert] = args.shift2057when '-t', '--token'2058opts[:api_token] = args.shift2059when '-l', '--list-services'2060list_saved_data_services2061return2062when '-n', '--name'2063opts[:name] = args.shift2064if opts[:name] =~ /\/|\[|\]/2065print_error "Provided name contains an invalid character. Aborting connection."2066return2067end2068when '--skip-verify'2069opts[:skip_verify] = true2070else2071found_name = ::Msf::DbConnector.data_service_search(name: arg)2072if found_name2073opts = ::Msf::DbConnector.load_db_config(found_name)2074else2075opts[:url] = arg2076end2077end2078end20792080if !opts[:url] && !opts[:yaml_file]2081print_error 'A URL or saved data service name is required.'2082print_line2083cmd_db_connect_help2084return2085end20862087if opts[:url] =~ /http/2088new_conn_type = 'http'2089else2090new_conn_type = framework.db.driver2091end20922093# Currently only able to be connected to one DB at a time2094if framework.db.connection_established?2095# But the http connection still requires a local database to support AR, so we have to allow that2096# Don't allow more than one HTTP service, though2097if new_conn_type != 'http' || framework.db.get_services_metadata.count >= 22098print_error('Connection already established. Only one connection is allowed at a time.')2099print_error('Run db_disconnect first if you wish to connect to a different data service.')2100print_line2101print_line 'Current connection information:'2102print_connection_info2103return2104end2105end21062107result = Msf::DbConnector.db_connect(framework, opts)2108if result[:error]2109print_error result[:error]2110return2111end21122113if result[:result]2114print_status result[:result]2115end2116if framework.db.active2117name = opts[:name]2118if !name || name.empty?2119if found_name2120name = found_name2121elsif result[:data_service_name]2122name = result[:data_service_name]2123else2124name = Rex::Text.rand_text_alphanumeric(8)2125end2126end21272128save_db_to_config(framework.db, name)2129@current_data_service = name2130end2131end21322133def cmd_db_disconnect_help2134print_line "Usage:"2135print_line " db_disconnect Temporarily disconnects from the currently configured dataservice."2136print_line " db_disconnect --clear Clears the default dataservice that msfconsole will use when opened."2137print_line2138end21392140def cmd_db_disconnect(*args)2141return if not db_check_driver21422143if args[0] == '-h' || args[0] == '--help'2144cmd_db_disconnect_help2145return2146elsif args[0] == '-c' || args[0] == '--clear'2147clear_default_db2148return2149end21502151previous_name = framework.db.name2152result = Msf::DbConnector.db_disconnect(framework)21532154if result[:error]2155print_error "Unable to disconnect from the data service: #{@current_data_service}"2156print_error result[:error]2157elsif result[:old_data_service_name].nil?2158print_error 'Not currently connected to a data service.'2159else2160print_line "Successfully disconnected from the data service: #{previous_name}."2161@current_data_service = result[:data_service_name]2162if @current_data_service2163print_line "Now connected to: #{@current_data_service}."2164end2165end2166end21672168def cmd_db_rebuild_cache(*args)2169print_line "This command is deprecated with Metasploit 5"2170end21712172def cmd_db_save_help2173print_line "Usage: db_save"2174print_line2175print_line "Save the current data service connection as the default to reconnect on startup."2176print_line2177end21782179def cmd_db_save(*args)2180while (arg = args.shift)2181case arg2182when '-h', '--help'2183cmd_db_save_help2184return2185end2186end21872188if !framework.db.active || !@current_data_service2189print_error "Not currently connected to a data service that can be saved."2190return2191end21922193begin2194Msf::Config.save(DB_CONFIG_PATH => { 'default_db' => @current_data_service })2195print_line "Successfully saved data service as default: #{@current_data_service}"2196rescue ArgumentError => e2197print_error e.message2198end2199end22002201def save_db_to_config(database, database_name)2202if database_name =~ /\/|\[|\]/2203raise ArgumentError, 'Data service name contains an invalid character.'2204end2205config_path = "#{DB_CONFIG_PATH}/#{database_name}"2206config_opts = {}2207if !database.is_local?2208begin2209config_opts['url'] = database.endpoint2210if database.https_opts2211config_opts['cert'] = database.https_opts[:cert] if database.https_opts[:cert]2212config_opts['skip_verify'] = true if database.https_opts[:skip_verify]2213end2214if database.api_token2215config_opts['api_token'] = database.api_token2216end2217Msf::Config.save(config_path => config_opts)2218rescue => e2219print_error "There was an error saving the data service configuration: #{e.message}"2220end2221else2222url = Msf::DbConnector.build_postgres_url2223config_opts['url'] = url2224Msf::Config.save(config_path => config_opts)2225end2226end22272228def cmd_db_remove_help2229print_line "Usage: db_remove <name>"2230print_line2231print_line "Delete the specified saved data service."2232print_line2233end22342235def cmd_db_remove(*args)2236if args[0] == '-h' || args[0] == '--help' || args[0].nil? || args[0].empty?2237cmd_db_remove_help2238return2239end2240delete_db_from_config(args[0])2241end22422243def delete_db_from_config(db_name)2244conf = Msf::Config.load2245db_path = "#{DB_CONFIG_PATH}/#{db_name}"2246if conf[db_path]2247clear_default_db if conf[DB_CONFIG_PATH]['default_db'] && conf[DB_CONFIG_PATH]['default_db'] == db_name2248Msf::Config.delete_group(db_path)2249print_line "Successfully deleted data service: #{db_name}"2250else2251print_line "Unable to locate saved data service with name #{db_name}."2252end2253end22542255def clear_default_db2256conf = Msf::Config.load2257if conf[DB_CONFIG_PATH] && conf[DB_CONFIG_PATH]['default_db']2258updated_opts = conf[DB_CONFIG_PATH]2259updated_opts.delete('default_db')2260Msf::Config.save(DB_CONFIG_PATH => updated_opts)2261print_line "Cleared the default data service."2262else2263print_line "No default data service was configured."2264end2265end22662267def db_find_tools(tools)2268missed = []2269tools.each do |name|2270if(! Rex::FileUtils.find_full_path(name))2271missed << name2272end2273end2274if(not missed.empty?)2275print_error("This database command requires the following tools to be installed: #{missed.join(", ")}")2276return2277end2278true2279end22802281#######2282private22832284def run_nmap(nmap, arguments, use_sudo: false)2285print_warning('Running Nmap with sudo') if use_sudo2286begin2287nmap_pipe = use_sudo ? ::Open3::popen3('sudo', nmap, *arguments) : ::Open3::popen3(nmap, *arguments)2288temp_nmap_threads = []2289temp_nmap_threads << framework.threads.spawn("db_nmap-Stdout", false, nmap_pipe[1]) do |np_1|2290np_1.each_line do |nmap_out|2291next if nmap_out.strip.empty?2292print_status("Nmap: #{nmap_out.strip}")2293end2294end22952296temp_nmap_threads << framework.threads.spawn("db_nmap-Stderr", false, nmap_pipe[2]) do |np_2|22972298np_2.each_line do |nmap_err|2299next if nmap_err.strip.empty?2300print_status("Nmap: '#{nmap_err.strip}'")2301# Check if the stderr text includes 'root', this only happens if the scan requires root privileges2302if nmap_err =~ /requires? root privileges/ or2303nmap_err.include? 'only works if you are root' or nmap_err =~ /requires? raw socket access/2304return run_nmap(nmap, arguments, use_sudo: true) unless use_sudo2305end2306end2307end23082309temp_nmap_threads.map { |t| t.join rescue nil }2310nmap_pipe.each { |p| p.close rescue nil }2311rescue ::IOError2312end2313end23142315#######23162317def print_connection_info2318cdb = ''2319if framework.db.driver == 'http'2320cdb = framework.db.name2321else2322::ApplicationRecord.connection_pool.with_connection do |conn|2323if conn.respond_to?(:current_database)2324cdb = conn.current_database2325end2326end2327end2328output = "Connected to #{cdb}. Connection type: #{framework.db.driver}."2329output += " Connection name: #{@current_data_service}." if @current_data_service2330print_status(output)2331end23322333def list_saved_data_services2334conf = Msf::Config.load2335default = nil2336tbl = Rex::Text::Table.new({2337'Header' => 'Data Services',2338'Columns' => ['current', 'name', 'url', 'default?'],2339'SortIndex' => 12340})23412342conf.each_pair do |k,v|2343if k =~ /#{DB_CONFIG_PATH}/2344default = v['default_db'] if v['default_db']2345name = k.split('/').last2346next if name == 'database' # Data service information is not stored in 'framework/database', just metadata2347url = v['url']2348current = ''2349current = '*' if name == @current_data_service2350default_output = ''2351default_output = '*' if name == default2352line = [current, name, url, default_output]2353tbl << line2354end2355end2356print_line2357print_line tbl.to_s2358end23592360def print_msgs(status_msg, error_msg)2361status_msg.each do |s|2362print_status(s)2363end23642365error_msg.each do |e|2366print_error(e)2367end2368end23692370def _format_vulns_and_vuln_attempts(vulns)2371vulns.map.with_index do |vuln, index|2372service_str = ''2373if vuln.service.present?2374service_str << "#{vuln.service.name} (port: #{vuln.service.port}, resource: #{vuln.service.resource.to_json})"2375if vuln.service.parents.any?2376service_str << "\nParent Services:\n".indent(5)2377service_str << _print_service_parents(vuln.service).indent(7)2378end2379end23802381vuln_formatted = <<~EOF.strip.indent(2)2382#{index}. Vuln ID: #{vuln.id}2383Timestamp: #{vuln.created_at}2384Host: #{vuln.host.address}2385Name: #{vuln.name}2386References: #{vuln.refs.map {|r| r.name}.join(',')}2387Information: #{_format_vuln_value(vuln.info)}2388Resource: #{vuln.resource.to_json}2389Service: #{service_str}2390EOF23912392vuln_attempts_formatted = vuln.vuln_attempts.map.with_index do |vuln_attempt, i|2393<<~EOF.strip.indent(5)2394#{i}. ID: #{vuln_attempt.id}2395Vuln ID: #{vuln_attempt.vuln_id}2396Timestamp: #{vuln_attempt.attempted_at}2397Exploit: #{vuln_attempt.exploited}2398Fail reason: #{_format_vuln_value(vuln_attempt.fail_reason)}2399Username: #{vuln_attempt.username}2400Module: #{vuln_attempt.module}2401Session ID: #{_format_vuln_value(vuln_attempt.session_id)}2402Loot ID: #{_format_vuln_value(vuln_attempt.loot_id)}2403Fail Detail: #{_format_vuln_value(vuln_attempt.fail_detail)}2404EOF2405end24062407{ :vuln => vuln_formatted, :vuln_attempts => vuln_attempts_formatted }2408end2409end24102411def _print_service_parents(service, indent_level = 0)2412service.parents.map do |parent_service|2413parent_service_str = "#{parent_service.name} (port: #{parent_service.port}, resource: #{parent_service.resource.to_json})".indent(indent_level * 2)2414if parent_service.parents&.any?2415parent_service_str << "\n#{_print_service_parents(parent_service, indent_level + 1)}"2416end2417parent_service_str2418end.flatten.join("\n")2419end24202421def _print_vulns_and_attempts(vulns_and_attempts)2422print_line("Vulnerabilities\n===============")2423vulns_and_attempts.each do |vuln_and_attempt|2424print_line(vuln_and_attempt[:vuln])2425print_line("Vuln attempts:".indent(5))2426vuln_and_attempt[:vuln_attempts].each do |attempt|2427print_line(attempt)2428end2429end2430end24312432def _format_vuln_value(s)2433s.blank? ? s.inspect : s.to_s2434end2435end24362437end end end end243824392440