Path: blob/master/lib/msf/ui/console/driver.rb
21546 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%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)72end7374# Check if files have been modified and force immediate loading if so75has_modified_metasploit_files = !Msf::Modules::Metadata::Store.valid_checksum?7677if has_modified_metasploit_files78current_checksum = Msf::Modules::Metadata::Store.get_current_checksum79Msf::Modules::Metadata::Store.update_cache_checksum(current_checksum)80# Force immediate module loading when files have changed81opts['DeferModuleLoads'] = false82end8384if opts['DeferModuleLoads'].nil?85opts['DeferModuleLoads'] = Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DEFER_MODULE_LOADS)86end8788# Initialize attributes8990framework_create_options = opts.merge({ 'DeferModuleLoads' => true })9192if Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DNS)93dns_resolver = Rex::Proto::DNS::CachedResolver.new94dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider)95dns_resolver.load_config if dns_resolver.has_config?9697# Defer loading of modules until paths from opts can be added below98framework_create_options = framework_create_options.merge({ 'CustomDnsResolver' => dns_resolver })99end100self.framework = opts['Framework'] || Msf::Simple::Framework.create(framework_create_options)101102if self.framework.datastore['Prompt']103prompt = self.framework.datastore['Prompt']104prompt_char = self.framework.datastore['PromptChar'] || DefaultPromptChar105end106107# Call the parent108super(prompt, prompt_char, histfile, framework, :msfconsole)109110# Temporarily disable output111self.disable_output = true112113# Load pre-configuration114load_preconfig115116# Initialize the user interface to use a different input and output117# handle if one is supplied118input = opts['LocalInput']119input ||= Rex::Ui::Text::Input::Stdio.new120121if !opts['Readline']122input.disable_readline123end124125if (opts['LocalOutput'])126if (opts['LocalOutput'].kind_of?(String))127output = Rex::Ui::Text::Output::File.new(opts['LocalOutput'])128else129output = opts['LocalOutput']130end131else132output = Rex::Ui::Text::Output::Stdio.new133end134135init_ui(input, output)136init_tab_complete137138# Add the core command dispatcher as the root of the dispatcher139# stack140enstack_dispatcher(CommandDispatcher::Core)141142# Load the other "core" command dispatchers143CommandDispatchers.each do |dispatcher_class|144dispatcher = enstack_dispatcher(dispatcher_class)145dispatcher.load_config(opts['Config'])146end147148if !framework.db || !framework.db.active149if framework.db.error == "disabled"150print_warning("Database support has been disabled")151else152error_msg = "#{framework.db.error.class.is_a?(String) ? "#{framework.db.error.class} " : nil}#{framework.db.error}"153print_warning("No database support: #{error_msg}")154end155end156157# Register event handlers158register_event_handlers159160# Re-enable output161self.disable_output = false162163# Whether or not command passthru should be allowed164self.command_passthru = opts.fetch('AllowCommandPassthru', true)165166# Whether or not to confirm before exiting167self.confirm_exit = opts['ConfirmExit']168169# Initialize the module paths only if we didn't get passed a Framework instance and 'DeferModuleLoads' is false170unless opts['Framework']171# Configure the framework module paths172self.framework.init_module_paths(module_paths: opts['ModulePath'], defer_module_loads: opts['DeferModuleLoads'])173end174175# Refresh module cache if modules are modified, or we're not deferring loads176if has_modified_metasploit_files || !opts['DeferModuleLoads']177framework.threads.spawn("ModuleCacheRebuild", true) do178framework.modules.refresh_cache_from_module_files179end180end181182# Load console-specific configuration (after module paths are added)183load_config(opts['Config'])184185# Process things before we actually display the prompt and get rocking186on_startup(opts)187188# Process any resource scripts189if opts['Resource'].blank?190# None given, load the default191default_resource = ::File.join(Msf::Config.config_directory, 'msfconsole.rc')192load_resource(default_resource) if ::File.exist?(default_resource)193else194opts['Resource'].each { |r|195load_resource(r)196}197end198199# Process persistent job handler200begin201restore_handlers = JSON.parse(File.read(Msf::Config.persist_file))202rescue Errno::ENOENT, JSON::ParserError203restore_handlers = nil204end205206if restore_handlers207print_status("Starting persistent handler(s)...")208209restore_handlers.each.with_index do |handler_opts, index|210handler = framework.modules.create(handler_opts['mod_name'])211handler.init_ui(self.input, self.output)212replicant_handler = nil213handler.exploit_simple(handler_opts['mod_options']) do |yielded_replicant_handler|214replicant_handler = yielded_replicant_handler215end216217if replicant_handler.nil? || replicant_handler.error218print_status("Failed to start persistent payload handler ##{index} (#{handler_opts['mod_name']})")219next220end221222if replicant_handler.error.nil?223job_id = handler.job_id224print_status "Persistent payload handler started as Job #{job_id}"225end226end227end228229# Process any additional startup commands230if opts['XCommands'] and opts['XCommands'].kind_of? Array231opts['XCommands'].each { |c|232run_single(c)233}234end235end236237#238# Loads configuration that needs to be analyzed before the framework239# instance is created.240#241def load_preconfig242begin243conf = Msf::Config.load244rescue245wlog("Failed to load configuration: #{$!}")246return247end248249if (conf.group?(ConfigCore))250conf[ConfigCore].each_pair { |k, v|251on_variable_set(true, k, v)252}253end254end255256#257# Loads configuration for the console.258#259def load_config(path=nil)260begin261conf = Msf::Config.load(path)262rescue263wlog("Failed to load configuration: #{$!}")264return265end266267# If we have configuration, process it268if (conf.group?(ConfigGroup))269conf[ConfigGroup].each_pair { |k, v|270case k.downcase271when 'activemodule'272run_single("use #{v}")273when 'activeworkspace'274if framework.db.active275workspace = framework.db.find_workspace(v)276framework.db.workspace = workspace if workspace277end278end279}280end281end282283#284# Generate configuration for the console.285#286def get_config287# Build out the console config group288group = {}289290if (active_module)291group['ActiveModule'] = active_module.fullname292end293294if framework.db.active295unless framework.db.workspace.default?296group['ActiveWorkspace'] = framework.db.workspace.name297end298end299300group301end302303def get_config_core304ConfigCore305end306307def get_config_group308ConfigGroup309end310311#312# Saves configuration for the console.313#314def save_config315begin316Msf::Config.save(ConfigGroup => get_config)317rescue ::Exception318print_error("Failed to save console config: #{$!}")319end320end321322#323# Saves the recent history to the specified file324#325def save_recent_history(path)326num = Readline::HISTORY.length - hist_last_saved - 1327328tmprc = ""329num.times { |x|330tmprc << Readline::HISTORY[hist_last_saved + x] + "\n"331}332333if tmprc.length > 0334print_status("Saving last #{num} commands to #{path} ...")335save_resource(tmprc, path)336else337print_error("No commands to save!")338end339340# Always update this, even if we didn't save anything. We do this341# so that we don't end up saving the "makerc" command itself.342self.hist_last_saved = Readline::HISTORY.length343end344345#346# Creates the resource script file for the console.347#348def save_resource(data, path=nil)349path ||= File.join(Msf::Config.config_directory, 'msfconsole.rc')350351begin352rcfd = File.open(path, 'w')353rcfd.write(data)354rcfd.close355rescue ::Exception356end357end358359#360# Called before things actually get rolling such that banners can be361# displayed, scripts can be processed, and other fun can be had.362#363def on_startup(opts = {})364# Check for modules that failed to load365if framework.modules.module_load_error_by_path.length > 0366wlog("The following modules could not be loaded!")367368framework.modules.module_load_error_by_path.each do |path, _error|369wlog("\t#{path}")370end371end372373if framework.modules.module_load_warnings.length > 0374print_warning("The following modules were loaded with warnings:")375376framework.modules.module_load_warnings.each do |path, _error|377wlog("\t#{path}")378end379end380381if framework.db&.active382framework.db.workspace = framework.db.default_workspace unless framework.db.workspace383end384385framework.events.on_ui_start(Msf::Framework::Revision)386387if $msf_spinner_thread388$msf_spinner_thread.kill389$stderr.print "\r" + (" " * 50) + "\n"390end391392run_single("banner") unless opts['DisableBanner']393394payloads_manifest_errors = []395begin396payloads_manifest_errors = ::MetasploitPayloads.manifest_errors if framework.features.enabled?(::Msf::FeatureManager::METASPLOIT_PAYLOAD_WARNINGS)397rescue ::StandardError => e398$stderr.print('Could not verify the integrity of the Metasploit Payloads manifest')399elog(e)400end401402av_warning_message if (framework.eicar_corrupted? || payloads_manifest_errors.any?)403404if framework.features.enabled?(::Msf::FeatureManager::METASPLOIT_PAYLOAD_WARNINGS)405if payloads_manifest_errors.any?406warn_msg = "Metasploit Payloads manifest errors:\n"407payloads_manifest_errors.each do |file|408warn_msg << "\t#{file[:path]} : #{file[:error]}\n"409end410$stderr.print(warn_msg)411end412end413414opts["Plugins"].each do |plug|415run_single("load '#{plug}'")416end if opts["Plugins"]417418self.on_command_proc = Proc.new { |command| framework.events.on_ui_command(command) }419end420421def av_warning_message422avdwarn = "\e[31m"\423"Warning: This copy of the Metasploit Framework has been corrupted by an installed anti-virus program."\424" We recommend that you disable your anti-virus or exclude your Metasploit installation path, "\425"then restore the removed files from quarantine or reinstall the framework.\e[0m"\426"\n\n"427428$stderr.puts(Rex::Text.wordwrap(avdwarn, 0, 80))429end430431#432# Called when a variable is set to a specific value. This allows the433# console to do extra processing, such as enabling logging or doing434# some other kind of task. If this routine returns false it will indicate435# that the variable is not being set to a valid value.436#437def on_variable_set(glob, var, val)438case var.downcase439when 'sessionlogging'440handle_session_logging(val) if glob441when 'sessiontlvlogging'442handle_session_tlv_logging(val) if glob443when 'consolelogging'444handle_console_logging(val) if glob445when 'loglevel'446handle_loglevel(val) if glob447when 'payload'448handle_payload(val)449when 'ssh_ident'450handle_ssh_ident(val)451end452end453454#455# Called when a variable is unset. If this routine returns false it is an456# indication that the variable should not be allowed to be unset.457#458def on_variable_unset(glob, var)459case var.downcase460when 'sessionlogging'461handle_session_logging('0') if glob462when 'sessiontlvlogging'463handle_session_tlv_logging('false') if glob464when 'consolelogging'465handle_console_logging('0') if glob466when 'loglevel'467handle_loglevel(nil) if glob468end469end470471#472# Proxies to shell.rb's update prompt with our own extras473#474def update_prompt(*args)475if args.empty?476pchar = framework.datastore['PromptChar'] || DefaultPromptChar477p = framework.datastore['Prompt'] || DefaultPrompt478p = "#{p} #{active_module.type}(%bld%red#{active_module.promptname}%clr)" if active_module479super(p, pchar)480else481# Don't squash calls from within lib/rex/ui/text/shell.rb482super(*args)483end484end485486#487# The framework instance associated with this driver.488#489attr_reader :framework490#491# Whether or not to confirm before exiting492#493attr_reader :confirm_exit494#495# Whether or not commands can be passed through.496#497attr_reader :command_passthru498#499# The active module associated with the driver.500#501attr_accessor :active_module502#503# The active session associated with the driver.504#505attr_accessor :active_session506507def stop508framework.events.on_ui_stop()509super510end511512protected513514attr_writer :framework # :nodoc:515attr_writer :confirm_exit # :nodoc:516attr_writer :command_passthru # :nodoc:517518#519# If an unknown command was passed, try to see if it's a valid local520# executable. This is only allowed if command passthru has been permitted521#522def unknown_command(method, line)523if File.basename(method) == 'msfconsole'524print_error('msfconsole cannot be run inside msfconsole')525return526end527528[method, method+".exe"].each do |cmd|529if command_passthru && Rex::FileUtils.find_full_path(cmd)530531self.busy = true532begin533run_unknown_command(line)534rescue ::Errno::EACCES, ::Errno::ENOENT535print_error("Permission denied exec: #{line}")536end537self.busy = false538return539end540end541542if framework.modules.create(method)543super544if prompt_yesno "This is a module we can load. Do you want to use #{method}?"545run_single "use #{method}"546end547548return549end550551super552end553554def run_unknown_command(command)555print_status("exec: #{command}")556print_line('')557system(command)558end559560##561#562# Handlers for various global configuration values563#564##565566#567# SessionLogging.568#569def handle_session_logging(val)570if (val =~ /^(y|t|1)/i)571Msf::Logging.enable_session_logging(true)572framework.sessions.values.each do |session|573Msf::Logging.start_session_log(session)574end575print_line("Session logging enabled.")576else577Msf::Logging.enable_session_logging(false)578framework.sessions.values.each do |session|579Msf::Logging.stop_session_log(session)580end581print_line("Session logging disabled.")582end583end584585#586# ConsoleLogging.587#588def handle_console_logging(val)589if (val =~ /^(y|t|1)/i)590Msf::Logging.enable_log_source('console')591print_line("Console logging is now enabled.")592593set_log_source('console')594595rlog("\n[*] Console logging started: #{Time.now}\n\n", 'console')596else597rlog("\n[*] Console logging stopped: #{Time.now}\n\n", 'console')598599unset_log_source600601Msf::Logging.disable_log_source('console')602print_line("Console logging is now disabled.")603end604end605606#607# This method handles adjusting the global log level threshold.608#609def handle_loglevel(val)610set_log_level(Rex::LogSource, val)611set_log_level(Msf::LogSource, val)612end613614#615# This method handles setting a desired payload616#617# TODO: Move this out of the console driver!618#619def handle_payload(val)620if framework && !framework.payloads.valid?(val)621return false622elsif active_module && (active_module.exploit? || active_module.evasion?)623return false unless active_module.is_payload_compatible?(val)624end625end626627#628# This method monkeypatches Net::SSH's client identification string629#630# TODO: Move this out of the console driver!631#632def handle_ssh_ident(val)633# HACK: Suppress already initialized constant warning634verbose, $VERBOSE = $VERBOSE, nil635636return false unless val.is_a?(String) && !val.empty?637638require 'net/ssh'639640# HACK: Bypass dynamic constant assignment error641::Net::SSH::Transport::ServerVersion.const_set(:PROTO_VERSION, val)642643true644rescue LoadError645print_error('Net::SSH could not be loaded')646false647rescue NameError648print_error('Invalid constant Net::SSH::Transport::ServerVersion::PROTO_VERSION')649false650ensure651# Restore warning652$VERBOSE = verbose653end654655def handle_session_tlv_logging(val)656return false if val.nil?657658if val.casecmp?('console') || val.casecmp?('true') || val.casecmp?('false')659return true660elsif val.start_with?('file:') && !val.split('file:').empty?661pathname = ::Pathname.new(val.split('file:').last)662663# Check if we want to write the log to file664if ::File.file?(pathname)665if ::File.writable?(pathname)666return true667else668print_status "No write permissions for log output file: #{pathname}"669return false670end671# Check if we want to write the log file to a directory672elsif ::File.directory?(pathname)673if ::File.writable?(pathname)674return true675else676print_status "No write permissions for log output directory: #{pathname}"677return false678end679# Check if the subdirectory exists680elsif ::File.directory?(pathname.dirname)681if ::File.writable?(pathname.dirname)682return true683else684print_status "No write permissions for log output directory: #{pathname.dirname}"685return false686end687else688# Else the directory doesn't exist. Check if we can create it.689begin690::FileUtils.mkdir_p(pathname.dirname)691return true692rescue ::StandardError => e693print_status "Error when trying to create directory #{pathname.dirname}: #{e.message}"694return false695end696end697end698699false700end701702# Require the appropriate readline library based on the user's preference.703#704# @return [void]705def setup_readline706require 'readline'707708# Only Windows requires a monkey-patched RbReadline709return unless Rex::Compat.is_windows710711if defined?(::RbReadline) && !defined?(RbReadline.refresh_console_handle)712::RbReadline.instance_eval do713class << self714alias_method :old_rl_move_cursor_relative, :_rl_move_cursor_relative715alias_method :old_rl_get_screen_size, :_rl_get_screen_size716alias_method :old_space_to_eol, :space_to_eol717alias_method :old_insert_some_chars, :insert_some_chars718end719720def self.refresh_console_handle721# hConsoleHandle gets set only when RbReadline detects it is running on Windows.722# Therefore, we don't need to check Rex::Compat.is_windows, we can simply check if hConsoleHandle is nil or not.723@hConsoleHandle = @GetStdHandle.Call(::Readline::STD_OUTPUT_HANDLE) if @hConsoleHandle724end725726def self._rl_move_cursor_relative(*args)727refresh_console_handle728old_rl_move_cursor_relative(*args)729end730731def self._rl_get_screen_size(*args)732refresh_console_handle733old_rl_get_screen_size(*args)734end735736def self.space_to_eol(*args)737refresh_console_handle738old_space_to_eol(*args)739end740741def self.insert_some_chars(*args)742refresh_console_handle743old_insert_some_chars(*args)744end745end746end747end748end749750end751end752end753754755