Path: blob/master/lib/msf/ui/console/command_dispatcher/developer.rb
56371 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 in this context154if framework.datastore['VERBOSE']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 context245print_status("Launching #{editor} #{path}") if framework.datastore['VERBOSE']246247unless system(*editor.split, path)248print_error("Could not execute #{editor} #{path}")249return250end251252return if editing_module253254reload_file(path)255end256257#258# Tab completion for the edit command259#260def cmd_edit_tabs(str, words)261tab_complete_filenames(str, words)262end263264def cmd_reload_lib_help265cmd_reload_lib('-h')266end267268#269# Reload Ruby library files from specified paths270#271def cmd_reload_lib(*args)272files = []273options = OptionParser.new do |opts|274opts.banner = 'Usage: reload_lib lib/to/reload.rb [...]'275opts.separator ''276opts.separator 'Reload Ruby library files from specified paths.'277opts.separator ''278279opts.on '-h', '--help', 'Help banner.' do280return print(opts.help)281end282283opts.on '-a', '--all', 'Reload all* changed files in your current Git working tree.284*Excludes modules and non-Ruby files.' do285files.concat(modified_file_paths)286end287end288289# The remaining unparsed arguments are files290files.concat(options.order(args))291files.uniq!292293return print(options.help) if files.empty?294295files.each do |file|296reload_file(file)297rescue ScriptError, StandardError => e298print_error("Error while reloading file #{file.inspect}: #{e}:\n#{e.backtrace.to_a.map { |line| " #{line}" }.join("\n")}")299end300end301302#303# Tab completion for the reload_lib command304#305def cmd_reload_lib_tabs(str, words)306tab_complete_filenames(str, words)307end308309def cmd_log_help310print_line 'Usage: log'311print_line312print_line 'Display framework.log paged to the end if possible.'313print_line 'To change the preferred pager, you can "setg LocalPager".'314print_line 'For full effect, "setg LogLevel 3" before running modules.'315print_line316print_line "Log location: #{File.join(Msf::Config.log_directory, 'framework.log')}"317print_line318end319320#321# Display framework.log paged to the end if possible322#323def cmd_log(*args)324path = File.join(Msf::Config.log_directory, 'framework.log')325326# XXX: +G isn't portable and may hang on large files327pager = local_pager.to_s.include?('less') ? "#{local_pager} +G" : local_pager328329unless pager330pager = 'tail -n 50'331print_warning("LocalPager or $PAGER/$MANPAGER should be set. Falling back on #{pager}.")332end333334# XXX: No vprint_status in this context335print_status("Launching #{pager} #{path}") if framework.datastore['VERBOSE']336337unless system(*pager.split, path)338print_error("Could not execute #{pager} #{path}")339end340end341342#343# Interact with framework's service manager344#345def cmd__servicemanager(*args)346if args.include?('-h') || args.include?('--help')347cmd__servicemanager_help348return false349end350351opts = {}352@@_servicemanager_opts.parse(args) do |opt, idx, val|353case opt354when '-l', '--list'355opts[:list] = true356end357end358359if opts.empty?360opts[:list] = true361end362363if opts[:list]364table = Rex::Text::Table.new(365'Header' => 'Services',366'Indent' => 1,367'Columns' => ['Id', 'Name', 'References']368)369Rex::ServiceManager.instance.each.with_index do |(name, instance), id|370# TODO: Update rex-core to support querying the reference count371table << [id, name, instance.instance_variable_get(:@_references)]372end373374if table.rows.empty?375print_status("No framework services are currently running.")376else377print_line(table.to_s)378end379end380end381382#383# Tab completion for the _servicemanager command384#385def cmd__servicemanager_tabs(_str, words)386return [] if words.length > 1387388@@_servicemanager_opts.option_keys389end390391def cmd__servicemanager_help392print_line 'Usage: _servicemanager'393print_line394print_line 'Manage running framework services'395print @@_servicemanager_opts.usage396print_line397end398399#400# Interact with framework's history manager401#402def cmd__historymanager(*args)403if args.include?('-h') || args.include?('--help')404cmd__historymanager_help405return false406end407408opts = {}409@@_historymanager_opts.parse(args) do |opt, idx, val|410case opt411when '-l', '--list'412opts[:list] = true413when '-d', '--debug'414opts[:debug] = val.nil? ? true : val.downcase.start_with?(/t|y/)415end416end417418if opts.empty?419opts[:list] = true420end421422if opts.key?(:debug)423framework.history_manager._debug = opts[:debug]424print_status("HistoryManager debugging is now #{opts[:debug] ? 'on' : 'off'}")425end426427if opts[:list]428table = Rex::Text::Table.new(429'Header' => 'History contexts',430'Indent' => 1,431'Columns' => ['Id', 'File', 'Name']432)433framework.history_manager._contexts.each.with_index do |context, id|434table << [id, context[:history_file], context[:name]]435end436437if table.rows.empty?438print_status("No history contexts present.")439else440print_line(table.to_s)441end442end443end444445#446# Tab completion for the _historymanager command447#448def cmd__historymanager_tabs(_str, words)449return [] if words.length > 1450451@@_historymanager_opts.option_keys452end453454def cmd__historymanager_help455print_line 'Usage: _historymanager'456print_line457print_line 'Manage the history manager'458print @@_historymanager_opts.usage459print_line460end461462#463# Time how long in seconds a command takes to execute464#465def cmd_time(*args)466if args.empty? || args.first == '-h' || args.first == '--help'467cmd_time_help468return true469end470471profiler = nil472while args.first == '--cpu' || args.first == '--memory'473profiler = args.shift474end475476begin477start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)478command = Shellwords.shelljoin(args)479480case profiler481when '--cpu'482Metasploit::Framework::Profiler.record_cpu do483driver.run_single(command)484end485when '--memory'486Metasploit::Framework::Profiler.record_memory do487driver.run_single(command)488end489else490driver.run_single(command)491end492ensure493end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)494elapsed_time = end_time - start_time495print_good("Command #{command.inspect} completed in #{elapsed_time} seconds")496end497end498499def cmd_time_help500print_line 'Usage: time [options] [command]'501print_line502print_line 'Time how long a command takes to execute in seconds. Also supports profiling options.'503print_line504print_line ' Usage:'505print_line ' * time db_import ./db_import.html'506print_line ' * time show exploits'507print_line ' * time reload_all'508print_line ' * time missing_command'509print_line ' * time --cpu db_import ./db_import.html'510print_line ' * time --memory db_import ./db_import.html'511print @@time_opts.usage512print_line513end514515protected516517def source_directories518[Msf::Config.install_root]519end520521def modified_files522# Using an array avoids shelling out, so we avoid escaping/quoting523changed_files = %w[git diff --name-only]524525is_success = true526files = []527source_directories.each do |directory|528begin529output, status = Open3.capture2e(*changed_files, chdir: directory)530is_success = status.success?531break unless is_success532533files += output.split("\n").map do |path|534realpath = Pathname.new(directory).join(path).realpath535raise "invalid path" unless realpath.to_s.start_with?(directory)536realpath.to_s537end538rescue => e539elog(e)540is_success = false541break542end543end544[files, is_success]545end546end547548549