Path: blob/master/lib/msf/ui/console/command_dispatcher/developer.rb
19758 views
# -*- coding: binary -*-12class Msf::Ui::Console::CommandDispatcher::Developer34include Msf::Ui::Console::CommandDispatcher56@@irb_opts = Rex::Parser::Arguments.new(7'-h' => [false, 'Help menu.' ],8'-e' => [true, 'Expression to evaluate.']9)1011@@time_opts = Rex::Parser::Arguments.new(12['-h', '--help'] => [ false, 'Help banner.' ],13'--cpu' => [false, 'Profile the CPU usage.'],14'--memory' => [false, 'Profile the memory usage.']15)1617@@_servicemanager_opts = Rex::Parser::Arguments.new(18['-l', '--list'] => [false, 'View the currently running services' ]19)2021@@_historymanager_opts = Rex::Parser::Arguments.new(22'-h' => [false, 'Help menu.' ],23['-l', '--list'] => [true, 'View the current history manager contexts.'],24['-d', '--debug'] => [true, 'Debug the current history manager contexts.']25)2627def initialize(driver)28super29@modified_files = modified_file_paths(print_errors: false)30end3132def name33'Developer'34end3536def commands37commands = {38'irb' => 'Open an interactive Ruby shell in the current context',39'pry' => 'Open the Pry debugger on the current module or Framework',40'edit' => 'Edit the current module or a file with the preferred editor',41'reload_lib' => 'Reload Ruby library files from specified paths',42'log' => 'Display framework.log paged to the end if possible',43'time' => 'Time how long it takes to run a particular command'44}45if framework.features.enabled?(Msf::FeatureManager::MANAGER_COMMANDS)46commands['_servicemanager'] = 'Interact with the Rex::ServiceManager'47commands['_historymanager'] = 'Interact with the Rex::Ui::Text::Shell::HistoryManager'48end49commands50end5152def local_editor53framework.datastore['LocalEditor'] ||54Rex::Compat.getenv('VISUAL') ||55Rex::Compat.getenv('EDITOR') ||56Msf::Util::Helper.which('vim') ||57Msf::Util::Helper.which('vi')58end5960def local_pager61framework.datastore['LocalPager'] ||62Rex::Compat.getenv('PAGER') ||63Rex::Compat.getenv('MANPAGER') ||64Msf::Util::Helper.which('less') ||65Msf::Util::Helper.which('more')66end6768# XXX: This will try to reload *any* .rb and break on modules69def reload_file(full_path, print_errors: true)70unless File.exist?(full_path) && full_path.end_with?('.rb')71print_error("#{full_path} must exist and be a .rb file") if print_errors72return73end7475# The file must exist to reach this, so we try our best here76if full_path.start_with?(Msf::Config.module_directory, Msf::Config.user_module_directory)77print_error('Reloading Metasploit modules is not supported (try "reload")') if print_errors78return79end8081print_status("Reloading #{full_path}")82load full_path83end8485# @return [Array<String>] The list of modified file paths since startup86def modified_file_paths(print_errors: true)87files, is_success = modified_files8889unless is_success90print_error("Git is not available") if print_errors91files = []92end9394ignored_patterns = %w[95**/Gemfile96**/Gemfile.lock97**/*_spec.rb98**/spec_helper.rb99]100@modified_files ||= []101@modified_files |= files.reject do |file|102ignored_patterns.any? { |pattern| File.fnmatch(pattern, file) }103end104@modified_files105end106107def cmd_irb_help108print_line 'Usage: irb'109print_line110print_line 'Open an interactive Ruby shell in the current context.'111print @@irb_opts.usage112end113114#115# Open an interactive Ruby shell in the current context116#117def cmd_irb(*args)118expressions = []119120# Parse the command options121@@irb_opts.parse(args) do |opt, idx, val|122case opt123when '-e'124expressions << val125when '-h'126cmd_irb_help127return false128end129end130131if expressions.empty?132print_status('Starting IRB shell...')133134framework.history_manager.with_context(name: :irb) do135begin136if active_module137print_status("You are in #{active_module.fullname}\n")138Rex::Ui::Text::IrbShell.new(active_module).run139else140print_status("You are in the \"framework\" object\n")141Rex::Ui::Text::IrbShell.new(framework).run142end143rescue144print_error("Error during IRB: #{$!}\n\n#{$@.join("\n")}")145end146end147148# Reset tab completion149if (driver.input.supports_readline)150driver.input.reset_tab_completion151end152else153# XXX: No vprint_status here either154if framework.datastore['VERBOSE'].to_s == 'true'155print_status("You are executing expressions in #{binding.receiver}")156end157158expressions.each { |expression| eval(expression, binding) }159end160end161162#163# Tab completion for the irb command164#165def cmd_irb_tabs(_str, words)166return [] if words.length > 1167168@@irb_opts.option_keys169end170171def cmd_pry_help172print_line 'Usage: pry'173print_line174print_line 'Open the Pry debugger on the current module or Framework.'175print_line176end177178#179# Open the Pry debugger on the current module or Framework180#181def cmd_pry(*args)182if args.include?('-h')183cmd_pry_help184return185end186187begin188require 'pry'189rescue LoadError190print_error('Failed to load Pry, try "gem install pry"')191return192end193194print_status('Starting Pry shell...')195196Pry.config.history_load = false197framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do198if active_module199print_status("You are in the \"#{active_module.fullname}\" module object\n")200active_module.pry201else202print_status("You are in the \"framework\" object\n")203framework.pry204end205end206end207208def cmd_edit_help209print_line 'Usage: edit [file/to/edit]'210print_line211print_line "Edit the currently active module or a local file with #{local_editor}."212print_line 'To change the preferred editor, you can "setg LocalEditor".'213print_line 'If a library file is specified, it will automatically be reloaded after editing.'214print_line 'Otherwise, you can reload the active module with "reload" or "rerun".'215print_line216end217218#219# Edit the current module or a file with the preferred editor220#221def cmd_edit(*args)222editing_module = false223224if args.length > 0225path = File.expand_path(args[0])226elsif active_module227editing_module = true228path = active_module.file_path229end230231unless path232print_error('Nothing to edit. Try using a module first or specifying a library file to edit.')233return234end235236editor = local_editor237238unless editor239# ed(1) is the standard editor240editor = 'ed'241print_warning("LocalEditor or $VISUAL/$EDITOR should be set. Falling back on #{editor}.")242end243244# XXX: No vprint_status in this context?245# XXX: VERBOSE is a string instead of Bool??246print_status("Launching #{editor} #{path}") if framework.datastore['VERBOSE'].to_s == 'true'247248unless system(*editor.split, path)249print_error("Could not execute #{editor} #{path}")250return251end252253return if editing_module254255reload_file(path)256end257258#259# Tab completion for the edit command260#261def cmd_edit_tabs(str, words)262tab_complete_filenames(str, words)263end264265def cmd_reload_lib_help266cmd_reload_lib('-h')267end268269#270# Reload Ruby library files from specified paths271#272def cmd_reload_lib(*args)273files = []274options = OptionParser.new do |opts|275opts.banner = 'Usage: reload_lib lib/to/reload.rb [...]'276opts.separator ''277opts.separator 'Reload Ruby library files from specified paths.'278opts.separator ''279280opts.on '-h', '--help', 'Help banner.' do281return print(opts.help)282end283284opts.on '-a', '--all', 'Reload all* changed files in your current Git working tree.285*Excludes modules and non-Ruby files.' do286files.concat(modified_file_paths)287end288end289290# The remaining unparsed arguments are files291files.concat(options.order(args))292files.uniq!293294return print(options.help) if files.empty?295296files.each do |file|297reload_file(file)298rescue ScriptError, StandardError => e299print_error("Error while reloading file #{file.inspect}: #{e}:\n#{e.backtrace.to_a.map { |line| " #{line}" }.join("\n")}")300end301end302303#304# Tab completion for the reload_lib command305#306def cmd_reload_lib_tabs(str, words)307tab_complete_filenames(str, words)308end309310def cmd_log_help311print_line 'Usage: log'312print_line313print_line 'Display framework.log paged to the end if possible.'314print_line 'To change the preferred pager, you can "setg LocalPager".'315print_line 'For full effect, "setg LogLevel 3" before running modules.'316print_line317print_line "Log location: #{File.join(Msf::Config.log_directory, 'framework.log')}"318print_line319end320321#322# Display framework.log paged to the end if possible323#324def cmd_log(*args)325path = File.join(Msf::Config.log_directory, 'framework.log')326327# XXX: +G isn't portable and may hang on large files328pager = local_pager.to_s.include?('less') ? "#{local_pager} +G" : local_pager329330unless pager331pager = 'tail -n 50'332print_warning("LocalPager or $PAGER/$MANPAGER should be set. Falling back on #{pager}.")333end334335# XXX: No vprint_status in this context?336# XXX: VERBOSE is a string instead of Bool??337print_status("Launching #{pager} #{path}") if framework.datastore['VERBOSE'].to_s == 'true'338339unless system(*pager.split, path)340print_error("Could not execute #{pager} #{path}")341end342end343344#345# Interact with framework's service manager346#347def cmd__servicemanager(*args)348if args.include?('-h') || args.include?('--help')349cmd__servicemanager_help350return false351end352353opts = {}354@@_servicemanager_opts.parse(args) do |opt, idx, val|355case opt356when '-l', '--list'357opts[:list] = true358end359end360361if opts.empty?362opts[:list] = true363end364365if opts[:list]366table = Rex::Text::Table.new(367'Header' => 'Services',368'Indent' => 1,369'Columns' => ['Id', 'Name', 'References']370)371Rex::ServiceManager.instance.each.with_index do |(name, instance), id|372# TODO: Update rex-core to support querying the reference count373table << [id, name, instance.instance_variable_get(:@_references)]374end375376if table.rows.empty?377print_status("No framework services are currently running.")378else379print_line(table.to_s)380end381end382end383384#385# Tab completion for the _servicemanager command386#387def cmd__servicemanager_tabs(_str, words)388return [] if words.length > 1389390@@_servicemanager_opts.option_keys391end392393def cmd__servicemanager_help394print_line 'Usage: _servicemanager'395print_line396print_line 'Manage running framework services'397print @@_servicemanager_opts.usage398print_line399end400401#402# Interact with framework's history manager403#404def cmd__historymanager(*args)405if args.include?('-h') || args.include?('--help')406cmd__historymanager_help407return false408end409410opts = {}411@@_historymanager_opts.parse(args) do |opt, idx, val|412case opt413when '-l', '--list'414opts[:list] = true415when '-d', '--debug'416opts[:debug] = val.nil? ? true : val.downcase.start_with?(/t|y/)417end418end419420if opts.empty?421opts[:list] = true422end423424if opts.key?(:debug)425framework.history_manager._debug = opts[:debug]426print_status("HistoryManager debugging is now #{opts[:debug] ? 'on' : 'off'}")427end428429if opts[:list]430table = Rex::Text::Table.new(431'Header' => 'History contexts',432'Indent' => 1,433'Columns' => ['Id', 'File', 'Name']434)435framework.history_manager._contexts.each.with_index do |context, id|436table << [id, context[:history_file], context[:name]]437end438439if table.rows.empty?440print_status("No history contexts present.")441else442print_line(table.to_s)443end444end445end446447#448# Tab completion for the _historymanager command449#450def cmd__historymanager_tabs(_str, words)451return [] if words.length > 1452453@@_historymanager_opts.option_keys454end455456def cmd__historymanager_help457print_line 'Usage: _historymanager'458print_line459print_line 'Manage the history manager'460print @@_historymanager_opts.usage461print_line462end463464#465# Time how long in seconds a command takes to execute466#467def cmd_time(*args)468if args.empty? || args.first == '-h' || args.first == '--help'469cmd_time_help470return true471end472473profiler = nil474while args.first == '--cpu' || args.first == '--memory'475profiler = args.shift476end477478begin479start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)480command = Shellwords.shelljoin(args)481482case profiler483when '--cpu'484Metasploit::Framework::Profiler.record_cpu do485driver.run_single(command)486end487when '--memory'488Metasploit::Framework::Profiler.record_memory do489driver.run_single(command)490end491else492driver.run_single(command)493end494ensure495end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)496elapsed_time = end_time - start_time497print_good("Command #{command.inspect} completed in #{elapsed_time} seconds")498end499end500501def cmd_time_help502print_line 'Usage: time [options] [command]'503print_line504print_line 'Time how long a command takes to execute in seconds. Also supports profiling options.'505print_line506print_line ' Usage:'507print_line ' * time db_import ./db_import.html'508print_line ' * time show exploits'509print_line ' * time reload_all'510print_line ' * time missing_command'511print_line ' * time --cpu db_import ./db_import.html'512print_line ' * time --memory db_import ./db_import.html'513print @@time_opts.usage514print_line515end516517protected518519def source_directories520[Msf::Config.install_root]521end522523def modified_files524# Using an array avoids shelling out, so we avoid escaping/quoting525changed_files = %w[git diff --name-only]526527is_success = true528files = []529source_directories.each do |directory|530begin531output, status = Open3.capture2e(*changed_files, chdir: directory)532is_success = status.success?533break unless is_success534535files += output.split("\n").map do |path|536realpath = Pathname.new(directory).join(path).realpath537raise "invalid path" unless realpath.to_s.start_with?(directory)538realpath.to_s539end540rescue => e541elog(e)542is_success = false543break544end545end546[files, is_success]547end548end549550551