# -*- coding: binary -*- # frozen_string_literal: true module Msf module Ui ### # # Displays Metasploit information useful for Debugging. # ### module Debug COMMAND_HISTORY_TOTAL = 50 FRAMEWORK_LOG_LINE_TOTAL = 50 WEB_SERVICE_LOG_LINE_TOTAL = 150 # "[mm/dd/yyyy hh:mm:ss] [e([ANY_NUMBER])]" Indicates the start of an error message # The end of an error message is indicated by the start of the next log message [mm/dd/yyyy hh:mm:ss] [[ANY_LETTER]([ANY_NUMBER])] # # # When using the commented regex, the below example framework.log will only return three separate errors, and their accompanying traces: # # [05/15/2020 14:13:38] [e(0)] core: [-] Error during IRB: undefined method `[]' for nil:NilClass # # [06/19/2020 12:05:02] [i(0)] core: Trying to continue despite failed database creation: could not connect to server: Connection refused # Is the server running on host "127.0.0.1" and accepting # TCP/IP connections on port 5433? # # [05/15/2020 14:19:20] [e(0)] core: [-] Error while running command debug: can't modify frozen String # Call stack: # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/msf/ui/debug.rb:33:in `get_all' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/msf/ui/console/command_dispatcher/core.rb:318:in `cmd_debug' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:523:in `run_command' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:474:in `block in run_single' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:468:in `each' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:468:in `run_single' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/rex/ui/text/shell.rb:158:in `run' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/metasploit/framework/command/console.rb:48:in `start' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/metasploit/framework/command/base.rb:82:in `start' # # [06/19/2020 11:51:44] [d(2)] core: Stager osx/armle/reverse_tcp and stage osx/x64/meterpreter have incompatible architectures: armle - x64 # # [05/15/2020 14:23:55] [e(0)] core: [-] Error during IRB: undefined method `[]' for nil:NilClass FRAMEWORK_ERROR_REGEX = %r|\[\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}\] \[e\(\d+\)\] (?:(?!\[\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}\] \[[A-Za-z]\(\d+\)\]).)+|m FRAMEWORK_ERROR_TOTAL = 10 # "[-]" Indicates the start of an error message # The end of an error message is indicated by a \n character followed by any non-whitespace character # # When using the commented regex, the below example msf-ws.log will only return three separate errors, and their accompanying traces: # # [-] Error that does not return a stack trace. # Writing PID to /Users/agalway/.msf4/msf-ws.pid # Thin web server (v1.7.2 codename Bachmanity) # Maximum connections set to 1024 # Listening on localhost:5443, CTRL+C to stop # # # [-] Error handling request: wrong number of arguments (given 4, expected 1). # Call Stack: # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/msf/core/db_manager/service.rb:44:in `get_service' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/msf/core/db_manager/note.rb:136:in `block in report_note' # /Users/agalway/vendor/bundle/gems/activerecord-5.2.4.4/lib/active_record/connection_adapters/abstract/connection_pool.rb:416:in `with_connection' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/msf/core/db_manager/note.rb:81:in `report_note' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/msf/core/web_services/servlet/note_servlet.rb:42:in `block (2 levels) in report_note' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/msf/core/web_services/servlet_helper.rb:78:in `exec_report_job' # /Users/agalway/vendor/bundle/gems/thin-1.7.2/bin/thin:6:in `' # /Users/agalway/vendor/bundle/bin/thin:23:in `load' # /Users/agalway/vendor/bundle/bin/thin:23:in `
' # [-] Error handling request: wrong number of arguments (given 4, expected 1). # Call Stack: # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/msf/core/db_manager/service.rb:44:in `get_service' # /Users/Shared/Relocated_Items/Security/rapid7/metasploit-framework/lib/msf/core/db_manager/note.rb:136:in `block in report_note' # /Users/agalway/vendor/bundle/gems/activerecord-5.2.4.4/lib/active_record/connection_adapters/abstract/connection_pool.rb:416:in `with_connection' # /Users/agalway/vendor/bundle/gems/thin-1.7.2/bin/thin:6:in `' # /Users/agalway/vendor/bundle/bin/thin:23:in `load' # /Users/agalway/vendor/bundle/bin/thin:23:in `
' WEB_SERVICE_ERROR_REGEX = %r|\[-\].+?\n(?!\s)|m WEB_SERVICE_ERROR_TOTAL = 10 ISSUE_LINK = 'https://github.com/rapid7/metasploit-framework/issues/new/choose' PREAMBLE = <<~PREMABLE Please provide the below information in any Github issues you open. New issues can be opened here #{ISSUE_LINK.dup} %red%undENSURE YOU HAVE REMOVED ANY SENSITIVE INFORMATION BEFORE SUBMITTING!%clr ===8<=== CUT AND PASTE EVERYTHING BELOW THIS LINE ===8<=== PREMABLE ERROR_BLURB = 'An error occurred when trying to build this section:' def self.issue_link return ISSUE_LINK.dup end def self.preamble return PREAMBLE.dup end def self.all(framework, driver) all_information = preamble all_information << datastore(framework, driver) all_information << database_configuration(framework) all_information << history(driver) all_information << errors all_information << logs all_information << versions(framework) all_information end def self.datastore(framework, driver) # Generate an ini with the existing config file ini = Rex::Parser::Ini.new(Msf::Config.config_file) # Delete all groups from the config ini that potentially have more up to date information ini.keys.each do |key| unless key.start_with?("framework/database") || key.start_with?("framework/features") ini.delete(key) end end # Retrieve and add more up to date information add_hash_to_ini_group(ini, framework.datastore.to_h, driver.get_config_core) add_hash_to_ini_group(ini, driver.get_config, driver.get_config_group) if driver.active_module add_hash_to_ini_group(ini, driver.active_module.datastore.to_h, driver.active_module.refname) end # Filter credentials ini.each do |key, value| if key =~ %r{^framework/database/} value.transform_values! { '[Filtered]' } end end if ini.to_s.empty? content = 'The local config file is empty, no global variables are set, and there is no active module.' else content = ini.to_s end build_section( 'Module/Datastore', 'The following global/module datastore, and database setup was configured before the issue occurred:', content ) rescue StandardError => e build_section( 'Module/Datastore', ERROR_BLURB, section_build_error('Failed to extract Datastore', e) ) end def self.database_configuration(framework) output = "```\nSession Type: #{db_connection_info(framework)}\n```\n\n" if framework.db&.active current_workspace = framework.db.workspace example_workspaces = ::Mdm::Workspace.order(id: :desc).take(10) ordered_workspaces = ([current_workspace] + example_workspaces).uniq.sort_by(&:id) workspace_rows = ordered_workspaces.map do |workspace| id = current_workspace.id == workspace.id ? "#{workspace.id.to_fs(:delimited)} **(Current)**" : workspace.id.to_fs(:delimited) [ id, workspace.hosts.count.to_fs(:delimited), workspace.vulns.count.to_fs(:delimited), workspace.notes.count.to_fs(:delimited), workspace.services.count.to_fs(:delimited) ] end totals_row = [ "**Total (#{::Mdm::Workspace.count.to_fs(:delimited)})**", "**#{::Mdm::Host.count.to_fs(:delimited)}**", "**#{::Mdm::Vuln.count.to_fs(:delimited)}**", "**#{::Mdm::Note.count.to_fs(:delimited)}**", "**#{::Mdm::Service.count.to_fs(:delimited)}**" ] table = "| ID | Hosts | Vulnerabilities | Notes | Services |\n" table += "|-:|-:|-:|-:|-:|\n" table += (workspace_rows + [totals_row]).map { |x| "| #{x.join(" | ")} |" }.join("\n") output += table end # The markdown table can't be placed in a code block or it will not render as a table. build_section_no_block( 'Database Configuration', 'The database contains the following information:', output ) rescue StandardError => e build_section( 'Database Configuration', ERROR_BLURB, section_build_error('Failed to extract Database configuration', e) ) end def self.history(driver) end_pos = Readline::HISTORY.length - 1 start_pos = end_pos - COMMAND_HISTORY_TOTAL > driver.hist_last_saved ? end_pos - (COMMAND_HISTORY_TOTAL - 1) : driver.hist_last_saved commands = '' while start_pos <= end_pos # Formats command position in history to 6 characters in length commands += "#{'%-6.6s' % start_pos.to_s} #{Readline::HISTORY[start_pos]}\n" start_pos += 1 end build_section( 'History', 'The following commands were ran during the session and before this issue occurred:', commands ) rescue StandardError => e build_section( 'History', ERROR_BLURB, section_build_error('Failed to extract History', e) ) end def self.errors errors = build_regex_file_section(Pathname.new(Msf::Config.log_directory).join('framework.log'), FRAMEWORK_ERROR_TOTAL, FRAMEWORK_ERROR_REGEX, 'Framework Errors', 'The following framework errors occurred before the issue occurred:') errors += build_regex_file_section(Pathname.new(Msf::Config.log_directory).join('msf-ws.log'), WEB_SERVICE_ERROR_TOTAL, WEB_SERVICE_ERROR_REGEX, 'Web Service Errors', 'The following web service errors occurred before the issue occurred:') errors end def self.logs logs = build_file_section(Pathname.new(Msf::Config.log_directory).join('framework.log'), FRAMEWORK_LOG_LINE_TOTAL, 'Framework Logs', 'The following framework logs were recorded before the issue occurred:') logs += build_file_section(Pathname.new(Msf::Config.log_directory).join('msf-ws.log'), WEB_SERVICE_LOG_LINE_TOTAL, 'Web Service Logs', 'The following web service logs were recorded before the issue occurred:') logs end def self.versions(framework) str = <<~VERSIONS Framework: #{framework.version} Ruby: #{RUBY_DESCRIPTION} OpenSSL: #{OpenSSL::OPENSSL_VERSION} Install Root: #{Msf::Config.install_root} Session Type: #{db_connection_info(framework)} Install Method: #{installation_method} VERSIONS build_section('Version/Install', 'The versions and install method of your Metasploit setup:', str) rescue StandardError => e build_section( 'Version/Install', ERROR_BLURB, section_build_error('Failed to extract Versions', e) ) end class << self private def build_regex_file_section(path, match_total, regex, header_name, blurb) unless File.file?(path) return build_section( header_name, blurb, "#{path.basename.to_s} does not exist." ) end file_contents = File.read(path) matches = file_contents.scan(regex) if matches.empty? return build_section( header_name, blurb, "No matching patterns were found in #{path.basename}." ) end # +.scan+ can sometimes return each match as a single item array matches.flatten! # create a string consisting of the last +match_total+ matches # if +matches.length+ < +match_total+ then concat all matches str = concat_str_array_from_last_idx(matches, match_total) build_section( header_name, blurb, str ) rescue StandardError => e build_section( header_name, ERROR_BLURB, section_build_error("Failed to extract matches from #{path.basename}", e) ) end def build_file_section(path, line_total, header_name, blurb) unless File.file?(path) return build_section( header_name, blurb, "#{path.basename.to_s} does not exist." ) end log_lines = File.readlines(path) # create a string consisting of the last +line_total+ lines # if +log_lines.length+ < +line_total+ then concat all lines str = concat_str_array_from_last_idx(log_lines, line_total) build_section( header_name, blurb, str ) rescue StandardError => e build_section( header_name, ERROR_BLURB, section_build_error("Failed to extract contents of #{path.basename.to_s}", e) ) end def add_hash_to_ini_group(ini, hash, group_name) if hash.empty? return end unless ini.group?(group_name) ini.add_group(group_name) end hash.each_pair do |k, v| ini[group_name][k] = v end end def concat_str_array_from_last_idx(array, concat_total) start_pos = array.length > concat_total ? array.length - concat_total : 0 end_pos = array.length - 1 str = array[start_pos..end_pos].join('') str.strip end # Copy pasta of the print_connection_info method in console/command_dispatcher/db.rb def db_connection_info(framework) unless framework.db.connection_established? return "#{framework.db.driver} selected, no connection" end cdb = '' if framework.db.driver == 'http' cdb = framework.db.name else ::ApplicationRecord.connection_pool.with_connection do |conn| if conn.respond_to?(:current_database) cdb = conn.current_database end end end if cdb.empty? output = "Connected Database Name could not be extracted. DB Connection type: #{framework.db.driver}." else output = "Connected to #{cdb}. Connection type: #{framework.db.driver}." end output end def build_section(header_name, blurb, content) <<~SECTION ## %grn#{header_name.strip}%clr #{blurb.strip} #{with_collapsible_wrapper(content.strip)} SECTION end def with_collapsible_wrapper(content) <<~WRAPPER
Collapse ``` #{content} ```
WRAPPER end def with_collapsible_wrapper_no_block(content) <<~WRAPPER
Collapse #{content}
WRAPPER end # Useful for building tables or other content that can't be placed inside a code block. def build_section_no_block(header_name, blurb, content) <<~SECTION ## %grn#{header_name.strip}%clr #{blurb.strip} #{with_collapsible_wrapper_no_block(content.strip)} SECTION end def installation_method if File.exist?(File.join(Msf::Config.install_root, 'version.yml')) 'Omnibus Installer' elsif File.directory?(File.join(Msf::Config.install_root, '.git')) 'Git Clone' else 'Other - Please specify' end end def section_build_error(msg, error) "#{msg}: #{error.class} - #{error.message} \n Call stack:\n#{error.backtrace.join("\n")}" end end end end end