Path: blob/master/modules/post/multi/recon/persistence_suggester.rb
27907 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post67include Msf::Auxiliary::Report89def initialize(info = {})10super(11update_info(12info,13'Name' => 'Persistence Exploit Suggester',14'Description' => %q{15This module suggests persistence modules that can be used.16The modules are suggested based on the architecture and platform17that the user has a shell opened as well as the available exploits18in meterpreter.19It's important to note that not all modules will be checked.20Exploits are chosen based on these conditions: session type,21platform, architecture, and required default options.22},23'License' => MSF_LICENSE,24'Author' => [ 'h00die' ],25'Platform' => all_platforms,26'SessionTypes' => [ 'meterpreter', 'shell' ],27'Notes' => {28'Stability' => [],29'Reliability' => [],30'SideEffects' => []31}32)33)34register_options [35Msf::OptInt.new('SESSION', [ true, 'The session to run this module on' ]),36Msf::OptBool.new('SHOWDESCRIPTION', [true, 'Displays a detailed description for the available exploits', false])37]3839register_advanced_options(40[41# most linux persistence modules are arch-cmd but for payload purposes only42# but usually we end up with a meterpreter session, thus making these invalid43# so disable this check by default44Msf::OptBool.new('ValidateArch', [true, 'Validate architecture', false]),45Msf::OptBool.new('ValidatePlatform', [true, 'Validate platform', true]),46Msf::OptBool.new('ValidateMeterpreterCommands', [true, 'Validate Meterpreter commands', false]),47# https://github.com/rapid7/rex-text/blob/a72151d409cd812978f63ad0c330efbc8f44b977/lib/rex/text/color.rb#L1348Msf::OptString.new('Colors', [false, 'Valid, Invalid and Ignored colors for module checks (unset to disable)', 'grn/red/blu'])49]50)51end5253def valid_colors?(color_str = datastore['Colors'])54tokens = color_str.split('/')55tokens.each do |tok|56print_warning "#{tok} is unlikely to have any functionality for printing colors." if tok == 'clr'5758unless Rex::Text::Color::SUPPORTED_FORMAT_CODES.include?("%#{tok}")59print_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"60return false61end62end63true64end6566def all_platforms67Msf::Module::Platform.subclasses.collect { |c| c.realname.downcase }68end6970def session_arch71# 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)72session.respond_to?(:native_arch) ? session.native_arch : session.arch73end7475def is_module_arch?(mod)76mod_arch = mod.target.arch || mod.arch77mod_arch.include?(session_arch)78rescue StandardError => e79print_error "Failed to check module arch for #{mod.fullname} => #{e}"80end8182def is_module_wanted?(mod)83mod[:result][:incompatibility_reasons].empty?84end8586def is_session_type?(mod)87# There are some modules that do not define any compatible session types.88# We could assume that means the module can run on all session types,89# Or we could consider that as incorrect module metadata.90mod.session_types.include?(session.type)91end9293def is_module_platform?(mod)94return false if mod.target.nil?9596platform_obj = Msf::Module::Platform.find_platform session.platform9798module_platforms = mod.target.platform ? mod.target.platform.platforms : mod.platform.platforms99module_platforms.include? platform_obj100rescue ArgumentError => e101# When not found, find_platform raises an ArgumentError102elog('Could not find a platform', error: e)103return false104end105106def has_required_module_options?(mod)107get_all_missing_module_options(mod).empty?108end109110def get_all_missing_module_options(mod)111missing_options = []112mod.options.each_pair do |option_name, option|113missing_options << option_name if option.required && option.default.nil? && mod.datastore[option_name].blank?114end115missing_options116end117118def valid_incompatibility_reasons(mod, verify_reasons)119# As we can potentially ignore some `reasons` (e.g. accepting arch values which are, on paper, not compatible),120# this keeps track of valid reasons why we will not consider the module that we are evaluating to be valid.121valid_reasons = []122valid_reasons << "Missing required module options (#{get_all_missing_module_options(mod).join('. ')})" unless verify_reasons[:has_required_module_options]123124incompatible_opts = []125incompatible_opts << 'architecture' unless verify_reasons[:is_module_arch]126incompatible_opts << 'platform' unless verify_reasons[:is_module_platform]127incompatible_opts << 'session type' unless verify_reasons[:is_session_type]128valid_reasons << "Not Compatible (#{incompatible_opts.join(', ')})" if incompatible_opts.any?129130valid_reasons << 'Missing/unloadable Meterpreter commands' if verify_reasons[:missing_meterpreter_commands].any?131valid_reasons132end133134def set_module_options(mod)135ignore_list = ['ACTION', 'TARGET'].freeze136datastore.each_pair do |k, v|137mod.datastore[k] = v unless ignore_list.include?(k.upcase)138end139if !mod.datastore['SESSION'] && session.present?140mod.datastore['SESSION'] = session.sid141end142end143144def set_module_target(mod)145session_platform = Msf::Module::Platform.find_platform(session.platform)146target_index = mod.targets.find_index do |target|147# If the target doesn't define its own compatible platforms or architectures, default to the parent (module) values.148target_platforms = target.platform&.platforms || mod.platform.platforms149target_architectures = target.arch || mod.arch150151target_platforms.include?(session_platform) && target_architectures.include?(session_arch)152end153mod.datastore['Target'] = target_index if target_index154end155156def setup157return unless session158159print_status "Collecting persistence modules for #{session.session_type}..."160161setup_validation_options162if valid_colors?163setup_color_options164else165fail_with(Failure::BadConfig, 'Colors options set incorrectly')166end167168# Collects persistence modules into an array169@persistence_modules = []170exploit_refnames = framework.exploits.module_refnames171exploit_refnames.each_with_index do |name, index|172print "%bld%blu[*]%clr Collecting exploit #{index + 1} / #{exploit_refnames.count}\r"173next unless name.include? '/persistence/'174175mod = framework.exploits.create name176next unless mod177178set_module_options mod179set_module_target mod180verify_result = verify_mod(mod)181@persistence_modules << { module: mod, result: verify_result } if verify_result[:has_check]182end183end184185def verify_mod(mod)186return { has_check: false } unless mod.is_a?(Msf::Exploit::Local) && mod.has_check?187188result = {189has_check: true,190is_module_platform: (@validate_platform ? is_module_platform?(mod) : true),191is_module_arch: (@validate_arch ? is_module_arch?(mod) : true),192has_required_module_options: has_required_module_options?(mod),193missing_meterpreter_commands: (@validate_meterpreter_commands && session.type == 'meterpreter') ? meterpreter_session_incompatibility_reasons(session) : [],194is_session_type: is_session_type?(mod)195}196result[:incompatibility_reasons] = valid_incompatibility_reasons(mod, result)197result198end199200def setup_validation_options201@validate_arch = datastore['ValidateArch']202@validate_platform = datastore['ValidatePlatform']203@validate_meterpreter_commands = datastore['ValidateMeterpreterCommands']204end205206def setup_color_options207@valid_color, @invalid_color, @ignored_color =208(datastore['Colors'] || '').split('/')209210@valid_color = "%#{@valid_color}" unless @valid_color.blank?211@invalid_color = "%#{@invalid_color}" unless @invalid_color.blank?212@ignored_color = "%#{@ignored_color}" unless @ignored_color.blank?213end214215def show_found_exploits216unless datastore['VERBOSE']217print_status "#{@persistence_modules.length} exploit checks are being tried..."218return219end220221vprint_status "The following #{@persistence_modules.length} exploit checks are being tried:"222@persistence_modules.each do |x|223vprint_status x[:module].fullname224end225end226227def run228runnable_exploits = @persistence_modules.select { |mod| is_module_wanted?(mod) }229if runnable_exploits.empty?230print_error 'No suggestions available.'231vprint_line232vprint_session_info233vprint_status unwanted_modules_table(@persistence_modules.reject { |mod| is_module_wanted?(mod) })234return235end236237show_found_exploits238results = runnable_exploits.map.with_index do |mod, index|239print "%bld%blu[*]%clr Running check method for exploit #{index + 1} / #{runnable_exploits.count}\r"240begin241checkcode = mod[:module].check242rescue StandardError => e243elog("#Local Persistence Suggester failed with: #{e.class} when using #{mod[:module].shortname}", error: e)244vprint_error "Check with module #{mod[:module].fullname} failed with error #{e.class}"245next { module: mod[:module], errors: ['The check raised an exception.'] }246end247248if checkcode.nil?249vprint_error "Check failed with #{mod[:module].fullname} for unknown reasons"250next { module: mod[:module], errors: ['The check failed for unknown reasons.'] }251end252253# See def is_check_interesting?254unless is_check_interesting? checkcode255vprint_status "#{mod[:module].fullname}: #{checkcode.message}"256next { module: mod[:module], errors: [checkcode.message] }257end258259# Prints the full name and the checkcode message for the exploit260print_good "#{mod[:module].fullname}: #{checkcode.message}"261262# If the datastore option is true, a detailed description will show263if datastore['SHOWDESCRIPTION']264# Formatting for the description text265Rex::Text.wordwrap(Rex::Text.compress(mod[:module].description), 2, 70).split(/\n/).each do |line|266print_line line267end268end269270next { module: mod[:module], checkcode: checkcode.message }271end272273print_line274print_status valid_modules_table(results)275276vprint_line277vprint_session_info278vprint_status unwanted_modules_table(@persistence_modules.reject { |mod| is_module_wanted?(mod) })279280report_data = {}281results.each do |result|282report_data[result[:module].fullname] = result[:checkcode] if result[:checkcode]283end284285report_note({286host: session.session_host,287type: 'persistence.suggested_module',288data: report_data289})290end291292def valid_modules_table(results)293name_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new294check_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new295296# Split all the results by their checkcode.297# We want the modules that returned a checkcode to be at the top.298checkcode_rows, without_checkcode_rows = results.partition { |result| result[:checkcode] }299rows = (checkcode_rows + without_checkcode_rows).map.with_index do |result, index|300color = result[:checkcode] ? @valid_color : @invalid_color301check_res = result.fetch(:checkcode) { result[:errors].join('. ') }302name_styler.merge!({ result[:module].fullname => color })303check_styler.merge!({ check_res => color })304305[306index + 1,307result[:module].fullname,308result[:checkcode] ? 'Yes' : 'No',309check_res310]311end312313Rex::Text::Table.new(314'Header' => "Valid modules for session #{session.sid}:",315'Indent' => 1,316'Columns' => [ '#', 'Name', 'Potentially Vulnerable?', 'Check Result' ],317'SortIndex' => -1,318'WordWrap' => false, # Don't wordwrap as it messes up coloured output when it is broken up into more than one line319'ColProps' => {320'Name' => {321'Stylers' => [name_styler]322},323'Potentially Vulnerable?' => {324'Stylers' => [::Msf::Ui::Console::TablePrint::CustomColorStyler.new({ 'Yes' => @valid_color, 'No' => @invalid_color })]325},326'Check Result' => {327'Stylers' => [check_styler]328}329},330'Rows' => rows331)332end333334def unwanted_modules_table(unwanted_modules)335arch_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new336platform_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new337session_type_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new338339rows = unwanted_modules.map.with_index do |mod, index|340begin341platforms = mod[:module].target.platform&.platforms&.any? ? mod[:module].target.platform.platforms : mod[:module].platform.platforms342rescue NoMethodError343platforms = nil344end345platforms ||= []346begin347arch = mod[:module].target.arch&.any? ? mod[:module].target.arch : mod[:module].arch348rescue NoMethodError349arch = nil350end351arch ||= []352353arch.each do |a|354if a != session_arch355if @validate_arch356color = @invalid_color357else358color = @ignored_color359end360else361color = @valid_color362end363364arch_styler.merge!({ a.to_s => color })365end366367platforms.each do |module_platform|368if module_platform != ::Msf::Module::Platform.find_platform(session.platform)369if @validate_platform370color = @invalid_color371else372color = @ignored_color373end374else375color = @valid_color376end377378platform_styler.merge!({ module_platform.realname => color })379end380381mod[:module].session_types.each do |session_type|382color = session_type == session.type ? @valid_color : @invalid_color383session_type_styler.merge!(session_type.to_s => color)384end385386[387index + 1,388mod[:module].fullname,389mod[:result][:incompatibility_reasons].join('. '),390platforms.any? ? platforms.map(&:realname).sort.join(', ') : 'No defined platforms',391arch.any? ? arch.sort.join(', ') : 'No defined architectures',392mod[:module].session_types.any? ? mod[:module].session_types.sort.join(', ') : 'No defined session types'393]394end395396Rex::Text::Table.new(397'Header' => "Incompatible modules for session #{session.sid}:",398'Indent' => 1,399'Columns' => [ '#', 'Name', 'Reasons', 'Platform', 'Architecture', 'Session Type' ],400'WordWrap' => false,401'ColProps' => {402'Architecture' => {403'Stylers' => [arch_styler]404},405'Platform' => {406'Stylers' => [platform_styler]407},408'Session Type' => {409'Stylers' => [session_type_styler]410}411},412'Rows' => rows413)414end415416def vprint_session_info417vprint_status 'Current Session Info:'418vprint_status " Session Type: #{session.type}"419vprint_status " Architecture: #{session_arch}"420vprint_status " Platform: #{session.platform}"421end422423def is_check_interesting?(checkcode)424[425Msf::Exploit::CheckCode::Vulnerable,426Msf::Exploit::CheckCode::Appears,427Msf::Exploit::CheckCode::Detected428].include? checkcode429end430431def print_status(msg = '')432super(session ? "#{session.session_host} - #{msg}" : msg)433end434435def print_good(msg = '')436super(session ? "#{session.session_host} - #{msg}" : msg)437end438439def print_error(msg = '')440super(session ? "#{session.session_host} - #{msg}" : msg)441end442end443444445