diff --git a/lib/msf/ui/console/command_dispatcher/core.rb b/lib/msf/ui/console/command_dispatcher/core.rb index 800b885369..10c9528831 100644 --- a/lib/msf/ui/console/command_dispatcher/core.rb +++ b/lib/msf/ui/console/command_dispatcher/core.rb @@ -106,6 +106,7 @@ class Core "history" => "Show command history", "load" => "Load a framework plugin", "quit" => "Exit the console", + "repeat" => "Repeat a list of commands", "route" => "Route traffic through a session", "save" => "Saves the active datastores", "sessions" => "Dump session listings and display information about sessions", @@ -2013,8 +2014,8 @@ class Core end # OptionParser#order allows us to take the rest of the line for the command - pattern, *cmd = opts.order(args) - cmd = cmd.join(" ") + pattern, *rest = opts.order(args) + cmd = Shellwords.shelljoin(rest) return print(opts.help) if !pattern || cmd.empty? rx = Regexp.new(pattern, match_mods[:insensitive]) @@ -2038,7 +2039,7 @@ class Core # Bail if the command failed if cmd_output =~ /Unknown command:/ - print_error("Unknown command: #{args[0]}.") + print_error("Unknown command: '#{rest[0]}'.") return false end # put lines into an array so we can access them more easily and split('\n') doesn't work on the output obj. @@ -2087,6 +2088,81 @@ class Core tabs end + def cmd_repeat_help + cmd_repeat '-h' + end + + # + # Repeats (loops) a given list of commands + # + def cmd_repeat(*args) + looper = method :loop + + opts = OptionParser.new do |opts| + opts.banner = 'Usage: repeat [OPTIONS] COMMAND...' + opts.separator 'Repeat (loop) a ;-separated list of msfconsole commands indefinitely, or for a' + opts.separator 'number of iterations or a certain amount of time.' + opts.separator '' + + opts.on '-t SECONDS', '--time SECONDS', 'Number of seconds to repeat COMMAND...', Integer do |n| + looper = ->(&block) do + # While CLOCK_MONOTONIC is a Linux thing, Ruby emulates it for *BSD, MacOS, and Windows + ending_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) + n + while Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) < ending_time + block.call + end + end + end + + opts.on '-n TIMES', '--number TIMES', 'Number of times to repeat COMMAND..', Integer do |n| + looper = n.method(:times) + end + + opts.on '-h', '--help', 'Help banner.' do + return print(opts.help) + end + + # Internal use + opts.on '--generate-completions str', 'Return possible tab completions for given string.' do |str| + return opts.candidate str + end + end + + cmds = opts.order(args).slice_when do |prev, _| + # If the last character of a shellword was a ';' it's probably to + # delineate commands and we can remove it + prev[-1] == ';' && prev[-1] = '' + end.map do |c| + Shellwords.shelljoin(c) + end + + # Print help if we have no commands, or all the commands are empty + return cmd_repeat '-h' if cmds.all? &:empty? + + begin + looper.call do + cmds.each do |c| + driver.run_single c, propagate_errors: true + end + end + rescue ::Exception + # Stop looping on exception + nil + end + end + + # Almost the exact same as grep + def cmd_repeat_tabs(str, words) + str = '-' if str.empty? # default to use repeat's options + tabs = cmd_repeat '--generate-completions', str + + # if not an opt, use normal tab comp. + # @todo uncomment out next line when tab_completion normalization is complete RM7649 or + # replace with new code that permits "nested" tab completion + # tabs = driver.get_all_commands if (str and str =~ /\w/) + tabs + end + # # Provide tab completion for option values # diff --git a/lib/rex/ui/text/dispatcher_shell.rb b/lib/rex/ui/text/dispatcher_shell.rb index c91d02bfeb..3b6b972202 100644 --- a/lib/rex/ui/text/dispatcher_shell.rb +++ b/lib/rex/ui/text/dispatcher_shell.rb @@ -432,7 +432,7 @@ module DispatcherShell # # Run a single command line. # - def run_single(line) + def run_single(line, propagate_errors: false) arguments = parse_line(line) method = arguments.shift found = false @@ -453,17 +453,27 @@ module DispatcherShell run_command(dispatcher, method, arguments) found = true end + rescue ::Interrupt + print_error("#{method}: Interrupted") + raise if propagate_errors + rescue OptionParser::ParseError => e + print_error("#{method}: #{e.message}") + raise if propagate_errors rescue error = $! print_error( "Error while running command #{method}: #{$!}" + "\n\nCall stack:\n#{$@.join("\n")}") - rescue ::Exception + + raise if propagate_errors + rescue ::Exception => e error = $! print_error( "Error while running command #{method}: #{$!}") + + raise if propagate_errors end # If the dispatcher stack changed as a result of this command, @@ -490,8 +500,6 @@ module DispatcherShell else dispatcher.send('cmd_' + method, *arguments) end - rescue OptionParser::ParseError => e - print_error("#{method}: #{e.message}") ensure self.busy = false end diff --git a/modules/auxiliary/scanner/ssl/openssl_heartbleed.rb b/modules/auxiliary/scanner/ssl/openssl_heartbleed.rb index b155975db5..59bb1dfbaa 100644 --- a/modules/auxiliary/scanner/ssl/openssl_heartbleed.rb +++ b/modules/auxiliary/scanner/ssl/openssl_heartbleed.rb @@ -109,7 +109,10 @@ class MetasploitModule < Msf::Auxiliary STARTTLS may also be vulnerable. The module supports several actions, allowing for scanning, dumping of - memory contents, and private key recovery. + memory contents, and private key recovery. The `repeat` command can be + used to make running the `DUMP` many times more convenient. As in: + repeat -t 60 run; sleep 2 + To run every two seconds for one minute. }, 'Author' => [ 'Neel Mehta', # Vulnerability discovery