Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/msf/ui/console/command_dispatcher/db.rb
Views: 11784
# -*- 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::Klist2122DB_CONFIG_PATH = 'framework/database'2324#25# The dispatcher's name.26#27def name28"Database Backend"29end3031#32# Returns the hash of commands supported by this dispatcher.33#34def commands35base = {36"db_connect" => "Connect to an existing data service",37"db_disconnect" => "Disconnect from the current data service",38"db_status" => "Show the current data service status",39"db_save" => "Save the current data service connection as the default to reconnect on startup",40"db_remove" => "Remove the saved data service entry"41}4243more = {44"workspace" => "Switch between database workspaces",45"hosts" => "List all hosts in the database",46"services" => "List all services in the database",47"vulns" => "List all vulnerabilities in the database",48"notes" => "List all notes in the database",49"loot" => "List all loot in the database",50"klist" => "List Kerberos tickets in the database",51"db_import" => "Import a scan result file (filetype will be auto-detected)",52"db_export" => "Export a file containing the contents of the database",53"db_nmap" => "Executes nmap and records the output automatically",54"db_rebuild_cache" => "Rebuilds the database-stored module cache (deprecated)",55"analyze" => "Analyze database information about a specific address or address range",56"db_stats" => "Show statistics for the database"57}5859# Always include commands that only make sense when connected.60# This avoids the problem of them disappearing unexpectedly if the61# database dies or times out. See #19236263base.merge(more)64end6566def deprecated_commands67[68"db_autopwn",69"db_driver",70"db_hosts",71"db_notes",72"db_services",73"db_vulns",74]75end7677#78# Attempts to connect to the previously configured database, and additionally keeps track of79# the currently loaded data service.80#81def load_config(path = nil)82result = Msf::DbConnector.db_connect_from_config(framework, path)8384if result[:error]85print_error(result[:error])86end87if result[:data_service_name]88@current_data_service = result[:data_service_name]89end90end9192@@workspace_opts = Rex::Parser::Arguments.new(93[ '-h', '--help' ] => [ false, 'Help banner.'],94[ '-a', '--add' ] => [ true, 'Add a workspace.', '<name>'],95[ '-d', '--delete' ] => [ true, 'Delete a workspace.', '<name>'],96[ '-D', '--delete-all' ] => [ false, 'Delete all workspaces.'],97[ '-r', '--rename' ] => [ true, 'Rename a workspace.', '<old> <new>'],98[ '-l', '--list' ] => [ false, 'List workspaces.'],99[ '-v', '--list-verbose' ] => [ false, 'List workspaces verbosely.'],100[ '-S', '--search' ] => [ true, 'Search for a workspace.', '<name>']101)102103def cmd_workspace_help104print_line "Usage:"105print_line " workspace List workspaces"106print_line " workspace [name] Switch workspace"107print_line @@workspace_opts.usage108end109110def cmd_workspace(*args)111return unless active?112113state = :nil114115list = false116verbose = false117names = []118search_term = nil119120@@workspace_opts.parse(args) do |opt, idx, val|121case opt122when '-h', '--help'123cmd_workspace_help124return125when '-a', '--add'126return cmd_workspace_help unless state == :nil127128state = :adding129names << val if !val.nil?130when '-d', '--del'131return cmd_workspace_help unless state == :nil132133state = :deleting134names << val if !val.nil?135when '-D', '--delete-all'136return cmd_workspace_help unless state == :nil137138state = :delete_all139when '-r', '--rename'140return cmd_workspace_help unless state == :nil141142state = :renaming143names << val if !val.nil?144when '-v', '--verbose'145verbose = true146when '-l', '--list'147list = true148when '-S', '--search'149search_term = val150else151names << val if !val.nil?152end153end154155if state == :adding and names156# Add workspaces157wspace = nil158names.each do |name|159wspace = framework.db.workspaces(name: name).first160if wspace161print_status("Workspace '#{wspace.name}' already existed, switching to it.")162else163wspace = framework.db.add_workspace(name)164print_status("Added workspace: #{wspace.name}")165end166end167framework.db.workspace = wspace168print_status("Workspace: #{framework.db.workspace.name}")169elsif state == :deleting and names170ws_ids_to_delete = []171starting_ws = framework.db.workspace172names.uniq.each do |n|173ws = framework.db.workspaces(name: n).first174ws_ids_to_delete << ws.id if ws175end176if ws_ids_to_delete.count > 0177deleted = framework.db.delete_workspaces(ids: ws_ids_to_delete)178process_deleted_workspaces(deleted, starting_ws)179else180print_status("No workspaces matching the given name(s) were found.")181end182elsif state == :delete_all183ws_ids_to_delete = []184starting_ws = framework.db.workspace185framework.db.workspaces.each do |ws|186ws_ids_to_delete << ws.id187end188deleted = framework.db.delete_workspaces(ids: ws_ids_to_delete)189process_deleted_workspaces(deleted, starting_ws)190elsif state == :renaming191if names.length != 2192print_error("Wrong number of arguments to rename")193return194end195196ws_to_update = framework.db.find_workspace(names.first)197unless ws_to_update198print_error("Workspace '#{names.first}' does not exist")199return200end201opts = {202id: ws_to_update.id,203name: names.last204}205begin206updated_ws = framework.db.update_workspace(opts)207if updated_ws208framework.db.workspace = updated_ws if names.first == framework.db.workspace.name209print_status("Renamed workspace '#{names.first}' to '#{updated_ws.name}'")210else211print_error "There was a problem updating the workspace. Setting to the default workspace."212framework.db.workspace = framework.db.default_workspace213return214end215if names.first == Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME216print_status("Recreated default workspace")217end218rescue => e219print_error "Failed to rename workspace: #{e.message}"220end221222elsif !names.empty?223name = names.last224# Switch workspace225workspace = framework.db.find_workspace(name)226if workspace227framework.db.workspace = workspace228print_status("Workspace: #{workspace.name}")229else230print_error("Workspace not found: #{name}")231return232end233else234current_workspace = framework.db.workspace235236unless verbose237current = nil238framework.db.workspaces.sort_by {|s| s.name}.each do |s|239if s.name == current_workspace.name240current = s.name241else242print_line(" #{s.name}")243end244end245print_line("%red* #{current}%clr") unless current.nil?246return247end248col_names = %w{current name hosts services vulns creds loots notes}249250tbl = Rex::Text::Table.new(251'Header' => 'Workspaces',252'Columns' => col_names,253'SortIndex' => -1,254'SearchTerm' => search_term255)256257framework.db.workspaces.each do |ws|258tbl << [259current_workspace.name == ws.name ? '*' : '',260ws.name,261framework.db.hosts(workspace: ws.name).count,262framework.db.services(workspace: ws.name).count,263framework.db.vulns(workspace: ws.name).count,264framework.db.creds(workspace: ws.name).count,265framework.db.loots(workspace: ws.name).count,266framework.db.notes(workspace: ws.name).count267]268end269270print_line271print_line(tbl.to_s)272end273end274275def process_deleted_workspaces(deleted_workspaces, starting_ws)276deleted_workspaces.each do |ws|277print_status "Deleted workspace: #{ws.name}"278if ws.name == Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME279framework.db.workspace = framework.db.default_workspace280print_status 'Recreated the default workspace'281elsif ws == starting_ws282framework.db.workspace = framework.db.default_workspace283print_status "Switched to workspace: #{framework.db.workspace.name}"284end285end286end287288def cmd_workspace_tabs(str, words)289return [] unless active?290framework.db.workspaces.map(&:name) if (words & ['-a','--add']).empty?291end292293#294# Tab completion for the hosts command295#296# @param str [String] the string currently being typed before tab was hit297# @param words [Array<String>] the previously completed words on the command line. words is always298# at least 1 when tab completion has reached this stage since the command itself has been completed299def cmd_hosts_tabs(str, words)300if words.length == 1301return @@hosts_opts.option_keys.select { |opt| opt.start_with?(str) }302end303304case words[-1]305when '-c', '--columns', '-C', '--columns-until-restart'306return @@hosts_columns307when '-o', '--output'308return tab_complete_filenames(str, words)309end310311if @@hosts_opts.arg_required?(words[-1])312return []313end314315return @@hosts_opts.option_keys.select { |opt| opt.start_with?(str) }316end317318def cmd_hosts_help319# This command does some lookups for the list of appropriate column320# names, so instead of putting all the usage stuff here like other321# help methods, just use it's "-h" so we don't have to recreating322# that list323cmd_hosts("-h")324end325326# Changes the specified host data327#328# @param host_ranges - range of hosts to process329# @param host_data - hash of host data to be updated330def change_host_data(host_ranges, host_data)331if !host_data || host_data.length != 1332print_error("A single key-value data hash is required to change the host data")333return334end335attribute = host_data.keys[0]336337if host_ranges == [nil]338print_error("In order to change the host #{attribute}, you must provide a range of hosts")339return340end341342each_host_range_chunk(host_ranges) do |host_search|343next if host_search && host_search.empty?344345framework.db.hosts(address: host_search).each do |host|346framework.db.update_host(host_data.merge(id: host.id))347framework.db.report_note(host: host.address, type: "host.#{attribute}", data: host_data[attribute])348end349end350end351352def add_host_tag(rws, tag_name)353if rws == [nil]354print_error("In order to add a tag, you must provide a range of hosts")355return356end357358opts = Hash.new()359opts[:workspace] = framework.db.workspace360opts[:tag_name] = tag_name361362rws.each do |rw|363rw.each do |ip|364opts[:address] = ip365unless framework.db.add_host_tag(opts)366print_error("Host #{ip} could not be found.")367end368end369end370end371372def find_host_tags(workspace, host_id)373opts = Hash.new()374opts[:workspace] = workspace375opts[:id] = host_id376377framework.db.get_host_tags(opts)378end379380def delete_host_tag(rws, tag_name)381opts = Hash.new()382opts[:workspace] = framework.db.workspace383opts[:tag_name] = tag_name384385# This will be the case if no IP was passed in, and we are just trying to delete all386# instances of a given tag within the database.387if rws == [nil]388wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)389wspace.hosts.each do |host|390opts[:address] = host.address391framework.db.delete_host_tag(opts)392end393else394rws.each do |rw|395rw.each do |ip|396opts[:address] = ip397unless framework.db.delete_host_tag(opts)398print_error("Host #{ip} could not be found.")399end400end401end402end403end404405@@hosts_columns = [ 'address', 'mac', 'name', 'os_name', 'os_flavor', 'os_sp', 'purpose', 'info', 'comments']406407@@hosts_opts = Rex::Parser::Arguments.new(408[ '-h', '--help' ] => [ false, 'Show this help information' ],409[ '-a', '--add' ] => [ true, 'Add the hosts instead of searching', '<host>' ],410[ '-u', '--up' ] => [ false, 'Only show hosts which are up' ],411[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search' ],412[ '-S', '--search' ] => [ true, 'Search string to filter by', '<filter>' ],413[ '-i', '--info' ] => [ true, 'Change the info of a host', '<info>' ],414[ '-n', '--name' ] => [ true, 'Change the name of a host', '<name>' ],415[ '-m', '--comment' ] => [ true, 'Change the comment of a host', '<comment>' ],416[ '-t', '--tag' ] => [ true, 'Add or specify a tag to a range of hosts', '<tag>' ],417[ '-T', '--delete-tag' ] => [ true, 'Remove a tag from a range of hosts', '<tag>' ],418[ '-d', '--delete' ] => [ true, 'Delete the hosts instead of searching', '<hosts>' ],419[ '-o', '--output' ] => [ true, 'Send output to a file in csv format', '<filename>' ],420[ '-O', '--order' ] => [ true, 'Order rows by specified column number', '<column id>' ],421[ '-c', '--columns' ] => [ true, 'Only show the given columns (see list below)', '<columns>' ],422[ '-C', '--columns-until-restart' ] => [ true, 'Only show the given columns until the next restart (see list below)', '<columns>' ],423)424425def cmd_hosts(*args)426return unless active?427onlyup = false428set_rhosts = false429mode = []430delete_count = 0431432rhosts = []433host_ranges = []434search_term = nil435436order_by = nil437info_data = nil438name_data = nil439comment_data = nil440tag_name = nil441442output = nil443default_columns = [444'address',445'arch',446'comm',447'comments',448'created_at',449'cred_count',450'detected_arch',451'exploit_attempt_count',452'host_detail_count',453'info',454'mac',455'name',456'note_count',457'os_family',458'os_flavor',459'os_lang',460'os_name',461'os_sp',462'purpose',463'scope',464'service_count',465'state',466'updated_at',467'virtual_host',468'vuln_count',469'workspace_id']470471default_columns << 'tags' # Special case472virtual_columns = [ 'svcs', 'vulns', 'workspace', 'tags' ]473474col_search = @@hosts_columns475476default_columns.delete_if {|v| (v[-2,2] == "id")}477@@hosts_opts.parse(args) do |opt, idx, val|478case opt479when '-h', '--help'480print_line "Usage: hosts [ options ] [addr1 addr2 ...]"481print_line482print @@hosts_opts.usage483print_line484print_line "Available columns: #{default_columns.join(", ")}"485print_line486return487when '-a', '--add'488mode << :add489arg_host_range(val, host_ranges)490when '-d', '--delete'491mode << :delete492arg_host_range(val, host_ranges)493when '-u', '--up'494onlyup = true495when '-o'496output = val497output = ::File.expand_path(output)498when '-R', '--rhosts'499set_rhosts = true500when '-S', '--search'501search_term = val502when '-i', '--info'503mode << :new_info504info_data = val505when '-n', '--name'506mode << :new_name507name_data = val508when '-m', '--comment'509mode << :new_comment510comment_data = val511when '-t', '--tag'512mode << :tag513tag_name = val514when '-T', '--delete-tag'515mode << :delete_tag516tag_name = val517when '-c', '-C'518list = val519if(!list)520print_error("Invalid column list")521return522end523col_search = list.strip().split(",")524col_search.each { |c|525if not default_columns.include?(c) and not virtual_columns.include?(c)526all_columns = default_columns + virtual_columns527print_error("Invalid column list. Possible values are (#{all_columns.join("|")})")528return529end530}531if opt == '-C'532@@hosts_columns = col_search533end534when '-O'535if (order_by = val.to_i - 1) < 0536print_error('Please specify a column number starting from 1')537return538end539else540# Anything that wasn't an option is a host to search for541unless (arg_host_range(val, host_ranges))542return543end544end545end546547if col_search548col_names = col_search549else550col_names = default_columns + virtual_columns551end552553mode << :search if mode.empty?554555if mode == [:add]556host_ranges.each do |range|557range.each do |address|558host = framework.db.find_or_create_host(:host => address)559print_status("Time: #{host.created_at} Host: host=#{host.address}")560end561end562return563end564565cp_hsh = {}566col_names.map do |col|567cp_hsh[col] = { 'MaxChar' => 52 }568end569# If we got here, we're searching. Delete implies search570tbl = Rex::Text::Table.new(571{572'Header' => "Hosts",573'Columns' => col_names,574'ColProps' => cp_hsh,575'SortIndex' => order_by576})577578# Sentinel value meaning all579host_ranges.push(nil) if host_ranges.empty?580581case582when mode == [:new_info]583change_host_data(host_ranges, info: info_data)584return585when mode == [:new_name]586change_host_data(host_ranges, name: name_data)587return588when mode == [:new_comment]589change_host_data(host_ranges, comments: comment_data)590return591when mode == [:tag]592begin593add_host_tag(host_ranges, tag_name)594rescue => e595if e.message.include?('Validation failed')596print_error(e.message)597else598raise e599end600end601return602when mode == [:delete_tag]603begin604delete_host_tag(host_ranges, tag_name)605rescue => e606if e.message.include?('Validation failed')607print_error(e.message)608else609raise e610end611end612return613end614615matched_host_ids = []616each_host_range_chunk(host_ranges) do |host_search|617next if host_search && host_search.empty?618619framework.db.hosts(address: host_search, non_dead: onlyup, search_term: search_term).each do |host|620matched_host_ids << host.id621columns = col_names.map do |n|622# Deal with the special cases623if virtual_columns.include?(n)624case n625when "svcs"; host.service_count626when "vulns"; host.vuln_count627when "workspace"; host.workspace.name628when "tags"629found_tags = find_host_tags(framework.db.workspace, host.id)630tag_names = found_tags.map(&:name).join(', ')631tag_names632end633# Otherwise, it's just an attribute634else635host[n] || ""636end637end638639tbl << columns640if set_rhosts641addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address)642rhosts << addr643end644end645646if mode == [:delete]647result = framework.db.delete_host(ids: matched_host_ids)648delete_count += result.size649end650end651652if output653print_status("Wrote hosts to #{output}")654::File.open(output, "wb") { |ofd|655ofd.write(tbl.to_csv)656}657else658print_line659print_line(tbl.to_s)660end661662# Finally, handle the case where the user wants the resulting list663# of hosts to go into RHOSTS.664set_rhosts_from_addrs(rhosts.uniq) if set_rhosts665666print_status("Deleted #{delete_count} hosts") if delete_count > 0667end668669#670# Tab completion for the services command671#672# @param str [String] the string currently being typed before tab was hit673# @param words [Array<String>] the previously completed words on the command line. words is always674# at least 1 when tab completion has reached this stage since the command itself has been completed675def cmd_services_tabs(str, words)676if words.length == 1677return @@services_opts.option_keys.select { |opt| opt.start_with?(str) }678end679680case words[-1]681when '-c', '--column'682return @@services_columns683when '-O', '--order'684return []685when '-o', '--output'686return tab_complete_filenames(str, words)687when '-p', '--port'688return []689when '-r', '--protocol'690return []691end692693[]694end695696def cmd_services_help697print_line "Usage: services [-h] [-u] [-a] [-r <proto>] [-p <port1,port2>] [-s <name1,name2>] [-o <filename>] [addr1 addr2 ...]"698print_line699print @@services_opts.usage700print_line701print_line "Available columns: #{@@services_columns.join(", ")}"702print_line703end704705@@services_columns = [ 'created_at', 'info', 'name', 'port', 'proto', 'state', 'updated_at' ]706707@@services_opts = Rex::Parser::Arguments.new(708[ '-a', '--add' ] => [ false, 'Add the services instead of searching.' ],709[ '-d', '--delete' ] => [ false, 'Delete the services instead of searching.' ],710[ '-U', '--update' ] => [ false, 'Update data for existing service.' ],711[ '-u', '--up' ] => [ false, 'Only show services which are up.' ],712[ '-c', '--column' ] => [ true, 'Only show the given columns.', '<col1,col2>' ],713[ '-p', '--port' ] => [ true, 'Search for a list of ports.', '<ports>' ],714[ '-r', '--protocol' ] => [ true, 'Protocol type of the service being added [tcp|udp].', '<protocol>' ],715[ '-s', '--name' ] => [ true, 'Name of the service to add.', '<name>' ],716[ '-o', '--output' ] => [ true, 'Send output to a file in csv format.', '<filename>' ],717[ '-O', '--order' ] => [ true, 'Order rows by specified column number.', '<column id>' ],718[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],719[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],720[ '-h', '--help' ] => [ false, 'Show this help information.' ]721)722723def db_connection_info(framework)724unless framework.db.connection_established?725return "#{framework.db.driver} selected, no connection"726end727728cdb = ''729if framework.db.driver == 'http'730cdb = framework.db.name731else732::ApplicationRecord.connection_pool.with_connection do |conn|733if conn.respond_to?(:current_database)734cdb = conn.current_database735end736end737end738739if cdb.empty?740output = "Connected Database Name could not be extracted. DB Connection type: #{framework.db.driver}."741else742output = "Connected to #{cdb}. Connection type: #{framework.db.driver}."743end744745output746end747748def cmd_db_stats(*args)749return unless active?750print_line "Session Type: #{db_connection_info(framework)}"751752current_workspace = framework.db.workspace753example_workspaces = ::Mdm::Workspace.order(id: :desc)754ordered_workspaces = ([current_workspace] + example_workspaces).uniq.sort_by(&:id)755756tbl = Rex::Text::Table.new(757'Indent' => 2,758'Header' => "Database Stats",759'Columns' =>760[761"IsTarget",762"ID",763"Name",764"Hosts",765"Services",766"Services per Host",767"Vulnerabilities",768"Vulns per Host",769"Notes",770"Creds",771"Kerberos Cache"772],773'SortIndex' => 1,774'ColProps' => {775'IsTarget' => {776'Stylers' => [Msf::Ui::Console::TablePrint::RowIndicatorStyler.new],777'ColumnStylers' => [Msf::Ui::Console::TablePrint::OmitColumnHeader.new],778'Width' => 2779}780}781)782783total_hosts = 0784total_services = 0785total_vulns = 0786total_notes = 0787total_creds = 0788total_tickets = 0789790ordered_workspaces.map do |workspace|791792hosts = workspace.hosts.count793services = workspace.services.count794vulns = workspace.vulns.count795notes = workspace.notes.count796creds = framework.db.creds(workspace: workspace.name).count # workspace.creds.count.to_fs(:delimited) is always 0 for whatever reason797kerbs = ticket_search([nil], nil, :workspace => workspace).count798799total_hosts += hosts800total_services += services801total_vulns += vulns802total_notes += notes803total_creds += creds804total_tickets += kerbs805806tbl << [807current_workspace.id == workspace.id,808workspace.id,809workspace.name,810hosts.to_fs(:delimited),811services.to_fs(:delimited),812hosts > 0 ? (services.to_f / hosts).truncate(2) : 0,813vulns.to_fs(:delimited),814hosts > 0 ? (vulns.to_f / hosts).truncate(2) : 0,815notes.to_fs(:delimited),816creds.to_fs(:delimited),817kerbs.to_fs(:delimited)818]819end820821# total row822tbl << [823"",824"Total",825ordered_workspaces.length.to_fs(:delimited),826total_hosts.to_fs(:delimited),827total_services.to_fs(:delimited),828total_hosts > 0 ? (total_services.to_f / total_hosts).truncate(2) : 0,829total_vulns,830total_hosts > 0 ? (total_vulns.to_f / total_hosts).truncate(2) : 0,831total_notes,832total_creds.to_fs(:delimited),833total_tickets.to_fs(:delimited)834]835836print_line tbl.to_s837end838839def cmd_services(*args)840return unless active?841mode = :search842onlyup = false843output_file = nil844set_rhosts = false845col_search = ['port', 'proto', 'name', 'state', 'info']846847names = nil848order_by = nil849proto = nil850host_ranges = []851port_ranges = []852rhosts = []853delete_count = 0854search_term = nil855opts = {}856857@@services_opts.parse(args) do |opt, idx, val|858case opt859when '-a', '--add'860mode = :add861when '-d', '--delete'862mode = :delete863when '-U', '--update'864mode = :update865when '-u', '--up'866onlyup = true867when '-c'868list = val869if(!list)870print_error("Invalid column list")871return872end873col_search = list.strip().split(",")874col_search.each { |c|875if not @@services_columns.include? c876print_error("Invalid column list. Possible values are (#{@@services_columns.join("|")})")877return878end879}880when '-p'881unless (arg_port_range(val, port_ranges, true))882return883end884when '-r'885proto = val886if (!proto)887print_status("Invalid protocol")888return889end890proto = proto.strip891when '-s'892namelist = val893if (!namelist)894print_error("Invalid name list")895return896end897names = namelist.strip().split(",")898when '-o'899output_file = val900if (!output_file)901print_error("Invalid output filename")902return903end904output_file = ::File.expand_path(output_file)905when '-O'906if (order_by = val.to_i - 1) < 0907print_error('Please specify a column number starting from 1')908return909end910when '-R', '--rhosts'911set_rhosts = true912when '-S', '--search'913search_term = val914opts[:search_term] = search_term915when '-h', '--help'916cmd_services_help917return918else919# Anything that wasn't an option is a host to search for920unless (arg_host_range(val, host_ranges))921return922end923end924end925926ports = port_ranges.flatten.uniq927928if mode == :add929# Can only deal with one port and one service name at a time930# right now. Them's the breaks.931if ports.length != 1932print_error("Exactly one port required")933return934end935if host_ranges.empty?936print_error("Host address or range required")937return938end939host_ranges.each do |range|940range.each do |addr|941info = {942:host => addr,943:port => ports.first.to_i944}945info[:proto] = proto.downcase if proto946info[:name] = names.first.downcase if names and names.first947948svc = framework.db.find_or_create_service(info)949print_status("Time: #{svc.created_at} Service: host=#{svc.host.address} port=#{svc.port} proto=#{svc.proto} name=#{svc.name}")950end951end952return953end954955# If we got here, we're searching. Delete implies search956col_names = @@services_columns957if col_search958col_names = col_search959end960tbl = Rex::Text::Table.new({961'Header' => "Services",962'Columns' => ['host'] + col_names,963'SortIndex' => order_by964})965966# Sentinel value meaning all967host_ranges.push(nil) if host_ranges.empty?968ports = nil if ports.empty?969matched_service_ids = []970971each_host_range_chunk(host_ranges) do |host_search|972next if host_search && host_search.empty?973opts[:workspace] = framework.db.workspace974opts[:hosts] = {address: host_search} if !host_search.nil?975opts[:port] = ports if ports976framework.db.services(opts).each do |service|977978unless service.state == 'open'979next if onlyup980end981982host = service.host983matched_service_ids << service.id984985if mode == :update986service.name = names.first if names987service.proto = proto if proto988service.port = ports.first if ports989framework.db.update_service(service.as_json.symbolize_keys)990end991992columns = [host.address] + col_names.map { |n| service[n].to_s || "" }993tbl << columns994if set_rhosts995addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address )996rhosts << addr997end998end999end10001001if (mode == :delete)1002result = framework.db.delete_service(ids: matched_service_ids)1003delete_count += result.size1004end10051006if (output_file == nil)1007print_line(tbl.to_s)1008else1009# create the output file1010::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) }1011print_status("Wrote services to #{output_file}")1012end10131014# Finally, handle the case where the user wants the resulting list1015# of hosts to go into RHOSTS.1016set_rhosts_from_addrs(rhosts.uniq) if set_rhosts10171018print_status("Deleted #{delete_count} services") if delete_count > 010191020end10211022#1023# Tab completion for the vulns command1024#1025# @param str [String] the string currently being typed before tab was hit1026# @param words [Array<String>] the previously completed words on the command line. words is always1027# at least 1 when tab completion has reached this stage since the command itself has been completed1028def cmd_vulns_tabs(str, words)1029if words.length == 11030return @@vulns_opts.option_keys.select { |opt| opt.start_with?(str) }1031end1032case words[-1]1033when '-o', '--output'1034return tab_complete_filenames(str, words)1035end1036end10371038def cmd_vulns_help1039print_line "Print all vulnerabilities in the database"1040print_line1041print_line "Usage: vulns [addr range]"1042print_line1043print @@vulns_opts.usage1044print_line1045print_line "Examples:"1046print_line " vulns -p 1-65536 # only vulns with associated services"1047print_line " vulns -p 1-65536 -s http # identified as http on any port"1048print_line1049end10501051@@vulns_opts = Rex::Parser::Arguments.new(1052[ '-h', '--help' ] => [ false, 'Show this help information.' ],1053[ '-o', '--output' ] => [ true, 'Send output to a file in csv format.', '<filename>' ],1054[ '-p', '--port' ] => [ true, 'List vulns matching this port spec.', '<port>' ],1055[ '-s', '--service' ] => [ true, 'List vulns matching these service names.', '<name>' ],1056[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],1057[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],1058[ '-i', '--info' ] => [ false, 'Display vuln information.' ],1059[ '-d', '--delete' ] => [ false, 'Delete vulnerabilities. Not officially supported.' ]1060)10611062def cmd_vulns(*args)1063return unless active?10641065default_columns = ['Timestamp', 'Host', 'Name', 'References']1066host_ranges = []1067port_ranges = []1068svcs = []1069rhosts = []10701071search_term = nil1072show_info = false1073set_rhosts = false1074output_file = nil1075delete_count = 010761077mode = nil10781079@@vulns_opts.parse(args) do |opt, idx, val|1080case opt1081when '-d', '--delete' # TODO: This is currently undocumented because it's not officially supported.1082mode = :delete1083when '-h', '--help'1084cmd_vulns_help1085return1086when '-o', '--output'1087output_file = val1088if output_file1089output_file = File.expand_path(output_file)1090else1091print_error("Invalid output filename")1092return1093end1094when '-p', '--port'1095unless (arg_port_range(val, port_ranges, true))1096return1097end1098when '-s', '--service'1099service = val1100if (!service)1101print_error("Argument required for -s")1102return1103end1104svcs = service.split(/[\s]*,[\s]*/)1105when '-R', '--rhosts'1106set_rhosts = true1107when '-S', '--search'1108search_term = val1109when '-i', '--info'1110show_info = true1111else1112# Anything that wasn't an option is a host to search for1113unless (arg_host_range(val, host_ranges))1114return1115end1116end1117end11181119if show_info1120default_columns << 'Information'1121end11221123# add sentinel value meaning all if empty1124host_ranges.push(nil) if host_ranges.empty?1125# normalize1126ports = port_ranges.flatten.uniq1127svcs.flatten!1128tbl = Rex::Text::Table.new(1129'Header' => 'Vulnerabilities',1130'Columns' => default_columns1131)11321133matched_vuln_ids = []1134vulns = []1135if host_ranges.compact.empty?1136vulns = framework.db.vulns({:search_term => search_term})1137else1138each_host_range_chunk(host_ranges) do |host_search|1139next if host_search && host_search.empty?11401141vulns.concat(framework.db.vulns({:hosts => { :address => host_search }, :search_term => search_term }))1142end1143end11441145vulns.each do |vuln|1146reflist = vuln.refs.map {|r| r.name}1147if (vuln.service)1148# Skip this one if the user specified a port and it1149# doesn't match.1150next unless ports.empty? or ports.include? vuln.service.port1151# Same for service names1152next unless svcs.empty? or svcs.include?(vuln.service.name)1153else1154# This vuln has no service, so it can't match1155next unless ports.empty? and svcs.empty?1156end11571158matched_vuln_ids << vuln.id11591160row = []1161row << vuln.created_at1162row << vuln.host.address1163row << vuln.name1164row << reflist.join(',')1165if show_info1166row << vuln.info1167end1168tbl << row11691170if set_rhosts1171addr = (vuln.host.scope.to_s != "" ? vuln.host.address + '%' + vuln.host.scope : vuln.host.address)1172rhosts << addr1173end1174end11751176if mode == :delete1177result = framework.db.delete_vuln(ids: matched_vuln_ids)1178delete_count = result.size1179end11801181if output_file1182File.write(output_file, tbl.to_csv)1183print_status("Wrote vulnerability information to #{output_file}")1184else1185print_line1186print_line(tbl.to_s)1187end11881189# Finally, handle the case where the user wants the resulting list1190# of hosts to go into RHOSTS.1191set_rhosts_from_addrs(rhosts.uniq) if set_rhosts11921193print_status("Deleted #{delete_count} vulnerabilities") if delete_count > 01194end11951196#1197# Tab completion for the notes command1198#1199# @param str [String] the string currently being typed before tab was hit1200# @param words [Array<String>] the previously completed words on the command line. words is always1201# at least 1 when tab completion has reached this stage since the command itself has been completed1202def cmd_notes_tabs(str, words)1203if words.length == 11204return @@notes_opts.option_keys.select { |opt| opt.start_with?(str) }1205end12061207case words[-1]1208when '-O', '--order'1209return []1210when '-o', '--output'1211return tab_complete_filenames(str, words)1212end12131214[]1215end12161217def cmd_notes_help1218print_line "Usage: notes [-h] [-t <type1,type2>] [-n <data string>] [-a] [addr range]"1219print_line1220print @@notes_opts.usage1221print_line1222print_line "Examples:"1223print_line " notes --add -t apps -n 'winzip' 10.1.1.34 10.1.20.41"1224print_line " notes -t smb.fingerprint 10.1.1.34 10.1.20.41"1225print_line " notes -S 'nmap.nse.(http|rtsp)'"1226print_line1227end12281229@@notes_opts = Rex::Parser::Arguments.new(1230[ '-a', '--add' ] => [ false, 'Add a note to the list of addresses, instead of listing.' ],1231[ '-d', '--delete' ] => [ false, 'Delete the notes instead of searching.' ],1232[ '-h', '--help' ] => [ false, 'Show this help information.' ],1233[ '-n', '--note' ] => [ true, 'Set the data for a new note (only with -a).', '<note>' ],1234[ '-O', '--order' ] => [ true, 'Order rows by specified column number.', '<column id>' ],1235[ '-o', '--output' ] => [ true, 'Save the notes to a csv file.', '<filename>' ],1236[ '-R', '--rhosts' ] => [ false, 'Set RHOSTS from the results of the search.' ],1237[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],1238[ '-t', '--type' ] => [ true, 'Search for a list of types, or set single type for add.', '<type1,type2>' ],1239[ '-u', '--update' ] => [ false, 'Update a note. Not officially supported.' ]1240)12411242def cmd_notes(*args)1243return unless active?1244::ApplicationRecord.connection_pool.with_connection {1245mode = :search1246data = nil1247types = nil1248set_rhosts = false12491250host_ranges = []1251rhosts = []1252search_term = nil1253output_file = nil1254delete_count = 01255order_by = nil12561257@@notes_opts.parse(args) do |opt, idx, val|1258case opt1259when '-a', '--add'1260mode = :add1261when '-d', '--delete'1262mode = :delete1263when '-n', '--note'1264data = val1265if(!data)1266print_error("Can't make a note with no data")1267return1268end1269when '-t', '--type'1270typelist = val1271if(!typelist)1272print_error("Invalid type list")1273return1274end1275types = typelist.strip().split(",")1276when '-R', '--rhosts'1277set_rhosts = true1278when '-S', '--search'1279search_term = val1280when '-o', '--output'1281output_file = val1282output_file = ::File.expand_path(output_file)1283when '-O'1284if (order_by = val.to_i - 1) < 01285print_error('Please specify a column number starting from 1')1286return1287end1288when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported.1289mode = :update1290when '-h', '--help'1291cmd_notes_help1292return1293else1294# Anything that wasn't an option is a host to search for1295unless (arg_host_range(val, host_ranges))1296return1297end1298end1299end13001301if mode == :add1302if host_ranges.compact.empty?1303print_error("Host address or range required")1304return1305end13061307if types.nil? || types.size != 11308print_error("Exactly one type is required")1309return1310end13111312if data.nil?1313print_error("Data required")1314return1315end13161317type = types.first1318host_ranges.each { |range|1319range.each { |addr|1320note = framework.db.find_or_create_note(host: addr, type: type, data: data)1321break if not note1322print_status("Time: #{note.created_at} Note: host=#{addr} type=#{note.ntype} data=#{note.data}")1323}1324}1325return1326end13271328if mode == :update1329if !types.nil? && types.size != 11330print_error("Exactly one type is required")1331return1332end13331334if types.nil? && data.nil?1335print_error("Update requires data or type")1336return1337end1338end13391340note_list = []1341if host_ranges.compact.empty?1342# No host specified - collect all notes1343opts = {search_term: search_term}1344opts[:ntype] = types if mode != :update && types && !types.empty?1345note_list = framework.db.notes(opts)1346else1347# Collect notes of specified hosts1348each_host_range_chunk(host_ranges) do |host_search|1349next if host_search && host_search.empty?13501351opts = {hosts: {address: host_search}, workspace: framework.db.workspace, search_term: search_term}1352opts[:ntype] = types if mode != :update && types && !types.empty?1353note_list.concat(framework.db.notes(opts))1354end1355end13561357# Now display them1358table = Rex::Text::Table.new(1359'Header' => 'Notes',1360'Indent' => 1,1361'Columns' => ['Time', 'Host', 'Service', 'Port', 'Protocol', 'Type', 'Data'],1362'SortIndex' => order_by1363)13641365matched_note_ids = []1366note_list.each do |note|1367if mode == :update1368begin1369update_opts = {id: note.id}1370unless types.nil?1371note.ntype = types.first1372update_opts[:ntype] = types.first1373end13741375unless data.nil?1376note.data = data1377update_opts[:data] = data1378end13791380framework.db.update_note(update_opts)1381rescue => e1382elog "There was an error updating note with ID #{note.id}: #{e.message}"1383next1384end1385end13861387matched_note_ids << note.id13881389row = []1390row << note.created_at13911392if note.host1393host = note.host1394row << host.address1395if set_rhosts1396addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address)1397rhosts << addr1398end1399else1400row << ''1401end14021403if note.service1404row << note.service.name || ''1405row << note.service.port || ''1406row << note.service.proto || ''1407else1408row << '' # For the Service field1409row << '' # For the Port field1410row << '' # For the Protocol field1411end14121413row << note.ntype1414row << note.data.inspect1415table << row1416end14171418if mode == :delete1419result = framework.db.delete_note(ids: matched_note_ids)1420delete_count = result.size1421end14221423if output_file1424save_csv_notes(output_file, table)1425else1426print_line1427print_line(table.to_s)1428end14291430# Finally, handle the case where the user wants the resulting list1431# of hosts to go into RHOSTS.1432set_rhosts_from_addrs(rhosts.uniq) if set_rhosts14331434print_status("Deleted #{delete_count} notes") if delete_count > 01435}1436end14371438def save_csv_notes(fpath, table)1439begin1440File.open(fpath, 'wb') do |f|1441f.write(table.to_csv)1442end1443print_status("Wrote notes to #{fpath}")1444rescue Errno::EACCES => e1445print_error("Unable to save notes. #{e.message}")1446end1447end14481449#1450# Tab completion for the loot command1451#1452# @param str [String] the string currently being typed before tab was hit1453# @param words [Array<String>] the previously completed words on the command line. words is always1454# at least 1 when tab completion has reached this stage since the command itself has been completed1455def cmd_loot_tabs(str, words)1456if words.length == 11457@@loot_opts.option_keys.select { |opt| opt.start_with?(str) }1458end1459end14601461def cmd_loot_help1462print_line "Usage: loot [options]"1463print_line " Info: loot [-h] [addr1 addr2 ...] [-t <type1,type2>]"1464print_line " Add: loot -f [fname] -i [info] -a [addr1 addr2 ...] -t [type]"1465print_line " Del: loot -d [addr1 addr2 ...]"1466print_line1467print @@loot_opts.usage1468print_line1469end14701471@@loot_opts = Rex::Parser::Arguments.new(1472[ '-a', '--add' ] => [ false, 'Add loot to the list of addresses, instead of listing.' ],1473[ '-d', '--delete' ] => [ false, 'Delete *all* loot matching host and type.' ],1474[ '-f', '--file' ] => [ true, 'File with contents of the loot to add.', '<filename>' ],1475[ '-i', '--info' ] => [ true, 'Info of the loot to add.', '<info>' ],1476[ '-t', '--type' ] => [ true, 'Search for a list of types.', '<type1,type2>' ],1477[ '-h', '--help' ] => [ false, 'Show this help information.' ],1478[ '-S', '--search' ] => [ true, 'Search string to filter by.', '<filter>' ],1479[ '-u', '--update' ] => [ false, 'Update loot. Not officially supported.' ]1480)14811482def cmd_loot(*args)1483return unless active?14841485mode = :search1486host_ranges = []1487types = nil1488delete_count = 01489search_term = nil1490file = nil1491name = nil1492info = nil1493filename = nil14941495@@loot_opts.parse(args) do |opt, idx, val|1496case opt1497when '-a', '--add'1498mode = :add1499when '-d', '--delete'1500mode = :delete1501when '-f', '--file'1502filename = val1503if(!filename)1504print_error("Can't make loot with no filename")1505return1506end1507if (!File.exist?(filename) or !File.readable?(filename))1508print_error("Can't read file")1509return1510end1511when '-i', '--info'1512info = val1513if(!info)1514print_error("Can't make loot with no info")1515return1516end1517when '-t', '--type'1518typelist = val1519if(!typelist)1520print_error("Invalid type list")1521return1522end1523types = typelist.strip().split(",")1524when '-S', '--search'1525search_term = val1526when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported.1527mode = :update1528when '-h', '--help'1529cmd_loot_help1530return1531else1532# Anything that wasn't an option is a host to search for1533unless (arg_host_range(val, host_ranges))1534return1535end1536end1537end15381539tbl = Rex::Text::Table.new({1540'Header' => "Loot",1541'Columns' => [ 'host', 'service', 'type', 'name', 'content', 'info', 'path' ],1542# For now, don't perform any word wrapping on the loot table as it breaks the workflow of1543# copying paths and pasting them into applications1544'WordWrap' => false,1545})15461547# Sentinel value meaning all1548host_ranges.push(nil) if host_ranges.empty?15491550if mode == :add1551if host_ranges.compact.empty?1552print_error('Address list required')1553return1554end1555if info.nil?1556print_error("Info required")1557return1558end1559if filename.nil?1560print_error("Loot file required")1561return1562end1563if types.nil? or types.size != 11564print_error("Exactly one loot type is required")1565return1566end1567type = types.first1568name = File.basename(filename)1569file = File.open(filename, "rb")1570contents = file.read1571host_ranges.each do |range|1572range.each do |host|1573lootfile = framework.db.find_or_create_loot(:type => type, :host => host, :info => info, :data => contents, :path => filename, :name => name)1574print_status("Added loot for #{host} (#{lootfile})")1575end1576end1577return1578end15791580matched_loot_ids = []1581loots = []1582if host_ranges.compact.empty?1583loots = loots + framework.db.loots(workspace: framework.db.workspace, search_term: search_term)1584else1585each_host_range_chunk(host_ranges) do |host_search|1586next if host_search && host_search.empty?15871588loots = loots + framework.db.loots(workspace: framework.db.workspace, hosts: { address: host_search }, search_term: search_term)1589end1590end15911592loots.each do |loot|1593row = []1594# TODO: This is just a temp implementation of update for the time being since it did not exist before.1595# It should be updated to not pass all of the attributes attached to the object, only the ones being updated.1596if mode == :update1597begin1598loot.info = info if info1599if types && types.size > 11600print_error "May only pass 1 type when performing an update."1601next1602end1603loot.ltype = types.first if types1604framework.db.update_loot(loot.as_json.symbolize_keys)1605rescue => e1606elog "There was an error updating loot with ID #{loot.id}: #{e.message}"1607next1608end1609end1610row.push (loot.host && loot.host.address) ? loot.host.address : ""1611if (loot.service)1612svc = (loot.service.name ? loot.service.name : "#{loot.service.port}/#{loot.service.proto}")1613row.push svc1614else1615row.push ""1616end1617row.push(loot.ltype)1618row.push(loot.name || "")1619row.push(loot.content_type)1620row.push(loot.info || "")1621row.push(loot.path)16221623tbl << row1624matched_loot_ids << loot.id1625end16261627if (mode == :delete)1628result = framework.db.delete_loot(ids: matched_loot_ids)1629delete_count = result.size1630end16311632print_line1633print_line(tbl.to_s)1634print_status("Deleted #{delete_count} loots") if delete_count > 01635end16361637# :category: Deprecated Commands1638def cmd_db_hosts_help; deprecated_help(:hosts); end1639# :category: Deprecated Commands1640def cmd_db_notes_help; deprecated_help(:notes); end1641# :category: Deprecated Commands1642def cmd_db_vulns_help; deprecated_help(:vulns); end1643# :category: Deprecated Commands1644def cmd_db_services_help; deprecated_help(:services); end1645# :category: Deprecated Commands1646def cmd_db_autopwn_help; deprecated_help; end1647# :category: Deprecated Commands1648def cmd_db_driver_help; deprecated_help; end16491650# :category: Deprecated Commands1651def cmd_db_hosts(*args); deprecated_cmd(:hosts, *args); end1652# :category: Deprecated Commands1653def cmd_db_notes(*args); deprecated_cmd(:notes, *args); end1654# :category: Deprecated Commands1655def cmd_db_vulns(*args); deprecated_cmd(:vulns, *args); end1656# :category: Deprecated Commands1657def cmd_db_services(*args); deprecated_cmd(:services, *args); end1658# :category: Deprecated Commands1659def cmd_db_autopwn(*args); deprecated_cmd; end16601661#1662# :category: Deprecated Commands1663#1664# This one deserves a little more explanation than standard deprecation1665# warning, so give the user a better understanding of what's going on.1666#1667def cmd_db_driver(*args)1668deprecated_cmd1669print_line1670print_line "Because Metasploit no longer supports databases other than the default"1671print_line "PostgreSQL, there is no longer a need to set the driver. Thus db_driver"1672print_line "is not useful and its functionality has been removed. Usually Metasploit"1673print_line "will already have connected to the database; check db_status to see."1674print_line1675cmd_db_status1676end16771678def cmd_db_import_tabs(str, words)1679tab_complete_filenames(str, words)1680end16811682def cmd_db_import_help1683print_line "Usage: db_import <filename> [file2...]"1684print_line1685print_line "Filenames can be globs like *.xml, or **/*.xml which will search recursively"1686print_line "Currently supported file types include:"1687print_line " Acunetix"1688print_line " Amap Log"1689print_line " Amap Log -m"1690print_line " Appscan"1691print_line " Burp Session XML"1692print_line " Burp Issue XML"1693print_line " CI"1694print_line " Foundstone"1695print_line " FusionVM XML"1696print_line " Group Policy Preferences Credentials"1697print_line " IP Address List"1698print_line " IP360 ASPL"1699print_line " IP360 XML v3"1700print_line " Libpcap Packet Capture"1701print_line " Masscan XML"1702print_line " Metasploit PWDump Export"1703print_line " Metasploit XML"1704print_line " Metasploit Zip Export"1705print_line " Microsoft Baseline Security Analyzer"1706print_line " NeXpose Simple XML"1707print_line " NeXpose XML Report"1708print_line " Nessus NBE Report"1709print_line " Nessus XML (v1)"1710print_line " Nessus XML (v2)"1711print_line " NetSparker XML"1712print_line " Nikto XML"1713print_line " Nmap XML"1714print_line " OpenVAS Report"1715print_line " OpenVAS XML (optional arguments -cert -dfn)"1716print_line " Outpost24 XML"1717print_line " Qualys Asset XML"1718print_line " Qualys Scan XML"1719print_line " Retina XML"1720print_line " Spiceworks CSV Export"1721print_line " Wapiti XML"1722print_line1723end17241725#1726# Generic import that automatically detects the file type1727#1728def cmd_db_import(*args)1729return unless active?1730openvas_cert = false1731openvas_dfn = false1732::ApplicationRecord.connection_pool.with_connection {1733if args.include?("-h") || ! (args && args.length > 0)1734cmd_db_import_help1735return1736end1737if args.include?("-dfn")1738openvas_dfn = true1739end1740if args.include?("-cert")1741openvas_cert = true1742end1743options = {:openvas_dfn => openvas_dfn, :openvas_cert => openvas_cert}1744args.each { |glob|1745next if (glob.include?("-cert") || glob.include?("-dfn"))1746files = ::Dir.glob(::File.expand_path(glob))1747if files.empty?1748print_error("No such file #{glob}")1749next1750end1751files.each { |filename|1752if (not ::File.readable?(filename))1753print_error("Could not read file #{filename}")1754next1755end1756begin1757warnings = 01758framework.db.import_file(:filename => filename, :options => options) do |type,data|1759case type1760when :debug1761print_error("DEBUG: #{data.inspect}")1762when :vuln1763inst = data[1] == 1 ? "instance" : "instances"1764print_status("Importing vulnerability '#{data[0]}' (#{data[1]} #{inst})")1765when :filetype1766print_status("Importing '#{data}' data")1767when :parser1768print_status("Import: Parsing with '#{data}'")1769when :address1770print_status("Importing host #{data}")1771when :service1772print_status("Importing service #{data}")1773when :msf_loot1774print_status("Importing loot #{data}")1775when :msf_task1776print_status("Importing task #{data}")1777when :msf_report1778print_status("Importing report #{data}")1779when :pcap_count1780print_status("Import: #{data} packets processed")1781when :record_count1782print_status("Import: #{data[1]} records processed")1783when :warning1784print_error1785data.split("\n").each do |line|1786print_error(line)1787end1788print_error1789warnings += 11790end1791end1792print_status("Successfully imported #{filename}")17931794print_error("Please note that there were #{warnings} warnings") if warnings > 11795print_error("Please note that there was one warning") if warnings == 117961797rescue Msf::DBImportError => e1798print_error("Failed to import #{filename}: #{$!}")1799elog("Failed to import #{filename}", error: e)1800dlog("Call stack: #{$@.join("\n")}", LEV_3)1801next1802rescue REXML::ParseException => e1803print_error("Failed to import #{filename} due to malformed XML:")1804print_error("#{e.class}: #{e}")1805elog("Failed to import #{filename}", error: e)1806dlog("Call stack: #{$@.join("\n")}", LEV_3)1807next1808end1809}1810}1811}1812end18131814def cmd_db_export_help1815# Like db_hosts and db_services, this creates a list of columns, so1816# use its -h1817cmd_db_export("-h")1818end18191820#1821# Export an XML1822#1823def cmd_db_export(*args)1824return unless active?1825::ApplicationRecord.connection_pool.with_connection {18261827export_formats = %W{xml pwdump}1828format = 'xml'1829output = nil18301831while (arg = args.shift)1832case arg1833when '-h','--help'1834print_line "Usage:"1835print_line " db_export -f <format> [filename]"1836print_line " Format can be one of: #{export_formats.join(", ")}"1837when '-f','--format'1838format = args.shift.to_s.downcase1839else1840output = arg1841end1842end18431844if not output1845print_error("No output file was specified")1846return1847end18481849if not export_formats.include?(format)1850print_error("Unsupported file format: #{format}")1851print_error("Unsupported file format: '#{format}'. Must be one of: #{export_formats.join(", ")}")1852return1853end18541855print_status("Starting export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...")1856framework.db.run_db_export(output, format)1857print_status("Finished export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...")1858}1859end18601861def find_nmap_path1862Rex::FileUtils.find_full_path("nmap") || Rex::FileUtils.find_full_path("nmap.exe")1863end18641865#1866# Import Nmap data from a file1867#1868def cmd_db_nmap(*args)1869return unless active?1870::ApplicationRecord.connection_pool.with_connection {1871if (args.length == 0)1872print_status("Usage: db_nmap [--save | [--help | -h]] [nmap options]")1873return1874end18751876save = false1877arguments = []1878while (arg = args.shift)1879case arg1880when '--save'1881save = true1882when '--help', '-h'1883cmd_db_nmap_help1884return1885else1886arguments << arg1887end1888end18891890nmap = find_nmap_path1891unless nmap1892print_error("The nmap executable could not be found")1893return1894end18951896fd = Rex::Quickfile.new(['msf-db-nmap-', '.xml'], Msf::Config.local_directory)18971898begin1899# When executing native Nmap in Cygwin, expand the Cygwin path to a Win32 path1900if(Rex::Compat.is_cygwin and nmap =~ /cygdrive/)1901# Custom function needed because cygpath breaks on 8.3 dirs1902tout = Rex::Compat.cygwin_to_win32(fd.path)1903arguments.push('-oX', tout)1904else1905arguments.push('-oX', fd.path)1906end19071908run_nmap(nmap, arguments)19091910framework.db.import_nmap_xml_file(:filename => fd.path)19111912print_status("Saved NMAP XML results to #{fd.path}") if save1913ensure1914fd.close1915fd.unlink unless save1916end1917}1918end19191920def cmd_db_nmap_help1921nmap = find_nmap_path1922unless nmap1923print_error("The nmap executable could not be found")1924return1925end19261927stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help')19281929stdout.each_line do |out_line|1930next if out_line.strip.empty?1931print_status(out_line.strip)1932end19331934stderr.each_line do |err_line|1935next if err_line.strip.empty?1936print_error(err_line.strip)1937end1938end19391940def cmd_db_nmap_tabs(str, words)1941nmap = find_nmap_path1942unless nmap1943return1944end19451946stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help')1947tabs = []1948stdout.each_line do |out_line|1949if out_line.strip.starts_with?('-')1950tabs.push(out_line.strip.split(':').first)1951end1952end19531954stderr.each_line do |err_line|1955next if err_line.strip.empty?1956print_error(err_line.strip)1957end19581959return tabs1960end19611962#1963# Database management1964#1965def db_check_driver1966unless framework.db.driver1967print_error("No database driver installed.")1968return false1969end1970true1971end19721973#1974# Is everything working?1975#1976def cmd_db_status(*args)1977return if not db_check_driver19781979if framework.db.connection_established?1980print_connection_info1981else1982print_status("#{framework.db.driver} selected, no connection")1983end1984end198519861987def cmd_db_connect_help1988print_line(" USAGE:")1989print_line(" * Postgres Data Service:")1990print_line(" db_connect <user:[pass]>@<host:[port]>/<database>")1991print_line(" Examples:")1992print_line(" db_connect user@metasploit3")1993print_line(" db_connect user:[email protected]/metasploit3")1994print_line(" db_connect user:[email protected]:1500/metasploit3")1995print_line(" db_connect -y [path/to/database.yml]")1996print_line(" ")1997print_line(" * HTTP Data Service:")1998print_line(" db_connect [options] <http|https>://<host:[port]>")1999print_line(" Examples:")2000print_line(" db_connect http://localhost:8080")2001print_line(" db_connect http://my-super-msf-data.service.com")2002print_line(" db_connect -c ~/cert.pem -t 6a7a74c1a5003802c955ead1bbddd4ab1b05a7f2940b4732d34bfc555bc6e1c5d7611a497b29e8f0 https://localhost:8080")2003print_line(" NOTE: You must be connected to a Postgres data service in order to successfully connect to a HTTP data service.")2004print_line(" ")2005print_line(" Persisting Connections:")2006print_line(" db_connect --name <name to save connection as> [options] <address>")2007print_line(" Examples:")2008print_line(" Saving: db_connect --name LA-server http://123.123.123.45:1234")2009print_line(" Connecting: db_connect LA-server")2010print_line(" ")2011print_line(" OPTIONS:")2012print_line(" -l,--list-services List the available data services that have been previously saved.")2013print_line(" -y,--yaml Connect to the data service specified in the provided database.yml file.")2014print_line(" -n,--name Name used to store the connection. Providing an existing name will overwrite the settings for that connection.")2015print_line(" -c,--cert Certificate file matching the remote data server's certificate. Needed when using self-signed SSL cert.")2016print_line(" -t,--token The API token used to authenticate to the remote data service.")2017print_line(" --skip-verify Skip validating authenticity of server's certificate (NOT RECOMMENDED).")2018print_line("")2019end20202021def cmd_db_connect(*args)2022return if not db_check_driver20232024opts = {}2025while (arg = args.shift)2026case arg2027when '-h', '--help'2028cmd_db_connect_help2029return2030when '-y', '--yaml'2031opts[:yaml_file] = args.shift2032when '-c', '--cert'2033opts[:cert] = args.shift2034when '-t', '--token'2035opts[:api_token] = args.shift2036when '-l', '--list-services'2037list_saved_data_services2038return2039when '-n', '--name'2040opts[:name] = args.shift2041if opts[:name] =~ /\/|\[|\]/2042print_error "Provided name contains an invalid character. Aborting connection."2043return2044end2045when '--skip-verify'2046opts[:skip_verify] = true2047else2048found_name = ::Msf::DbConnector.data_service_search(name: arg)2049if found_name2050opts = ::Msf::DbConnector.load_db_config(found_name)2051else2052opts[:url] = arg2053end2054end2055end20562057if !opts[:url] && !opts[:yaml_file]2058print_error 'A URL or saved data service name is required.'2059print_line2060cmd_db_connect_help2061return2062end20632064if opts[:url] =~ /http/2065new_conn_type = 'http'2066else2067new_conn_type = framework.db.driver2068end20692070# Currently only able to be connected to one DB at a time2071if framework.db.connection_established?2072# But the http connection still requires a local database to support AR, so we have to allow that2073# Don't allow more than one HTTP service, though2074if new_conn_type != 'http' || framework.db.get_services_metadata.count >= 22075print_error('Connection already established. Only one connection is allowed at a time.')2076print_error('Run db_disconnect first if you wish to connect to a different data service.')2077print_line2078print_line 'Current connection information:'2079print_connection_info2080return2081end2082end20832084result = Msf::DbConnector.db_connect(framework, opts)2085if result[:error]2086print_error result[:error]2087return2088end20892090if result[:result]2091print_status result[:result]2092end2093if framework.db.active2094name = opts[:name]2095if !name || name.empty?2096if found_name2097name = found_name2098elsif result[:data_service_name]2099name = result[:data_service_name]2100else2101name = Rex::Text.rand_text_alphanumeric(8)2102end2103end21042105save_db_to_config(framework.db, name)2106@current_data_service = name2107end2108end21092110def cmd_db_disconnect_help2111print_line "Usage:"2112print_line " db_disconnect Temporarily disconnects from the currently configured dataservice."2113print_line " db_disconnect --clear Clears the default dataservice that msfconsole will use when opened."2114print_line2115end21162117def cmd_db_disconnect(*args)2118return if not db_check_driver21192120if args[0] == '-h' || args[0] == '--help'2121cmd_db_disconnect_help2122return2123elsif args[0] == '-c' || args[0] == '--clear'2124clear_default_db2125return2126end21272128previous_name = framework.db.name2129result = Msf::DbConnector.db_disconnect(framework)21302131if result[:error]2132print_error "Unable to disconnect from the data service: #{@current_data_service}"2133print_error result[:error]2134elsif result[:old_data_service_name].nil?2135print_error 'Not currently connected to a data service.'2136else2137print_line "Successfully disconnected from the data service: #{previous_name}."2138@current_data_service = result[:data_service_name]2139if @current_data_service2140print_line "Now connected to: #{@current_data_service}."2141end2142end2143end21442145def cmd_db_rebuild_cache(*args)2146print_line "This command is deprecated with Metasploit 5"2147end21482149def cmd_db_save_help2150print_line "Usage: db_save"2151print_line2152print_line "Save the current data service connection as the default to reconnect on startup."2153print_line2154end21552156def cmd_db_save(*args)2157while (arg = args.shift)2158case arg2159when '-h', '--help'2160cmd_db_save_help2161return2162end2163end21642165if !framework.db.active || !@current_data_service2166print_error "Not currently connected to a data service that can be saved."2167return2168end21692170begin2171Msf::Config.save(DB_CONFIG_PATH => { 'default_db' => @current_data_service })2172print_line "Successfully saved data service as default: #{@current_data_service}"2173rescue ArgumentError => e2174print_error e.message2175end2176end21772178def save_db_to_config(database, database_name)2179if database_name =~ /\/|\[|\]/2180raise ArgumentError, 'Data service name contains an invalid character.'2181end2182config_path = "#{DB_CONFIG_PATH}/#{database_name}"2183config_opts = {}2184if !database.is_local?2185begin2186config_opts['url'] = database.endpoint2187if database.https_opts2188config_opts['cert'] = database.https_opts[:cert] if database.https_opts[:cert]2189config_opts['skip_verify'] = true if database.https_opts[:skip_verify]2190end2191if database.api_token2192config_opts['api_token'] = database.api_token2193end2194Msf::Config.save(config_path => config_opts)2195rescue => e2196print_error "There was an error saving the data service configuration: #{e.message}"2197end2198else2199url = Msf::DbConnector.build_postgres_url2200config_opts['url'] = url2201Msf::Config.save(config_path => config_opts)2202end2203end22042205def cmd_db_remove_help2206print_line "Usage: db_remove <name>"2207print_line2208print_line "Delete the specified saved data service."2209print_line2210end22112212def cmd_db_remove(*args)2213if args[0] == '-h' || args[0] == '--help' || args[0].nil? || args[0].empty?2214cmd_db_remove_help2215return2216end2217delete_db_from_config(args[0])2218end22192220def delete_db_from_config(db_name)2221conf = Msf::Config.load2222db_path = "#{DB_CONFIG_PATH}/#{db_name}"2223if conf[db_path]2224clear_default_db if conf[DB_CONFIG_PATH]['default_db'] && conf[DB_CONFIG_PATH]['default_db'] == db_name2225Msf::Config.delete_group(db_path)2226print_line "Successfully deleted data service: #{db_name}"2227else2228print_line "Unable to locate saved data service with name #{db_name}."2229end2230end22312232def clear_default_db2233conf = Msf::Config.load2234if conf[DB_CONFIG_PATH] && conf[DB_CONFIG_PATH]['default_db']2235updated_opts = conf[DB_CONFIG_PATH]2236updated_opts.delete('default_db')2237Msf::Config.save(DB_CONFIG_PATH => updated_opts)2238print_line "Cleared the default data service."2239else2240print_line "No default data service was configured."2241end2242end22432244def db_find_tools(tools)2245missed = []2246tools.each do |name|2247if(! Rex::FileUtils.find_full_path(name))2248missed << name2249end2250end2251if(not missed.empty?)2252print_error("This database command requires the following tools to be installed: #{missed.join(", ")}")2253return2254end2255true2256end22572258#######2259private22602261def run_nmap(nmap, arguments, use_sudo: false)2262print_warning('Running Nmap with sudo') if use_sudo2263begin2264nmap_pipe = use_sudo ? ::Open3::popen3('sudo', nmap, *arguments) : ::Open3::popen3(nmap, *arguments)2265temp_nmap_threads = []2266temp_nmap_threads << framework.threads.spawn("db_nmap-Stdout", false, nmap_pipe[1]) do |np_1|2267np_1.each_line do |nmap_out|2268next if nmap_out.strip.empty?2269print_status("Nmap: #{nmap_out.strip}")2270end2271end22722273temp_nmap_threads << framework.threads.spawn("db_nmap-Stderr", false, nmap_pipe[2]) do |np_2|22742275np_2.each_line do |nmap_err|2276next if nmap_err.strip.empty?2277print_status("Nmap: '#{nmap_err.strip}'")2278# Check if the stderr text includes 'root', this only happens if the scan requires root privileges2279if nmap_err =~ /requires? root privileges/ or2280nmap_err.include? 'only works if you are root' or nmap_err =~ /requires? raw socket access/2281return run_nmap(nmap, arguments, use_sudo: true) unless use_sudo2282end2283end2284end22852286temp_nmap_threads.map { |t| t.join rescue nil }2287nmap_pipe.each { |p| p.close rescue nil }2288rescue ::IOError2289end2290end22912292#######22932294def print_connection_info2295cdb = ''2296if framework.db.driver == 'http'2297cdb = framework.db.name2298else2299::ApplicationRecord.connection_pool.with_connection do |conn|2300if conn.respond_to?(:current_database)2301cdb = conn.current_database2302end2303end2304end2305output = "Connected to #{cdb}. Connection type: #{framework.db.driver}."2306output += " Connection name: #{@current_data_service}." if @current_data_service2307print_status(output)2308end23092310def list_saved_data_services2311conf = Msf::Config.load2312default = nil2313tbl = Rex::Text::Table.new({2314'Header' => 'Data Services',2315'Columns' => ['current', 'name', 'url', 'default?'],2316'SortIndex' => 12317})23182319conf.each_pair do |k,v|2320if k =~ /#{DB_CONFIG_PATH}/2321default = v['default_db'] if v['default_db']2322name = k.split('/').last2323next if name == 'database' # Data service information is not stored in 'framework/database', just metadata2324url = v['url']2325current = ''2326current = '*' if name == @current_data_service2327default_output = ''2328default_output = '*' if name == default2329line = [current, name, url, default_output]2330tbl << line2331end2332end2333print_line2334print_line tbl.to_s2335end23362337def print_msgs(status_msg, error_msg)2338status_msg.each do |s|2339print_status(s)2340end23412342error_msg.each do |e|2343print_error(e)2344end2345end23462347end23482349end end end end235023512352