From fbe5f90a2e87f05e5e2ee36cf460dfcf947ddbd0 Mon Sep 17 00:00:00 2001 From: cgranleese-r7 Date: Tue, 21 May 2024 11:00:24 +0100 Subject: [PATCH] Improves UX for scanner/login modules --- lib/msf/core/auxiliary/report_summary.rb | 138 ++++++++++++++++++ lib/msf/core/feature_manager.rb | 8 + .../auxiliary/scanner/mysql/mysql_login.rb | 23 +-- .../scanner/rservices/rlogin_login.rb | 1 + modules/auxiliary/scanner/ssh/ssh_login.rb | 1 + 5 files changed, 150 insertions(+), 21 deletions(-) create mode 100644 lib/msf/core/auxiliary/report_summary.rb diff --git a/lib/msf/core/auxiliary/report_summary.rb b/lib/msf/core/auxiliary/report_summary.rb new file mode 100644 index 0000000000000..3fc30b3ca1705 --- /dev/null +++ b/lib/msf/core/auxiliary/report_summary.rb @@ -0,0 +1,138 @@ +# -*- coding: binary -*- + +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# Framework web site for more information on licensing and terms of use. +# https://metasploit.com/framework/ +## + +module Msf + class Auxiliary + ### + # + # This module provides a means to report module summaries + # + ### + module ReportSummary + def initialize(info = {}) + super(info) + + if framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) + register_options( + [ + OptBool.new('ShowSuccessfulLogins', [false, 'Outputs a table of successful logins', true]), + ] + ) + end + end + + # TODO: Fix class variables + # - Need to sync up with Alan on stuff + def run + @@report = {} + super + return unless framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) + return unless datastore['ShowSuccessfulLogins'] == true + + print_report_summary + end + + def create_credential_login(credential_data) + return unless framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) + return unless datastore['ShowSuccessfulLogins'] == true + + credential = credential_data[:core].to_credential + @@report[rhost] = { successful_logins: [] } + @@report[rhost][:successful_logins] << credential + super + end + + # I think this will work for our new modules, but I don't know if it'll work + # for other modules that might not use 'session_setup' convention; + # i.e. you might need to hijack start_session, or create_session, or some other + # lower level building block that you'll have access to + def start_session(obj, info, ds_merge, crlf = false, sock = nil, sess = nil) + return unless framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) + return unless datastore['ShowSuccessfulLogins'] == true + + result = super + @@report[rhost].merge!({ successful_sessions: [] }) + @@report[rhost][:successful_sessions] << result + result + end + + private + + def print_report_summary + report = @@report + + conditional_verbose_output(report.keys.count) + + logins = report.flat_map { |_k, v| v[:successful_logins] }.compact + sessions = report.flat_map { |_k, v| v[:successful_sessions] }.compact + + # TODO - This was previously within the run command that was initially added as a workaround + # return results unless framework.features.enabled?(Msf::FeatureManager::MSSQL_SESSION_TYPE) + + print_status("Bruteforce completed, #{logins.size} #{logins.size == 1 ? 'credential was' : 'credentials were'} successful.") + if datastore['CreateSession'] + print_status("#{sessions.size} #{sessions.size == 1 ? 'session was' : 'sessions were'} opened successfully.") + else + print_status('You can open a session with these credentials and %grnCreateSession%clr set to true') + end + + show_successful_logins(report) + report + end + + # Logic to detect if the ShowSuccessLogins datastore option has been set + # + # @param [Hash] report Host mapped to successful logins and sessions + # @return [String] Rex::Text::Table containing successful logins + def show_successful_logins(report) + if datastore['ShowSuccessfulLogins'] == true && !report.empty? + successful_logins_to_table(report) + end + end + + # The idea here is to add a hybrid approach for scanner modules + # If only one host is scanned a more verbose output is useful to the user + # If scanning multiple hosts we would want more lightweight information + # + # @param [Object] host_count The number of hosts + def conditional_verbose_output(host_count) + if host_count == 1 + datastore['Verbose'] = true + end + end + + # Takes the login/session results and converts them into a Rex::Text::Table format + # + # @param report [Hash{String => [Metasploit::Framework::LoginScanner::Result, Msf::Sessions]}] + # @return [String] Rex::Text::Table containing successful logins + def successful_logins_to_table(report) + field_headers = %w[Host Public Private] + + markdown_fields = report.flat_map do |host, result| + if result[:successful_logins].nil? + next + end + + result[:successful_logins].map do |credential| + [host, credential.public, credential.private] + end + end + + table = ::Rex::Text::Table.new( + 'Header' => 'Successful logins', + 'Indent' => 4, + 'Columns' => field_headers, + 'Rows' => markdown_fields.compact + ) + + print_line("\n" + table.to_s + "\n") + end + end + end +end diff --git a/lib/msf/core/feature_manager.rb b/lib/msf/core/feature_manager.rb index 4c67e01fa4235..3ab3865ae771e 100644 --- a/lib/msf/core/feature_manager.rb +++ b/lib/msf/core/feature_manager.rb @@ -27,6 +27,7 @@ class FeatureManager MYSQL_SESSION_TYPE = 'mysql_session_type' MSSQL_SESSION_TYPE = 'mssql_session_type' LDAP_SESSION_TYPE = 'ldap_session_type' + SHOW_SUCCESSFUL_LOGINS = 'show_successful_logins' DEFAULTS = [ { @@ -103,6 +104,13 @@ class FeatureManager default_value: false, developer_notes: 'To be enabled by default after appropriate testing' }.freeze, + { + name: SHOW_SUCCESSFUL_LOGINS, + description: 'When enabled scanners/login modules will return a table off successful logins once the module completes', + requires_restart: false, + default_value: false, + developer_notes: 'To be enabled after appropriate testing' + }.freeze, { name: DNS, description: 'When enabled allows configuration of DNS resolution behaviour in Metasploit', diff --git a/modules/auxiliary/scanner/mysql/mysql_login.rb b/modules/auxiliary/scanner/mysql/mysql_login.rb index cec15d727f4bc..87b46b9820118 100644 --- a/modules/auxiliary/scanner/mysql/mysql_login.rb +++ b/modules/auxiliary/scanner/mysql/mysql_login.rb @@ -13,6 +13,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Scanner include Msf::Sessions::CreateSessionOptions include Msf::Auxiliary::CommandShell + include Msf::Auxiliary::ReportSummary def initialize(info = {}) super(update_info(info, @@ -60,21 +61,6 @@ def target [rhost,rport].join(":") end - def run - results = super - logins = results.flat_map { |_k, v| v[:successful_logins] } - sessions = results.flat_map { |_k, v| v[:successful_sessions] } - print_status("Bruteforce completed, #{logins.size} #{logins.size == 1 ? 'credential was' : 'credentials were'} successful.") - return results unless framework.features.enabled?(Msf::FeatureManager::MYSQL_SESSION_TYPE) - - if create_session? - print_status("#{sessions.size} MySQL #{sessions.size == 1 ? 'session was' : 'sessions were'} opened successfully.") - else - print_status('You can open an MySQL session with these credentials and %grnCreateSession%clr set to true') - end - results - end - def run_host(ip) begin if mysql_version_check("4.1.1") # Pushing down to 4.1.1. @@ -102,9 +88,6 @@ def run_host(ip) local_host: datastore['CHOST'] ) ) - - successful_logins = [] - successful_sessions = [] scanner.scan! do |result| credential_data = result.to_h credential_data.merge!( @@ -117,11 +100,10 @@ def run_host(ip) create_credential_login(credential_data) print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'" - successful_logins << result if create_session? begin - successful_sessions << session_setup(result) + session_setup(result) rescue ::StandardError => e elog('Failed to setup the session', error: e) print_brute level: :error, ip: ip, msg: "Failed to setup the session - #{e.class} #{e.message}" @@ -140,7 +122,6 @@ def run_host(ip) rescue ::Rex::ConnectionError, ::EOFError => e vprint_error "#{target} - Unable to connect: #{e.to_s}" end - { successful_logins: successful_logins, successful_sessions: successful_sessions } end # Tmtm's rbmysql is only good for recent versions of mysql, according diff --git a/modules/auxiliary/scanner/rservices/rlogin_login.rb b/modules/auxiliary/scanner/rservices/rlogin_login.rb index ca5356cd2dcf0..ecfe7c2df07f4 100644 --- a/modules/auxiliary/scanner/rservices/rlogin_login.rb +++ b/modules/auxiliary/scanner/rservices/rlogin_login.rb @@ -12,6 +12,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Login include Msf::Auxiliary::CommandShell include Msf::Sessions::CreateSessionOptions + include Msf::Auxiliary::ReportSummary def initialize super( diff --git a/modules/auxiliary/scanner/ssh/ssh_login.rb b/modules/auxiliary/scanner/ssh/ssh_login.rb index 1cab12905b80b..31cd0bcdafe13 100644 --- a/modules/auxiliary/scanner/ssh/ssh_login.rb +++ b/modules/auxiliary/scanner/ssh/ssh_login.rb @@ -15,6 +15,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Scanner include Msf::Exploit::Remote::SSH::Options include Msf::Sessions::CreateSessionOptions + include Msf::Auxiliary::ReportSummary def initialize super(