Path: blob/master/lib/msf/ui/console/driver.rb
19515 views
# -*- coding: binary -*-1require 'find'2require 'erb'3require 'rexml/document'4require 'fileutils'5require 'digest/md5'67module Msf8module Ui9module Console1011#12# A user interface driver on a console interface.13#14class Driver < Msf::Ui::Driver1516ConfigCore = "framework/core"17ConfigGroup = "framework/ui/console"1819DefaultPrompt = "%undmsf#{Metasploit::Framework::Version::MAJOR}%clr"20DefaultPromptChar = "%clr>"2122#23# Console Command Dispatchers to be loaded after the Core dispatcher.24#25CommandDispatchers = [26CommandDispatcher::Modules,27CommandDispatcher::Jobs,28CommandDispatcher::Resource,29CommandDispatcher::Db,30CommandDispatcher::Creds,31CommandDispatcher::Developer,32CommandDispatcher::DNS33]3435#36# The console driver processes various framework notified events.37#38include FrameworkEventManager3940#41# The console driver is a command shell.42#43include Rex::Ui::Text::DispatcherShell4445include Rex::Ui::Text::Resource4647#48# Initializes a console driver instance with the supplied prompt string and49# prompt character. The optional hash can take extra values that will50# serve to initialize the console driver.51#52# @option opts [Boolean] 'AllowCommandPassthru' (true) Whether to allow53# unrecognized commands to be executed by the system shell54# @option opts [Boolean] 'Readline' (true) Whether to use the readline or not55# @option opts [String] 'HistFile' (Msf::Config.history_file) Path to a file56# where we can store command history57# @option opts [Array<String>] 'Resources' ([]) A list of resource files to58# load. If no resources are given, will load the default resource script,59# 'msfconsole.rc' in the user's {Msf::Config.config_directory config60# directory}61# @option opts [Boolean] 'SkipDatabaseInit' (false) Whether to skip62# connecting to the database and running migrations63def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = {})64setup_readline6566histfile = opts['HistFile'] || Msf::Config.history_file6768begin69FeatureManager.instance.load_config70rescue StandardError => e71elog(e)72end7374if opts['DeferModuleLoads'].nil?75opts['DeferModuleLoads'] = Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DEFER_MODULE_LOADS)76end7778# Initialize attributes7980framework_create_options = opts.merge({ 'DeferModuleLoads' => true })8182if Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DNS)83dns_resolver = Rex::Proto::DNS::CachedResolver.new84dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider)85dns_resolver.load_config if dns_resolver.has_config?8687# Defer loading of modules until paths from opts can be added below88framework_create_options = framework_create_options.merge({ 'CustomDnsResolver' => dns_resolver })89end90self.framework = opts['Framework'] || Msf::Simple::Framework.create(framework_create_options)9192if self.framework.datastore['Prompt']93prompt = self.framework.datastore['Prompt']94prompt_char = self.framework.datastore['PromptChar'] || DefaultPromptChar95end9697# Call the parent98super(prompt, prompt_char, histfile, framework, :msfconsole)99100# Temporarily disable output101self.disable_output = true102103# Load pre-configuration104load_preconfig105106# Initialize the user interface to use a different input and output107# handle if one is supplied108input = opts['LocalInput']109input ||= Rex::Ui::Text::Input::Stdio.new110111if !opts['Readline']112input.disable_readline113end114115if (opts['LocalOutput'])116if (opts['LocalOutput'].kind_of?(String))117output = Rex::Ui::Text::Output::File.new(opts['LocalOutput'])118else119output = opts['LocalOutput']120end121else122output = Rex::Ui::Text::Output::Stdio.new123end124125init_ui(input, output)126init_tab_complete127128# Add the core command dispatcher as the root of the dispatcher129# stack130enstack_dispatcher(CommandDispatcher::Core)131132# Load the other "core" command dispatchers133CommandDispatchers.each do |dispatcher_class|134dispatcher = enstack_dispatcher(dispatcher_class)135dispatcher.load_config(opts['Config'])136end137138if !framework.db || !framework.db.active139if framework.db.error == "disabled"140print_warning("Database support has been disabled")141else142error_msg = "#{framework.db.error.class.is_a?(String) ? "#{framework.db.error.class} " : nil}#{framework.db.error}"143print_warning("No database support: #{error_msg}")144end145end146147# Register event handlers148register_event_handlers149150# Re-enable output151self.disable_output = false152153# Whether or not command passthru should be allowed154self.command_passthru = opts.fetch('AllowCommandPassthru', true)155156# Whether or not to confirm before exiting157self.confirm_exit = opts['ConfirmExit']158159# Initialize the module paths only if we didn't get passed a Framework instance and 'DeferModuleLoads' is false160unless opts['Framework']161# Configure the framework module paths162self.framework.init_module_paths(module_paths: opts['ModulePath'], defer_module_loads: opts['DeferModuleLoads'])163end164165unless opts['DeferModuleLoads']166framework.threads.spawn("ModuleCacheRebuild", true) do167framework.modules.refresh_cache_from_module_files168end169end170171# Load console-specific configuration (after module paths are added)172load_config(opts['Config'])173174# Process things before we actually display the prompt and get rocking175on_startup(opts)176177# Process any resource scripts178if opts['Resource'].blank?179# None given, load the default180default_resource = ::File.join(Msf::Config.config_directory, 'msfconsole.rc')181load_resource(default_resource) if ::File.exist?(default_resource)182else183opts['Resource'].each { |r|184load_resource(r)185}186end187188# Process persistent job handler189begin190restore_handlers = JSON.parse(File.read(Msf::Config.persist_file))191rescue Errno::ENOENT, JSON::ParserError192restore_handlers = nil193end194195if restore_handlers196print_status("Starting persistent handler(s)...")197198restore_handlers.each.with_index do |handler_opts, index|199handler = framework.modules.create(handler_opts['mod_name'])200handler.init_ui(self.input, self.output)201replicant_handler = nil202handler.exploit_simple(handler_opts['mod_options']) do |yielded_replicant_handler|203replicant_handler = yielded_replicant_handler204end205206if replicant_handler.nil? || replicant_handler.error207print_status("Failed to start persistent payload handler ##{index} (#{handler_opts['mod_name']})")208next209end210211if replicant_handler.error.nil?212job_id = handler.job_id213print_status "Persistent payload handler started as Job #{job_id}"214end215end216end217218# Process any additional startup commands219if opts['XCommands'] and opts['XCommands'].kind_of? Array220opts['XCommands'].each { |c|221run_single(c)222}223end224end225226#227# Loads configuration that needs to be analyzed before the framework228# instance is created.229#230def load_preconfig231begin232conf = Msf::Config.load233rescue234wlog("Failed to load configuration: #{$!}")235return236end237238if (conf.group?(ConfigCore))239conf[ConfigCore].each_pair { |k, v|240on_variable_set(true, k, v)241}242end243end244245#246# Loads configuration for the console.247#248def load_config(path=nil)249begin250conf = Msf::Config.load(path)251rescue252wlog("Failed to load configuration: #{$!}")253return254end255256# If we have configuration, process it257if (conf.group?(ConfigGroup))258conf[ConfigGroup].each_pair { |k, v|259case k.downcase260when 'activemodule'261run_single("use #{v}")262when 'activeworkspace'263if framework.db.active264workspace = framework.db.find_workspace(v)265framework.db.workspace = workspace if workspace266end267end268}269end270end271272#273# Generate configuration for the console.274#275def get_config276# Build out the console config group277group = {}278279if (active_module)280group['ActiveModule'] = active_module.fullname281end282283if framework.db.active284unless framework.db.workspace.default?285group['ActiveWorkspace'] = framework.db.workspace.name286end287end288289group290end291292def get_config_core293ConfigCore294end295296def get_config_group297ConfigGroup298end299300#301# Saves configuration for the console.302#303def save_config304begin305Msf::Config.save(ConfigGroup => get_config)306rescue ::Exception307print_error("Failed to save console config: #{$!}")308end309end310311#312# Saves the recent history to the specified file313#314def save_recent_history(path)315num = Readline::HISTORY.length - hist_last_saved - 1316317tmprc = ""318num.times { |x|319tmprc << Readline::HISTORY[hist_last_saved + x] + "\n"320}321322if tmprc.length > 0323print_status("Saving last #{num} commands to #{path} ...")324save_resource(tmprc, path)325else326print_error("No commands to save!")327end328329# Always update this, even if we didn't save anything. We do this330# so that we don't end up saving the "makerc" command itself.331self.hist_last_saved = Readline::HISTORY.length332end333334#335# Creates the resource script file for the console.336#337def save_resource(data, path=nil)338path ||= File.join(Msf::Config.config_directory, 'msfconsole.rc')339340begin341rcfd = File.open(path, 'w')342rcfd.write(data)343rcfd.close344rescue ::Exception345end346end347348#349# Called before things actually get rolling such that banners can be350# displayed, scripts can be processed, and other fun can be had.351#352def on_startup(opts = {})353# Check for modules that failed to load354if framework.modules.module_load_error_by_path.length > 0355wlog("The following modules could not be loaded!")356357framework.modules.module_load_error_by_path.each do |path, _error|358wlog("\t#{path}")359end360end361362if framework.modules.module_load_warnings.length > 0363print_warning("The following modules were loaded with warnings:")364365framework.modules.module_load_warnings.each do |path, _error|366wlog("\t#{path}")367end368end369370if framework.db&.active371framework.db.workspace = framework.db.default_workspace unless framework.db.workspace372end373374framework.events.on_ui_start(Msf::Framework::Revision)375376if $msf_spinner_thread377$msf_spinner_thread.kill378$stderr.print "\r" + (" " * 50) + "\n"379end380381run_single("banner") unless opts['DisableBanner']382383payloads_manifest_errors = []384begin385payloads_manifest_errors = ::MetasploitPayloads.manifest_errors if framework.features.enabled?(::Msf::FeatureManager::METASPLOIT_PAYLOAD_WARNINGS)386rescue ::StandardError => e387$stderr.print('Could not verify the integrity of the Metasploit Payloads manifest')388elog(e)389end390391av_warning_message if (framework.eicar_corrupted? || payloads_manifest_errors.any?)392393if framework.features.enabled?(::Msf::FeatureManager::METASPLOIT_PAYLOAD_WARNINGS)394if payloads_manifest_errors.any?395warn_msg = "Metasploit Payloads manifest errors:\n"396payloads_manifest_errors.each do |file|397warn_msg << "\t#{file[:path]} : #{file[:error]}\n"398end399$stderr.print(warn_msg)400end401end402403opts["Plugins"].each do |plug|404run_single("load '#{plug}'")405end if opts["Plugins"]406407self.on_command_proc = Proc.new { |command| framework.events.on_ui_command(command) }408end409410def av_warning_message411avdwarn = "\e[31m"\412"Warning: This copy of the Metasploit Framework has been corrupted by an installed anti-virus program."\413" We recommend that you disable your anti-virus or exclude your Metasploit installation path, "\414"then restore the removed files from quarantine or reinstall the framework.\e[0m"\415"\n\n"416417$stderr.puts(Rex::Text.wordwrap(avdwarn, 0, 80))418end419420#421# Called when a variable is set to a specific value. This allows the422# console to do extra processing, such as enabling logging or doing423# some other kind of task. If this routine returns false it will indicate424# that the variable is not being set to a valid value.425#426def on_variable_set(glob, var, val)427case var.downcase428when 'sessionlogging'429handle_session_logging(val) if glob430when 'sessiontlvlogging'431handle_session_tlv_logging(val) if glob432when 'consolelogging'433handle_console_logging(val) if glob434when 'loglevel'435handle_loglevel(val) if glob436when 'payload'437handle_payload(val)438when 'ssh_ident'439handle_ssh_ident(val)440end441end442443#444# Called when a variable is unset. If this routine returns false it is an445# indication that the variable should not be allowed to be unset.446#447def on_variable_unset(glob, var)448case var.downcase449when 'sessionlogging'450handle_session_logging('0') if glob451when 'sessiontlvlogging'452handle_session_tlv_logging('false') if glob453when 'consolelogging'454handle_console_logging('0') if glob455when 'loglevel'456handle_loglevel(nil) if glob457end458end459460#461# Proxies to shell.rb's update prompt with our own extras462#463def update_prompt(*args)464if args.empty?465pchar = framework.datastore['PromptChar'] || DefaultPromptChar466p = framework.datastore['Prompt'] || DefaultPrompt467p = "#{p} #{active_module.type}(%bld%red#{active_module.promptname}%clr)" if active_module468super(p, pchar)469else470# Don't squash calls from within lib/rex/ui/text/shell.rb471super(*args)472end473end474475#476# The framework instance associated with this driver.477#478attr_reader :framework479#480# Whether or not to confirm before exiting481#482attr_reader :confirm_exit483#484# Whether or not commands can be passed through.485#486attr_reader :command_passthru487#488# The active module associated with the driver.489#490attr_accessor :active_module491#492# The active session associated with the driver.493#494attr_accessor :active_session495496def stop497framework.events.on_ui_stop()498super499end500501protected502503attr_writer :framework # :nodoc:504attr_writer :confirm_exit # :nodoc:505attr_writer :command_passthru # :nodoc:506507#508# If an unknown command was passed, try to see if it's a valid local509# executable. This is only allowed if command passthru has been permitted510#511def unknown_command(method, line)512if File.basename(method) == 'msfconsole'513print_error('msfconsole cannot be run inside msfconsole')514return515end516517[method, method+".exe"].each do |cmd|518if command_passthru && Rex::FileUtils.find_full_path(cmd)519520self.busy = true521begin522run_unknown_command(line)523rescue ::Errno::EACCES, ::Errno::ENOENT524print_error("Permission denied exec: #{line}")525end526self.busy = false527return528end529end530531if framework.modules.create(method)532super533if prompt_yesno "This is a module we can load. Do you want to use #{method}?"534run_single "use #{method}"535end536537return538end539540super541end542543def run_unknown_command(command)544print_status("exec: #{command}")545print_line('')546system(command)547end548549##550#551# Handlers for various global configuration values552#553##554555#556# SessionLogging.557#558def handle_session_logging(val)559if (val =~ /^(y|t|1)/i)560Msf::Logging.enable_session_logging(true)561framework.sessions.values.each do |session|562Msf::Logging.start_session_log(session)563end564print_line("Session logging enabled.")565else566Msf::Logging.enable_session_logging(false)567framework.sessions.values.each do |session|568Msf::Logging.stop_session_log(session)569end570print_line("Session logging disabled.")571end572end573574#575# ConsoleLogging.576#577def handle_console_logging(val)578if (val =~ /^(y|t|1)/i)579Msf::Logging.enable_log_source('console')580print_line("Console logging is now enabled.")581582set_log_source('console')583584rlog("\n[*] Console logging started: #{Time.now}\n\n", 'console')585else586rlog("\n[*] Console logging stopped: #{Time.now}\n\n", 'console')587588unset_log_source589590Msf::Logging.disable_log_source('console')591print_line("Console logging is now disabled.")592end593end594595#596# This method handles adjusting the global log level threshold.597#598def handle_loglevel(val)599set_log_level(Rex::LogSource, val)600set_log_level(Msf::LogSource, val)601end602603#604# This method handles setting a desired payload605#606# TODO: Move this out of the console driver!607#608def handle_payload(val)609if framework && !framework.payloads.valid?(val)610return false611elsif active_module && (active_module.exploit? || active_module.evasion?)612return false unless active_module.is_payload_compatible?(val)613end614end615616#617# This method monkeypatches Net::SSH's client identification string618#619# TODO: Move this out of the console driver!620#621def handle_ssh_ident(val)622# HACK: Suppress already initialized constant warning623verbose, $VERBOSE = $VERBOSE, nil624625return false unless val.is_a?(String) && !val.empty?626627require 'net/ssh'628629# HACK: Bypass dynamic constant assignment error630::Net::SSH::Transport::ServerVersion.const_set(:PROTO_VERSION, val)631632true633rescue LoadError634print_error('Net::SSH could not be loaded')635false636rescue NameError637print_error('Invalid constant Net::SSH::Transport::ServerVersion::PROTO_VERSION')638false639ensure640# Restore warning641$VERBOSE = verbose642end643644def handle_session_tlv_logging(val)645return false if val.nil?646647if val.casecmp?('console') || val.casecmp?('true') || val.casecmp?('false')648return true649elsif val.start_with?('file:') && !val.split('file:').empty?650pathname = ::Pathname.new(val.split('file:').last)651652# Check if we want to write the log to file653if ::File.file?(pathname)654if ::File.writable?(pathname)655return true656else657print_status "No write permissions for log output file: #{pathname}"658return false659end660# Check if we want to write the log file to a directory661elsif ::File.directory?(pathname)662if ::File.writable?(pathname)663return true664else665print_status "No write permissions for log output directory: #{pathname}"666return false667end668# Check if the subdirectory exists669elsif ::File.directory?(pathname.dirname)670if ::File.writable?(pathname.dirname)671return true672else673print_status "No write permissions for log output directory: #{pathname.dirname}"674return false675end676else677# Else the directory doesn't exist. Check if we can create it.678begin679::FileUtils.mkdir_p(pathname.dirname)680return true681rescue ::StandardError => e682print_status "Error when trying to create directory #{pathname.dirname}: #{e.message}"683return false684end685end686end687688false689end690691# Require the appropriate readline library based on the user's preference.692#693# @return [void]694def setup_readline695require 'readline'696697# Only Windows requires a monkey-patched RbReadline698return unless Rex::Compat.is_windows699700if defined?(::RbReadline) && !defined?(RbReadline.refresh_console_handle)701::RbReadline.instance_eval do702class << self703alias_method :old_rl_move_cursor_relative, :_rl_move_cursor_relative704alias_method :old_rl_get_screen_size, :_rl_get_screen_size705alias_method :old_space_to_eol, :space_to_eol706alias_method :old_insert_some_chars, :insert_some_chars707end708709def self.refresh_console_handle710# hConsoleHandle gets set only when RbReadline detects it is running on Windows.711# Therefore, we don't need to check Rex::Compat.is_windows, we can simply check if hConsoleHandle is nil or not.712@hConsoleHandle = @GetStdHandle.Call(::Readline::STD_OUTPUT_HANDLE) if @hConsoleHandle713end714715def self._rl_move_cursor_relative(*args)716refresh_console_handle717old_rl_move_cursor_relative(*args)718end719720def self._rl_get_screen_size(*args)721refresh_console_handle722old_rl_get_screen_size(*args)723end724725def self.space_to_eol(*args)726refresh_console_handle727old_space_to_eol(*args)728end729730def self.insert_some_chars(*args)731refresh_console_handle732old_insert_some_chars(*args)733end734end735end736end737end738739end740end741end742743744