# -*- coding: binary -*- require 'json' require 'rexml/document' require 'rex/parser/nmap_xml' require 'msf/core/db_export' require 'metasploit/framework/data_service' require 'metasploit/framework/data_service/remote/http/core' module Msf module Ui module Console module CommandDispatcher class Db require 'tempfile' include Msf::Ui::Console::CommandDispatcher include Msf::Ui::Console::CommandDispatcher::Common # # The dispatcher's name. # def name "Database Backend" end # # Returns the hash of commands supported by this dispatcher. # def commands base = { "db_connect" => "Connect to an existing database", "db_disconnect" => "Disconnect from the current database instance", "db_status" => "Show the current database status", } more = { "workspace" => "Switch between database workspaces", "hosts" => "List all hosts in the database", "services" => "List all services in the database", "vulns" => "List all vulnerabilities in the database", "notes" => "List all notes in the database", "loot" => "List all loot in the database", "db_import" => "Import a scan result file (filetype will be auto-detected)", "db_export" => "Export a file containing the contents of the database", "db_nmap" => "Executes nmap and records the output automatically", "db_rebuild_cache" => "Rebuilds the database-stored module cache", "data_services" => "Command to add, list and set a data service", } # Always include commands that only make sense when connected. # This avoids the problem of them disappearing unexpectedly if the # database dies or times out. See #1923 base.merge(more) end def deprecated_commands [ "db_autopwn", "db_driver", "db_hosts", "db_notes", "db_services", "db_vulns", ] end # # Returns true if the db is connected, prints an error and returns # false if not. # # All commands that require an active database should call this before # doing anything. # def active? if not framework.db.active print_error("Database not connected") return false end true end def cmd_data_services(*args) while (arg = args.shift) case arg when '-h', '--help' data_service_help return when '-a', '--add' add_data_service(*args) return when '-s', '--set' set_data_service(args.shift) return end end list_data_services end def cmd_workspace_help print_line "Usage:" print_line " workspace List workspaces" print_line " workspace -v List workspaces verbosely" print_line " workspace [name] Switch workspace" print_line " workspace -a [name] ... Add workspace(s)" print_line " workspace -d [name] ... Delete workspace(s)" print_line " workspace -D Delete all workspaces" print_line " workspace -r Rename workspace" print_line " workspace -h Show this help information" print_line end def cmd_workspace(*args) return unless active? search_term = nil while (arg = args.shift) case arg when '-h','--help' cmd_workspace_help return when '-a','--add' adding = true when '-d','--del' deleting = true when '-D','--delete-all' delete_all = true when '-r','--rename' renaming = true when '-v','--verbose' verbose = true when '-S', '--search' search_term = args.shift else names ||= [] names << arg end end if adding and names # Add workspaces wspace = nil names.each do |name| wspace = framework.db.workspaces(name: name).first if wspace print_status("Workspace '#{wspace.name}' already existed, switching to it.") else wspace = framework.db.add_workspace(name) print_status("Added workspace: #{wspace.name}") end end framework.db.workspace = wspace print_status("Workspace: #{framework.db.workspace.name}") elsif deleting and names ws_ids_to_delete = [] starting_ws = framework.db.workspace names.uniq.each do |n| ws = framework.db.workspaces(name: n).first ws_ids_to_delete << ws.id if ws end if ws_ids_to_delete.count > 0 deleted = framework.db.delete_workspaces(ids: ws_ids_to_delete) process_deleted_workspaces(deleted, starting_ws) else print_status("No workspaces matching the given name(s) were found.") end elsif delete_all ws_ids_to_delete = [] starting_ws = framework.db.workspace framework.db.workspaces.each do |ws| ws_ids_to_delete << ws.id end deleted = framework.db.delete_workspaces(ids: ws_ids_to_delete) process_deleted_workspaces(deleted, starting_ws) elsif renaming if names.length != 2 print_error("Wrong number of arguments to rename") return end ws_to_update = framework.db.find_workspace(names.first) unless ws_to_update print_error("Workspace '#{names.first}' does not exist") return end opts = { id: ws_to_update.id, name: names.last } begin if names.last == Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME print_error("Unable to rename a workspace to '#{Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME}'") return end updated_ws = framework.db.update_workspace(opts) if updated_ws framework.db.workspace = updated_ws if names.first == framework.db.workspace.name print_status("Renamed workspace '#{names.first}' to '#{updated_ws.name}'") else print_error "There was a problem updating the workspace. Setting to the default workspace." framework.db.workspace = framework.db.default_workspace return end if names.first == Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME print_status("Recreated default workspace") end rescue => e print_error "Failed to rename workspace: #{e.message}" e.backtrace.each { |line| print_error "#{line}"} end elsif names name = names.last # Switch workspace workspace = framework.db.find_workspace(name) if workspace framework.db.workspace = workspace print_status("Workspace: #{workspace.name}") else print_error("Workspace not found: #{name}") return end else current_workspace = framework.db.workspace unless verbose current = nil framework.db.workspaces.sort_by {|s| s.name}.each do |s| if s.name == current_workspace.name current = s.name else print_line(" #{s.name}") end end print_line("%red* #{current}%clr") unless current.nil? return end col_names = %w{current name hosts services vulns creds loots notes} tbl = Rex::Text::Table.new( 'Header' => 'Workspaces', 'Columns' => col_names, 'SortIndex' => -1, 'SearchTerm' => search_term ) framework.db.workspaces.each do |ws| tbl << [ current_workspace.name == ws.name ? '*' : '', ws.name, framework.db.hosts(ws.name).count, framework.db.services(ws.name).count, framework.db.vulns({workspace: ws.name}).count, framework.db.creds({workspace: ws.name}).count, framework.db.loots(ws.name).count, framework.db.notes({workspace: ws.name}).count ] end print_line print_line(tbl.to_s) end end def process_deleted_workspaces(deleted_workspaces, starting_ws) deleted_workspaces.each do |ws| print_status "Deleted workspace: #{ws.name}" if ws.name == Msf::DBManager::Workspace::DEFAULT_WORKSPACE_NAME framework.db.workspace = framework.db.default_workspace print_status 'Recreated the default workspace' elsif ws == starting_ws framework.db.workspace = framework.db.default_workspace print_status "Switched to workspace: #{framework.db.workspace.name}" end end end def cmd_workspace_tabs(str, words) return [] unless active? framework.db.workspaces.map { |s| s.name } if (words & ['-a','--add']).empty? end def cmd_hosts_help # This command does some lookups for the list of appropriate column # names, so instead of putting all the usage stuff here like other # help methods, just use it's "-h" so we don't have to recreating # that list cmd_hosts("-h") end # Changes the specified host data # # @param host_ranges - range of hosts to process # @param host_data - hash of host data to be updated def change_host_data(host_ranges, host_data) if !host_data || host_data.length != 1 print_error("A single key-value data hash is required to change the host data") return end attribute = host_data.keys[0] if host_ranges == [nil] print_error("In order to change the host #{attribute}, you must provide a range of hosts") return end each_host_range_chunk(host_ranges) do |host_search| break if !host_search.nil? && host_search.empty? framework.db.hosts(framework.db.workspace, false, host_search).each do |host| framework.db.update_host(host_data.merge(id: host.id)) framework.db.report_note(host: host.address, type: "host.#{attribute}", data: host_data[attribute]) end end end def add_host_tag(rws, tag_name) if rws == [nil] print_error("In order to add a tag, you must provide a range of hosts") return end opts = Hash.new() opts[:workspace] = framework.db.workspace opts[:tag_name] = tag_name rws.each do |rw| rw.each do |ip| opts[:ip] = ip framework.db.add_host_tag(opts) end end end def find_hosts_with_tag(workspace_id, host_address, tag_name) opts = Hash.new() opts[:workspace_id] = workspace_id opts[:host_address] = host_address opts[:tag_name] = tag_name framework.db.find_hosts_with_tag(opts) end def find_host_tags(workspace_id, host_address) opts = Hash.new() opts[:workspace_id] = workspace_id opts[:host_address] = host_address framework.db.find_host_tags(opts) end def delete_host_tag(rws, tag_name) opts = Hash.new() opts[:workspace] = framework.db.workspace opts[:tag_name] = tag_name if rws == [nil] framework.db.delete_host_tag(opts) else rws.each do |rw| rw.each do |ip| opts[:ip] = ip framework.db.delete_host_tag(opts) end end end end @@hosts_columns = [ 'address', 'mac', 'name', 'os_name', 'os_flavor', 'os_sp', 'purpose', 'info', 'comments'] def cmd_hosts(*args) return unless active? onlyup = false set_rhosts = false mode = [] delete_count = 0 rhosts = [] host_ranges = [] search_term = nil output = nil default_columns = [ 'address', 'arch', 'comm', 'comments', 'created_at', 'cred_count', 'detected_arch', 'exploit_attempt_count', 'host_detail_count', 'info', 'mac', 'name', 'note_count', 'os_family', 'os_flavor', 'os_lang', 'os_name', 'os_sp', 'purpose', 'scope', 'service_count', 'state', 'updated_at', 'virtual_host', 'vuln_count', 'workspace_id'] default_columns << 'tags' # Special case virtual_columns = [ 'svcs', 'vulns', 'workspace', 'tags' ] col_search = @@hosts_columns default_columns.delete_if {|v| (v[-2,2] == "id")} while (arg = args.shift) case arg when '-a','--add' mode << :add when '-d','--delete' mode << :delete when '-c','-C' list = args.shift if(!list) print_error("Invalid column list") return end col_search = list.strip().split(",") col_search.each { |c| if not default_columns.include?(c) and not virtual_columns.include?(c) all_columns = default_columns + virtual_columns print_error("Invalid column list. Possible values are (#{all_columns.join("|")})") return end } if (arg == '-C') @@hosts_columns = col_search end when '-u','--up' onlyup = true when '-o' output = args.shift when '-O' if (order_by = args.shift.to_i - 1) < 0 print_error('Please specify a column number starting from 1') return end when '-R', '--rhosts' set_rhosts = true when '-S', '--search' search_term = args.shift when '-i', '--info' mode << :new_info info_data = args.shift when '-n', '--name' mode << :new_name name_data = args.shift when '-m', '--comment' mode << :new_comment comment_data = args.shift when '-t', '--tag' mode << :tag tag_name = args.shift when '-h','--help' print_line "Usage: hosts [ options ] [addr1 addr2 ...]" print_line print_line "OPTIONS:" print_line " -a,--add Add the hosts instead of searching" print_line " -d,--delete Delete the hosts instead of searching" print_line " -c Only show the given columns (see list below)" print_line " -C Only show the given columns until the next restart (see list below)" print_line " -h,--help Show this help information" print_line " -u,--up Only show hosts which are up" print_line " -o Send output to a file in csv format" print_line " -O Order rows by specified column number" print_line " -R,--rhosts Set RHOSTS from the results of the search" print_line " -S,--search Search string to filter by" print_line " -i,--info Change the info of a host" print_line " -n,--name Change the name of a host" print_line " -m,--comment Change the comment of a host" print_line " -t,--tag Add or specify a tag to a range of hosts" print_line print_line "Available columns: #{default_columns.join(", ")}" print_line return else # Anything that wasn't an option is a host to search for unless (arg_host_range(arg, host_ranges)) return end end end if col_search col_names = col_search else col_names = default_columns + virtual_columns end mode << :search if mode.empty? if mode == [:add] host_ranges.each do |range| range.each do |address| host = framework.db.find_or_create_host(:host => address) print_status("Time: #{host.created_at} Host: host=#{host.address}") end end return end cp_hsh = {} col_names.map do |col| cp_hsh[col] = { 'MaxChar' => 52 } end # If we got here, we're searching. Delete implies search tbl = Rex::Text::Table.new( { 'Header' => "Hosts", 'Columns' => col_names, 'ColProps' => cp_hsh, 'SortIndex' => order_by }) # Sentinel value meaning all host_ranges.push(nil) if host_ranges.empty? case when mode == [:new_info] change_host_data(host_ranges, info: info_data) return when mode == [:new_name] change_host_data(host_ranges, name: name_data) return when mode == [:new_comment] change_host_data(host_ranges, comments: comment_data) return when mode == [:tag] begin add_host_tag(host_ranges, tag_name) rescue => e if e.message.include?('Validation failed') print_error(e.message) else raise e end end return when mode.include?(:tag) && mode.include?(:delete) delete_host_tag(host_ranges, tag_name) return end matched_host_ids = [] each_host_range_chunk(host_ranges) do |host_search| break if !host_search.nil? && host_search.empty? framework.db.hosts(framework.db.workspace, onlyup, host_search, search_term = search_term).each do |host| matched_host_ids << host.id columns = col_names.map do |n| # Deal with the special cases if virtual_columns.include?(n) case n when "svcs"; host.service_count when "vulns"; host.vuln_count when "workspace"; host.workspace.name when "tags" found_tags = find_host_tags(framework.db.workspace.id, host.address) tag_names = [] found_tags.each { |t| tag_names << t.name } found_tags * ", " end # Otherwise, it's just an attribute else host[n] || "" end end tbl << columns if set_rhosts addr = (host.scope ? host.address + '%' + host.scope : host.address) rhosts << addr end end if mode == [:delete] result = framework.db.delete_host(ids: matched_host_ids) delete_count += result.size end end if output print_status("Wrote hosts to #{output}") ::File.open(output, "wb") { |ofd| ofd.write(tbl.to_csv) } else print_line print_line(tbl.to_s) end # Finally, handle the case where the user wants the resulting list # of hosts to go into RHOSTS. set_rhosts_from_addrs(rhosts.uniq) if set_rhosts print_status("Deleted #{delete_count} hosts") if delete_count > 0 end def cmd_services_help # Like cmd_hosts, use "-h" instead of recreating the column list # here cmd_services("-h") end def cmd_services(*args) return unless active? mode = :search onlyup = false output_file = nil set_rhosts = false col_search = ['port', 'proto', 'name', 'state', 'info'] default_columns = [ 'created_at', 'info', 'name', 'port', 'proto', 'state', 'updated_at'] host_ranges = [] port_ranges = [] rhosts = [] delete_count = 0 search_term = nil opts = {} # option parsing while (arg = args.shift) case arg when '-a','--add' mode = :add when '-d','--delete' mode = :delete when '-U', '--update' mode = :update when '-u','--up' onlyup = true when '-c' list = args.shift if(!list) print_error("Invalid column list") return end col_search = list.strip().split(",") col_search.each { |c| if not default_columns.include? c print_error("Invalid column list. Possible values are (#{default_columns.join("|")})") return end } when '-p' unless (arg_port_range(args.shift, port_ranges, true)) return end when '-r' proto = args.shift if (!proto) print_status("Invalid protocol") return end proto = proto.strip when '-s' namelist = args.shift if (!namelist) print_error("Invalid name list") return end names = namelist.strip().split(",") when '-o' output_file = args.shift if (!output_file) print_error("Invalid output filename") return end output_file = ::File.expand_path(output_file) when '-O' if (order_by = args.shift.to_i - 1) < 0 print_error('Please specify a column number starting from 1') return end when '-R', '--rhosts' set_rhosts = true when '-S', '--search' search_term = args.shift opts[:search_term] = search_term when '-h','--help' print_line print_line "Usage: services [-h] [-u] [-a] [-r ] [-p ] [-s ] [-o ] [addr1 addr2 ...]" print_line print_line " -a,--add Add the services instead of searching" print_line " -d,--delete Delete the services instead of searching" print_line " -c Only show the given columns" print_line " -h,--help Show this help information" print_line " -s Name of the service to add" print_line " -p Search for a list of ports" print_line " -r Protocol type of the service being added [tcp|udp]" print_line " -u,--up Only show services which are up" print_line " -o Send output to a file in csv format" print_line " -O Order rows by specified column number" print_line " -R,--rhosts Set RHOSTS from the results of the search" print_line " -S,--search Search string to filter by" print_line " -U,--update Update data for existing service" print_line print_line "Available columns: #{default_columns.join(", ")}" print_line return else # Anything that wasn't an option is a host to search for unless (arg_host_range(arg, host_ranges)) return end end end ports = port_ranges.flatten.uniq if mode == :add # Can only deal with one port and one service name at a time # right now. Them's the breaks. if ports.length != 1 print_error("Exactly one port required") return end if host_ranges.empty? print_error("Host address or range required") return end host_ranges.each do |range| range.each do |addr| info = { :host => addr, :port => ports.first.to_i } info[:proto] = proto.downcase if proto info[:name] = names.first.downcase if names and names.first svc = framework.db.find_or_create_service(info) print_status("Time: #{svc.created_at} Service: host=#{svc.host.address} port=#{svc.port} proto=#{svc.proto} name=#{svc.name}") end end return end # If we got here, we're searching. Delete implies search col_names = default_columns if col_search col_names = col_search end tbl = Rex::Text::Table.new({ 'Header' => "Services", 'Columns' => ['host'] + col_names, 'SortIndex' => order_by }) # Sentinel value meaning all host_ranges.push(nil) if host_ranges.empty? ports = nil if ports.empty? matched_service_ids = [] each_host_range_chunk(host_ranges) do |host_search| break if !host_search.nil? && host_search.empty? opts[:addresses] = host_search opts[:port] = ports if ports framework.db.services(framework.db.workspace, opts).each do |service| host = service.host matched_service_ids << service.id if mode == :update service.name = names.first if names service.proto = proto if proto service.port = ports.first if ports framework.db.update_service(service.as_json.symbolize_keys) end columns = [host.address] + col_names.map { |n| service[n].to_s || "" } tbl << columns if set_rhosts addr = (host.scope ? host.address + '%' + host.scope : host.address ) rhosts << addr end end end if (mode == :delete) result = framework.db.delete_service(ids: matched_service_ids) delete_count += result.size end if (output_file == nil) print_line(tbl.to_s) else # create the output file ::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) } print_status("Wrote services to #{output_file}") end # Finally, handle the case where the user wants the resulting list # of hosts to go into RHOSTS. set_rhosts_from_addrs(rhosts.uniq) if set_rhosts print_status("Deleted #{delete_count} services") if delete_count > 0 end def cmd_vulns_help print_line "Print all vulnerabilities in the database" print_line print_line "Usage: vulns [addr range]" print_line print_line " -h,--help Show this help information" print_line " -o Send output to a file in csv format" print_line " -p,--port List vulns matching this port spec" print_line " -s List vulns matching these service names" print_line " -R,--rhosts Set RHOSTS from the results of the search" print_line " -S,--search Search string to filter by" print_line " -i,--info Display vuln information" print_line print_line "Examples:" print_line " vulns -p 1-65536 # only vulns with associated services" print_line " vulns -p 1-65536 -s http # identified as http on any port" print_line end def cmd_vulns(*args) return unless active? default_columns = ['Timestamp', 'Host', 'Name', 'References'] host_ranges = [] port_ranges = [] svcs = [] rhosts = [] search_term = nil show_info = false set_rhosts = false output_file = nil delete_count = 0 while (arg = args.shift) case arg # when '-a', '--add' # mode = :add when '-d', '--delete' # TODO: This is currently undocumented because it's not officially supported. mode = :delete when '-h', '--help' cmd_vulns_help return when '-o', '--output' output_file = args.shift if output_file output_file = File.expand_path(output_file) else print_error("Invalid output filename") return end when '-p', '--port' unless (arg_port_range(args.shift, port_ranges, true)) return end when '-s', '--service' service = args.shift if (!service) print_error("Argument required for -s") return end svcs = service.split(/[\s]*,[\s]*/) when '-R', '--rhosts' set_rhosts = true when '-S', '--search' search_term = args.shift when '-i', '--info' show_info = true else # Anything that wasn't an option is a host to search for unless (arg_host_range(arg, host_ranges)) return end end end if show_info default_columns << 'Information' end # add sentinel value meaning all if empty host_ranges.push(nil) if host_ranges.empty? # normalize ports = port_ranges.flatten.uniq svcs.flatten! tbl = Rex::Text::Table.new( 'Header' => 'Vulnerabilities', 'Columns' => default_columns ) matched_vuln_ids = [] vulns = [] if host_ranges.compact.empty? vulns = framework.db.vulns({:search_term => search_term}) else each_host_range_chunk(host_ranges) do |host_search| break if !host_search.nil? && host_search.empty? vulns.concat(framework.db.vulns({:hosts => { :address => host_search }, :search_term => search_term })) end end vulns.each do |vuln| reflist = vuln.refs.map {|r| r.name} if (vuln.service) # Skip this one if the user specified a port and it # doesn't match. next unless ports.empty? or ports.include? vuln.service.port # Same for service names next unless svcs.empty? or svcs.include?(vuln.service.name) else # This vuln has no service, so it can't match next unless ports.empty? and svcs.empty? end matched_vuln_ids << vuln.id row = [] row << vuln.created_at row << vuln.host.address row << vuln.name row << reflist.join(',') if show_info row << vuln.info end tbl << row if set_rhosts addr = (vuln.host.scope ? vuln.host.address + '%' + vuln.host.scope : vuln.host.address) rhosts << addr end end if mode == :delete result = framework.db.delete_vuln(ids: matched_vuln_ids) delete_count = result.size end if output_file File.write(output_file, tbl.to_csv) print_status("Wrote vulnerability information to #{output_file}") else print_line print_line(tbl.to_s) end # Finally, handle the case where the user wants the resulting list # of hosts to go into RHOSTS. set_rhosts_from_addrs(rhosts.uniq) if set_rhosts print_status("Deleted #{delete_count} vulnerabilities") if delete_count > 0 end def cmd_notes_help print_line "Usage: notes [-h] [-t ] [-n ] [-a] [addr range]" print_line print_line " -a,--add Add a note to the list of addresses, instead of listing" print_line " -d,--delete Delete the hosts instead of searching" print_line " -n,--note Set the data for a new note (only with -a)" print_line " -t,--type Search for a list of types, or set single type for add" print_line " -h,--help Show this help information" print_line " -R,--rhosts Set RHOSTS from the results of the search" print_line " -S,--search Search string to filter by" print_line " -o,--output Save the notes to a csv file" print_line " -O Order rows by specified column number" print_line print_line "Examples:" print_line " notes --add -t apps -n 'winzip' 10.1.1.34 10.1.20.41" print_line " notes -t smb.fingerprint 10.1.1.34 10.1.20.41" print_line " notes -S 'nmap.nse.(http|rtsp)'" print_line end def cmd_notes(*args) return unless active? ::ActiveRecord::Base.connection_pool.with_connection { mode = :search data = nil types = nil set_rhosts = false host_ranges = [] rhosts = [] search_term = nil output_file = nil delete_count = 0 while (arg = args.shift) case arg when '-a', '--add' mode = :add when '-d', '--delete' mode = :delete when '-n', '--note' data = args.shift if(!data) print_error("Can't make a note with no data") return end when '-t', '--type' typelist = args.shift if(!typelist) print_error("Invalid type list") return end types = typelist.strip().split(",") when '-R', '--rhosts' set_rhosts = true when '-S', '--search' search_term = args.shift when '-o', '--output' output_file = args.shift when '-O' if (order_by = args.shift.to_i - 1) < 0 print_error('Please specify a column number starting from 1') return end when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported. mode = :update when '-h', '--help' cmd_notes_help return else # Anything that wasn't an option is a host to search for unless (arg_host_range(arg, host_ranges)) return end end end if mode == :add if host_ranges.compact.empty? print_error("Host address or range required") return end if types.nil? || types.size != 1 print_error("Exactly one type is required") return end if data.nil? print_error("Data required") return end type = types.first host_ranges.each { |range| range.each { |addr| note = framework.db.find_or_create_note(host: addr, type: type, data: data) break if not note print_status("Time: #{note.created_at} Note: host=#{addr} type=#{note.ntype} data=#{note.data}") } } return end if mode == :update if !types.nil? && types.size != 1 print_error("Exactly one type is required") return end if types.nil? && data.nil? print_error("Update requires data or type") return end end note_list = [] if host_ranges.compact.empty? # No host specified - collect all notes opts = {search_term: search_term} opts[:ntype] = types if mode != :update && types && !types.empty? note_list = framework.db.notes(opts) else # Collect notes of specified hosts each_host_range_chunk(host_ranges) do |host_search| break if !host_search.nil? && host_search.empty? opts = {hosts: {address: host_search}, workspace: framework.db.workspace, search_term: search_term} opts[:ntype] = types if mode != :update && types && !types.empty? note_list.concat(framework.db.notes(opts)) end end # Now display them table = Rex::Text::Table.new( 'Header' => 'Notes', 'Indent' => 1, 'Columns' => ['Time', 'Host', 'Service', 'Port', 'Protocol', 'Type', 'Data'], 'SortIndex' => order_by ) matched_note_ids = [] note_list.each do |note| if mode == :update begin update_opts = {id: note.id} unless types.nil? note.ntype = types.first update_opts[:ntype] = types.first end unless data.nil? note.data = data update_opts[:data] = data end framework.db.update_note(update_opts) rescue => e elog "There was an error updating note with ID #{note.id}: #{e.message}" next end end matched_note_ids << note.id row = [] row << note.created_at if note.host host = note.host row << host.address if set_rhosts addr = (host.scope ? host.address + '%' + host.scope : host.address) rhosts << addr end else row << '' end if note.service row << note.service.name || '' row << note.service.port || '' row << note.service.proto || '' else row << '' # For the Service field row << '' # For the Port field row << '' # For the Protocol field end row << note.ntype row << note.data.inspect table << row end if mode == :delete result = framework.db.delete_note(ids: matched_note_ids) delete_count = result.size end if output_file save_csv_notes(output_file, table) else print_line print_line(table.to_s) end # Finally, handle the case where the user wants the resulting list # of hosts to go into RHOSTS. set_rhosts_from_addrs(rhosts.uniq) if set_rhosts print_status("Deleted #{delete_count} notes") if delete_count > 0 } end def save_csv_notes(fpath, table) begin File.open(fpath, 'wb') do |f| f.write(table.to_csv) end print_status("Wrote notes to #{fpath}") rescue Errno::EACCES => e print_error("Unable to save notes. #{e.message}") end end def cmd_loot_help print_line "Usage: loot " print_line " Info: loot [-h] [addr1 addr2 ...] [-t ]" print_line " Add: loot -f [fname] -i [info] -a [addr1 addr2 ...] -t [type]" print_line " Del: loot -d [addr1 addr2 ...]" print_line print_line " -a,--add Add loot to the list of addresses, instead of listing" print_line " -d,--delete Delete *all* loot matching host and type" print_line " -f,--file File with contents of the loot to add" print_line " -i,--info Info of the loot to add" print_line " -t Search for a list of types" print_line " -h,--help Show this help information" print_line " -S,--search Search string to filter by" print_line end def cmd_loot(*args) return unless active? mode = :search host_ranges = [] types = nil delete_count = 0 search_term = nil file = nil name = nil info = nil while (arg = args.shift) case arg when '-a','--add' mode = :add when '-d','--delete' mode = :delete when '-f','--file' filename = args.shift if(!filename) print_error("Can't make loot with no filename") return end if (!File.exist?(filename) or !File.readable?(filename)) print_error("Can't read file") return end when '-i','--info' info = args.shift if(!info) print_error("Can't make loot with no info") return end when '-t', '--type' typelist = args.shift if(!typelist) print_error("Invalid type list") return end types = typelist.strip().split(",") when '-S', '--search' search_term = args.shift when '-u', '--update' # TODO: This is currently undocumented because it's not officially supported. mode = :update when '-h','--help' cmd_loot_help return else # Anything that wasn't an option is a host to search for unless (arg_host_range(arg, host_ranges)) return end end end tbl = Rex::Text::Table.new({ 'Header' => "Loot", 'Columns' => [ 'host', 'service', 'type', 'name', 'content', 'info', 'path' ], }) # Sentinel value meaning all host_ranges.push(nil) if host_ranges.empty? if mode == :add if host_ranges.compact.empty? print_error('Address list required') return end if info.nil? print_error("Info required") return end if filename.nil? print_error("Loot file required") return end if types.nil? or types.size != 1 print_error("Exactly one loot type is required") return end type = types.first name = File.basename(filename) file = File.open(filename, "rb") contents = file.read host_ranges.each do |range| range.each do |host| lootfile = framework.db.find_or_create_loot(:type => type, :host => host, :info => info, :data => contents, :path => filename, :name => name) print_status("Added loot for #{host} (#{lootfile})") end end return end matched_loot_ids = [] loots = [] if host_ranges.compact.empty? loots = loots + framework.db.loots(framework.db.workspace, {:search_term => search_term}) else each_host_range_chunk(host_ranges) do |host_search| break if !host_search.nil? && host_search.empty? loots = loots + framework.db.loots(framework.db.workspace, { :hosts => { :address => host_search }, :search_term => search_term }) end end loots.each do |loot| row = [] # TODO: This is just a temp implementation of update for the time being since it did not exist before. # It should be updated to not pass all of the attributes attached to the object, only the ones being updated. if mode == :update begin loot.info = info if info if types && types.size > 1 print_error "May only pass 1 type when performing an update." next end loot.ltype = types.first if types framework.db.update_loot(loot.as_json.symbolize_keys) rescue => e elog "There was an error updating loot with ID #{loot.id}: #{e.message}" next end end row.push (loot.host && loot.host.address) ? loot.host.address : "" if (loot.service) svc = (loot.service.name ? loot.service.name : "#{loot.service.port}/#{loot.service.proto}") row.push svc else row.push "" end row.push(loot.ltype) row.push(loot.name || "") row.push(loot.content_type) row.push(loot.info || "") row.push(loot.path) tbl << row matched_loot_ids << loot.id end if (mode == :delete) result = framework.db.delete_loot(ids: matched_loot_ids) delete_count = result.size end print_line print_line(tbl.to_s) print_status("Deleted #{delete_count} loots") if delete_count > 0 end # :category: Deprecated Commands def cmd_db_hosts_help; deprecated_help(:hosts); end # :category: Deprecated Commands def cmd_db_notes_help; deprecated_help(:notes); end # :category: Deprecated Commands def cmd_db_vulns_help; deprecated_help(:vulns); end # :category: Deprecated Commands def cmd_db_services_help; deprecated_help(:services); end # :category: Deprecated Commands def cmd_db_autopwn_help; deprecated_help; end # :category: Deprecated Commands def cmd_db_driver_help; deprecated_help; end # :category: Deprecated Commands def cmd_db_hosts(*args); deprecated_cmd(:hosts, *args); end # :category: Deprecated Commands def cmd_db_notes(*args); deprecated_cmd(:notes, *args); end # :category: Deprecated Commands def cmd_db_vulns(*args); deprecated_cmd(:vulns, *args); end # :category: Deprecated Commands def cmd_db_services(*args); deprecated_cmd(:services, *args); end # :category: Deprecated Commands def cmd_db_autopwn(*args); deprecated_cmd; end # # :category: Deprecated Commands # # This one deserves a little more explanation than standard deprecation # warning, so give the user a better understanding of what's going on. # def cmd_db_driver(*args) deprecated_cmd print_line print_line "Because Metasploit no longer supports databases other than the default" print_line "PostgreSQL, there is no longer a need to set the driver. Thus db_driver" print_line "is not useful and its functionality has been removed. Usually Metasploit" print_line "will already have connected to the database; check db_status to see." print_line cmd_db_status end def cmd_db_import_tabs(str, words) tab_complete_filenames(str, words) end def cmd_db_import_help print_line "Usage: db_import [file2...]" print_line print_line "Filenames can be globs like *.xml, or **/*.xml which will search recursively" print_line "Currently supported file types include:" print_line " Acunetix" print_line " Amap Log" print_line " Amap Log -m" print_line " Appscan" print_line " Burp Session XML" print_line " Burp Issue XML" print_line " CI" print_line " Foundstone" print_line " FusionVM XML" print_line " IP Address List" print_line " IP360 ASPL" print_line " IP360 XML v3" print_line " Libpcap Packet Capture" print_line " Masscan XML" print_line " Metasploit PWDump Export" print_line " Metasploit XML" print_line " Metasploit Zip Export" print_line " Microsoft Baseline Security Analyzer" print_line " NeXpose Simple XML" print_line " NeXpose XML Report" print_line " Nessus NBE Report" print_line " Nessus XML (v1)" print_line " Nessus XML (v2)" print_line " NetSparker XML" print_line " Nikto XML" print_line " Nmap XML" print_line " OpenVAS Report" print_line " OpenVAS XML" print_line " Outpost24 XML" print_line " Qualys Asset XML" print_line " Qualys Scan XML" print_line " Retina XML" print_line " Spiceworks CSV Export" print_line " Wapiti XML" print_line end # # Generic import that automatically detects the file type # def cmd_db_import(*args) return unless active? ::ActiveRecord::Base.connection_pool.with_connection { if args.include?("-h") || ! (args && args.length > 0) cmd_db_import_help return end args.each { |glob| files = ::Dir.glob(::File.expand_path(glob)) if files.empty? print_error("No such file #{glob}") next end files.each { |filename| if (not ::File.readable?(filename)) print_error("Could not read file #{filename}") next end begin warnings = 0 framework.db.import_file(:filename => filename) do |type,data| case type when :debug print_error("DEBUG: #{data.inspect}") when :vuln inst = data[1] == 1 ? "instance" : "instances" print_status("Importing vulnerability '#{data[0]}' (#{data[1]} #{inst})") when :filetype print_status("Importing '#{data}' data") when :parser print_status("Import: Parsing with '#{data}'") when :address print_status("Importing host #{data}") when :service print_status("Importing service #{data}") when :msf_loot print_status("Importing loot #{data}") when :msf_task print_status("Importing task #{data}") when :msf_report print_status("Importing report #{data}") when :pcap_count print_status("Import: #{data} packets processed") when :record_count print_status("Import: #{data[1]} records processed") when :warning print_error data.split("\n").each do |line| print_error(line) end print_error warnings += 1 end end print_status("Successfully imported #{filename}") print_error("Please note that there were #{warnings} warnings") if warnings > 1 print_error("Please note that there was one warning") if warnings == 1 rescue Msf::DBImportError print_error("Failed to import #{filename}: #{$!}") elog("Failed to import #{filename}: #{$!.class}: #{$!}") dlog("Call stack: #{$@.join("\n")}", LEV_3) next rescue REXML::ParseException => e print_error("Failed to import #{filename} due to malformed XML:") print_error("#{e.class}: #{e}") elog("Failed to import #{filename}: #{e.class}: #{e}") dlog("Call stack: #{$@.join("\n")}", LEV_3) next end } } } end def cmd_db_export_help # Like db_hosts and db_services, this creates a list of columns, so # use its -h cmd_db_export("-h") end # # Export an XML # def cmd_db_export(*args) return unless active? ::ActiveRecord::Base.connection_pool.with_connection { export_formats = %W{xml pwdump} format = 'xml' output = nil while (arg = args.shift) case arg when '-h','--help' print_line "Usage:" print_line " db_export -f [filename]" print_line " Format can be one of: #{export_formats.join(", ")}" when '-f','--format' format = args.shift.to_s.downcase else output = arg end end if not output print_error("No output file was specified") return end if not export_formats.include?(format) print_error("Unsupported file format: #{format}") print_error("Unsupported file format: '#{format}'. Must be one of: #{export_formats.join(", ")}") return end print_status("Starting export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...") framework.db.run_db_export(output, format) print_status("Finished export of workspace #{framework.db.workspace.name} to #{output} [ #{format} ]...") } end def find_nmap_path Rex::FileUtils.find_full_path("nmap") || Rex::FileUtils.find_full_path("nmap.exe") end # # Import Nmap data from a file # def cmd_db_nmap(*args) return unless active? ::ActiveRecord::Base.connection_pool.with_connection { if (args.length == 0) print_status("Usage: db_nmap [--save | [--help | -h]] [nmap options]") return end save = false arguments = [] while (arg = args.shift) case arg when '--save' save = true when '--help', '-h' cmd_db_nmap_help return else arguments << arg end end nmap = find_nmap_path unless nmap print_error("The nmap executable could not be found") return end fd = Rex::Quickfile.new(['msf-db-nmap-', '.xml'], Msf::Config.local_directory) begin # When executing native Nmap in Cygwin, expand the Cygwin path to a Win32 path if(Rex::Compat.is_cygwin and nmap =~ /cygdrive/) # Custom function needed because cygpath breaks on 8.3 dirs tout = Rex::Compat.cygwin_to_win32(fd.path) arguments.push('-oX', tout) else arguments.push('-oX', fd.path) end begin nmap_pipe = ::Open3::popen3([nmap, 'nmap'], *arguments) temp_nmap_threads = [] temp_nmap_threads << framework.threads.spawn("db_nmap-Stdout", false, nmap_pipe[1]) do |np_1| np_1.each_line do |nmap_out| next if nmap_out.strip.empty? print_status("Nmap: #{nmap_out.strip}") end end temp_nmap_threads << framework.threads.spawn("db_nmap-Stderr", false, nmap_pipe[2]) do |np_2| np_2.each_line do |nmap_err| next if nmap_err.strip.empty? print_status("Nmap: '#{nmap_err.strip}'") end end temp_nmap_threads.map {|t| t.join rescue nil} nmap_pipe.each {|p| p.close rescue nil} rescue ::IOError end framework.db.import_nmap_xml_file(:filename => fd.path) print_status("Saved NMAP XML results to #{fd.path}") if save ensure fd.close fd.unlink unless save end } end def cmd_db_nmap_help nmap = find_nmap_path unless nmap print_error("The nmap executable could not be found") return end stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help') stdout.each_line do |out_line| next if out_line.strip.empty? print_status(out_line.strip) end stderr.each_line do |err_line| next if err_line.strip.empty? print_error(err_line.strip) end end def cmd_db_nmap_tabs(str, words) nmap = find_nmap_path unless nmap return end stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help') tabs = [] stdout.each_line do |out_line| if out_line.strip.starts_with?('-') tabs.push(out_line.strip.split(':').first) end end stderr.each_line do |err_line| next if err_line.strip.empty? print_error(err_line.strip) end return tabs end # # Database management # def db_check_driver unless framework.db.driver print_error("No database driver installed.") return false end true end # # Is everything working? # def cmd_db_status(*args) return if not db_check_driver if framework.db.connection_established? cdb = '' ::ActiveRecord::Base.connection_pool.with_connection do |conn| if conn.respond_to?(:current_database) cdb = conn.current_database end end print_status("#{framework.db.driver} connected to #{cdb}") else print_status("#{framework.db.driver} selected, no connection") end end def cmd_db_connect_help # Help is specific to each driver cmd_db_connect("-h") end def cmd_db_connect(*args) return if not db_check_driver if args[0] != '-h' && framework.db.connection_established? cdb = '' ::ActiveRecord::Base.connection_pool.with_connection do |conn| if conn.respond_to?(:current_database) cdb = conn.current_database end end print_error("#{framework.db.driver} already connected to #{cdb}") print_error('Run db_disconnect first if you wish to connect to a different database') return end if (args[0] == "-y") if (args[1] and not ::File.exist? ::File.expand_path(args[1])) print_error("File not found") return end file = args[1] || ::File.join(Msf::Config.get_config_root, "database.yml") file = ::File.expand_path(file) if (::File.exist? file) db = YAML.load(::File.read(file))['production'] framework.db.connect(db) if framework.db.active and not framework.db.modules_cached print_status("Rebuilding the module cache in the background...") framework.threads.spawn("ModuleCacheRebuild", true) do framework.db.update_all_module_details end end return end end meth = "db_connect_#{framework.db.driver}" if(self.respond_to?(meth, true)) self.send(meth, *args) if framework.db.active and not framework.db.modules_cached print_status("Rebuilding the module cache in the background...") framework.threads.spawn("ModuleCacheRebuild", true) do framework.db.update_all_module_details end end else print_error("This database driver #{framework.db.driver} is not currently supported") end end def cmd_db_disconnect_help print_line "Usage: db_disconnect" print_line print_line "Disconnect from the database." print_line end def cmd_db_disconnect(*args) return if not db_check_driver if(args[0] and (args[0] == "-h" || args[0] == "--help")) cmd_db_disconnect_help return end if (framework.db) framework.db.disconnect() end end def cmd_db_rebuild_cache unless framework.db.active print_error("The database is not connected") return end print_status("Purging and rebuilding the module cache in the background...") framework.threads.spawn("ModuleCacheRebuild", true) do framework.db.purge_all_module_details framework.db.update_all_module_details end end def cmd_db_rebuild_cache_help print_line "Usage: db_rebuild_cache" print_line print_line "Purge and rebuild the SQL module cache." print_line end def db_find_tools(tools) missed = [] tools.each do |name| if(! Rex::FileUtils.find_full_path(name)) missed << name end end if(not missed.empty?) print_error("This database command requires the following tools to be installed: #{missed.join(", ")}") return end true end # # Database management: Postgres # # # Connect to an existing Postgres database # def db_connect_postgresql(*args) if(args[0] == nil or args[0] == "-h" or args[0] == "--help") print_status(" Usage: db_connect @/") print_status(" OR: db_connect -y [path/to/database.yml]") print_status("Examples:") print_status(" db_connect user@metasploit3") print_status(" db_connect user:pass@192.168.0.2/metasploit3") print_status(" db_connect user:pass@192.168.0.2:1500/metasploit3") return end info = db_parse_db_uri_postgresql(args[0]) opts = { 'adapter' => 'postgresql' } opts['username'] = info[:user] if (info[:user]) opts['password'] = info[:pass] if (info[:pass]) opts['database'] = info[:name] opts['host'] = info[:host] if (info[:host]) opts['port'] = info[:port] if (info[:port]) opts['pass'] ||= '' # Do a little legwork to find the real database socket if(! opts['host']) while(true) done = false dirs = %W{ /var/run/postgresql /tmp } dirs.each do |dir| if(::File.directory?(dir)) d = ::Dir.new(dir) d.entries.grep(/^\.s\.PGSQL.(\d+)$/).each do |ent| opts['port'] = ent.split('.')[-1].to_i opts['host'] = dir done = true break end end break if done end break end end # Default to loopback if(! opts['host']) opts['host'] = '127.0.0.1' end if (not framework.db.connect(opts)) raise RuntimeError.new("Failed to connect to the database: #{framework.db.error}") end end def db_parse_db_uri_postgresql(path) res = {} if (path) auth, dest = path.split('@') (dest = auth and auth = nil) if not dest # remove optional scheme in database url auth = auth.sub(/^\w+:\/\//, "") if auth res[:user],res[:pass] = auth.split(':') if auth targ,name = dest.split('/') (name = targ and targ = nil) if not name res[:host],res[:port] = targ.split(':') if targ end res[:name] = name || 'metasploit3' res end # # Miscellaneous option helpers # # # Takes +host_ranges+, an Array of RangeWalkers, and chunks it up into # blocks of 1024. # def each_host_range_chunk(host_ranges, &block) # Chunk it up and do the query in batches. The naive implementation # uses so much memory for a /8 that it's basically unusable (1.6 # billion IP addresses take a rather long time to allocate). # Chunking has roughly the same performance for small batches, so # don't worry about it too much. host_ranges.each do |range| if range.nil? or range.length.nil? chunk = nil end_of_range = true else chunk = [] end_of_range = false # Set up this chunk of hosts to search for while chunk.length < 1024 and chunk.length < range.length n = range.next_ip if n.nil? end_of_range = true break end chunk << n end end # The block will do some yield chunk # Restart the loop with the same RangeWalker if we didn't get # to the end of it in this chunk. redo unless end_of_range end end ####### private ####### def add_data_service(*args) # database is required to use Mdm objects unless framework.db.active print_error("Database not connected; connect to an existing database with db_connect before using data_services") return end protocol = "http" port = 8080 https_opts = {} while (arg = args.shift) case arg when '-p' port = args.shift when '-s', '--ssl' protocol = "https" when '-c', '--cert' https_opts[:cert] = args.shift when '--skip-verify' https_opts[:skip_verify] = true else host = arg end end if host.nil? || port.nil? print_error "Host and port are required." return end endpoint = "#{protocol}://#{host}:#{port}" remote_data_service = Metasploit::Framework::DataService::RemoteHTTPDataService.new(endpoint, https_opts) begin framework.db.register_data_service(remote_data_service) print_line "Registered data service: #{remote_data_service.name}" framework.db.workspace = framework.db.default_workspace rescue => e print_error "There was a problem registering the remote data service: #{e.message}" end end def set_data_service(service_id) begin data_service = framework.db.set_data_service(service_id) framework.db.workspace = framework.db.default_workspace data_service rescue => e print_error "Unable to set data service: #{e.message}" end end def list_data_services() framework.db.get_services_metadata.each {|metadata| out = "id: #{metadata.id}, name: #{metadata.name}" if metadata.active out += " [active]" end print_line out } end def data_service_help print_line "Usage: data_services [ options ] - list data services by default" print_line print_line "OPTIONS:" print_line " -h, --help Show this help information." print_line " -s, --set Set the data service by identifier." print_line " -a, --add [ options ] host Adds data service" print_line " Add Data Service Options:" print_line " -p The port the data service is listening on. Default is 8080." print_line " -s, --ssl Enable SSL. Required for HTTPS data services." print_line " -c, --cert Certificate file matching the server's certificate. Needed when using self-signed SSL cert." print_line " --skip-verify Skip validating authenticity of server's certificate. NOT RECOMMENDED." print_line end def print_msgs(status_msg, error_msg) status_msg.each do |s| print_status(s) end error_msg.each do |e| print_error(e) end end end end end end end