module Msf
module Ui
module Debug
COMMAND_HISTORY_TOTAL = 50
FRAMEWORK_LOG_LINE_TOTAL = 50
WEB_SERVICE_LOG_LINE_TOTAL = 150
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
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 << framework_config(framework)
all_information << history(driver)
all_information << errors
all_information << logs
all_information << versions(framework)
all_information
end
def self.datastore(framework, driver)
ini = Rex::Parser::Ini.new(Msf::Config.config_file)
ini.keys.each do |key|
unless key.start_with?("framework/database") || key.start_with?("framework/features")
ini.delete(key)
end
end
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
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
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.framework_config(framework)
required_features = framework.features.all.map { |feature| [feature[:name], feature[:enabled].to_s] }
markdown_formatted_features = required_features.map { |feature| "| #{feature.join(' | ')} |" }
required_fields = %w[name enabled]
table = "| #{required_fields.join(' | ')} |\n"
table += '|' + '-:|' * required_fields.count + "\n"
table += markdown_formatted_features.join("\n").to_s
build_section_no_block(
'Framework Configuration',
'The features are configured as follows:',
table
)
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
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
matches.flatten!
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)
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
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
<details>
<summary>Collapse</summary>
```
#{content}
```
</details>
WRAPPER
end
def with_collapsible_wrapper_no_block(content)
<<~WRAPPER
<details>
<summary>Collapse</summary>
#{content}
</details>
WRAPPER
end
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