Add support for long arguments to Rex Parser

This commit is contained in:
sjanusz 2021-12-02 16:27:18 +00:00
parent 950e976f7b
commit 063c3936a9
No known key found for this signature in database
GPG Key ID: 62086A0F9E2BB842
12 changed files with 221 additions and 105 deletions

View File

@ -33,22 +33,22 @@ class Core
# Session command options
@@sessions_opts = Rex::Parser::Arguments.new(
"-c" => [ true, "Run a command on the session given with -i, or all" ],
"-C" => [ true, "Run a Meterpreter Command on the session given with -i, or all" ],
"-h" => [ false, "Help banner" ],
"-i" => [ true, "Interact with the supplied session ID" ],
"-l" => [ false, "List all active sessions" ],
"-v" => [ false, "List all active sessions in verbose mode" ],
"-d" => [ false, "List all inactive sessions" ],
"-q" => [ false, "Quiet mode" ],
"-k" => [ true, "Terminate sessions by session ID and/or range" ],
"-K" => [ false, "Terminate all sessions" ],
"-s" => [ true, "Run a script or module on the session given with -i, or all" ],
"-u" => [ true, "Upgrade a shell to a meterpreter session on many platforms" ],
"-t" => [ true, "Set a response timeout (default: 15)" ],
"-S" => [ true, "Row search filter." ],
"-x" => [ false, "Show extended information in the session table" ],
"-n" => [ true, "Name or rename a session by ID" ])
["-c", "--command"] => [ true, "Run a command on the session given with -i, or all" ],
["-C", "--meterpreter-command"] => [ true, "Run a Meterpreter Command on the session given with -i, or all" ],
["-h", "--help"] => [ false, "Help banner" ],
["-i", "--interact"] => [ true, "Interact with the supplied session ID" ],
["-l", "--list-active"] => [ false, "List all active sessions" ],
["-v", "--list-verbose"] => [ false, "List all active sessions in verbose mode" ],
["-d", "--list-inactive"] => [ false, "List all inactive sessions" ],
["-q", "--quiet"] => [ false, "Quiet mode" ],
["-k", "--kill"] => [ true, "Terminate sessions by session ID and/or range" ],
["-K", "--kill-all"] => [ false, "Terminate all sessions" ],
["-s", "--script"] => [ true, "Run a script or module on the session given with -i, or all" ],
["-u", "--upgrade"] => [ true, "Upgrade a shell to a meterpreter session on many platforms" ],
["-t", "--timeout"] => [ true, "Set a response timeout (default: 15)" ],
["-S", "--search"] => [ true, "Row search filter." ],
["-x", "--list-extended"] => [ false, "Show extended information in the session table" ],
["-n", "--name"] => [ true, "Name or rename a session by ID" ])
@@threads_opts = Rex::Parser::Arguments.new(
@ -730,7 +730,7 @@ class Core
def cmd_history_tabs(str, words)
return [] if words.length > 1
@@history_opts.fmt.keys
@@history_opts.option_keys
end
def cmd_sleep_help
@ -867,10 +867,10 @@ class Core
def cmd_threads_tabs(str, words)
if words.length == 1
return @@threads_opts.fmt.keys
return @@threads_opts.option_keys
end
if words.length == 2 and (@@threads_opts.fmt[words[1]] || [false])[0]
if words.length == 2 && @@threads_opts.include?(words[1]) && @@threads_opts.arg_required?(words[1])
return framework.threads.each_index.map{ |idx| idx.to_s }
end
@ -1363,54 +1363,54 @@ class Core
# Parse the command options
@@sessions_opts.parse(args) do |opt, idx, val|
case opt
when "-q"
when "-q", "--quiet"
quiet = true
# Run a command on all sessions, or the session given with -i
when "-c"
when "-c", "--command"
method = 'cmd'
cmds << val if val
when "-C"
when "-C", "--meterpreter-command"
method = 'meterp-cmd'
cmds << val if val
# Display the list of inactive sessions
when "-d"
when "-d", "--list-inactive"
show_inactive = true
method = 'list_inactive'
when "-x"
when "-x", "--list-extended"
show_extended = true
when "-v"
when "-v", "--list-verbose"
verbose = true
# Do something with the supplied session identifier instead of
# all sessions.
when "-i"
when "-i", "--interact"
sid = val
# Display the list of active sessions
when "-l"
when "-l", "--list-active"
show_active = true
method = 'list'
when "-k"
when "-k", "--kill"
method = 'kill'
sid = val || false
when "-K"
when "-K", "--kill-all"
method = 'killall'
# Run a script or module on specified sessions
when "-s"
when "-s", "--script"
unless script
method = 'script'
script = val
end
# Upload and exec to the specific command session
when "-u"
when "-u", "--upgrade"
method = 'upexec'
sid = val || false
# Search for specific session
when "-S", "--search"
search_term = val
# Display help banner
when "-h"
when "-h", "--help"
cmd_sessions_help
return false
when "-t"
when "-t", "--timeout"
if val.to_s =~ /^\d+$/
response_timeout = val.to_i
end
@ -1713,17 +1713,17 @@ class Core
def cmd_sessions_tabs(str, words)
if words.length == 1
return @@sessions_opts.fmt.keys.select { |opt| opt.start_with?(str) }
return @@sessions_opts.option_keys.select { |opt| opt.start_with?(str) }
end
case words[-1]
when "-i", "-k", "-u"
when "-i", "--interact", "-k", "--kill", "-u", "--upgrade"
return framework.sessions.keys.map { |k| k.to_s }
when "-c"
when "-c", "--command"
# Can't really complete commands hehe
when "-s"
when "-s", "--search"
# XXX: Complete scripts
end

View File

@ -144,7 +144,7 @@ class Msf::Ui::Console::CommandDispatcher::Developer
def cmd_irb_tabs(_str, words)
return [] if words.length > 1
@@irb_opts.fmt.keys
@@irb_opts.option_keys
end
def cmd_pry_help

View File

@ -289,9 +289,9 @@ module Msf
# at least 1 when tab completion has reached this stage since the command itself has been completed
def cmd_jobs_tabs(_str, words)
return @@jobs_opts.fmt.keys if words.length == 1
return @@jobs_opts.option_keys if words.length == 1
if words.length == 2 && (@@jobs_opts.fmt[words[1]] || [false])[0]
if words.length == 2 && @@jobs_opts.include?(words[1]) && @@jobs_opts.arg_required?(words[1])
return framework.jobs.keys
end

View File

@ -538,7 +538,7 @@ module Msf
def cmd_search_tabs(str, words)
if words.length == 1
return @@search_opts.fmt.keys
return @@search_opts.option_keys
end
[]

View File

@ -87,7 +87,7 @@ module ModuleActionCommands
# at least 1 when tab completion has reached this stage since the command itself has been completed
#
def cmd_run_tabs(str, words)
flags = @@module_opts_with_action_support.fmt.keys
flags = @@module_opts_with_action_support.option_keys
options = tab_complete_option(active_module, str, words)
flags + options
end

View File

@ -24,18 +24,18 @@ module ModuleArgumentParsing
'-q' => [ false, 'Run the module in quiet mode with no output' ]
)
@@module_opts_with_action_support = Rex::Parser::Arguments.new(@@module_opts.fmt.merge(
@@module_opts_with_action_support = @@module_opts.merge(
'-a' => [ true, 'The action to use. If none is specified, ACTION is used.']
))
)
@@exploit_opts = Rex::Parser::Arguments.new(@@module_opts.fmt.merge(
@@exploit_opts = @@module_opts.merge(
'-e' => [ true, 'The payload encoder to use. If none is specified, ENCODER is used.' ],
'-f' => [ false, 'Force the exploit to run regardless of the value of MinimumRank.' ],
'-n' => [ true, 'The NOP generator to use. If none is specified, NOP is used.' ],
'-p' => [ true, 'The payload to use. If none is specified, PAYLOAD is used.' ],
'-t' => [ true, 'The target index to use. If none is specified, TARGET is used.' ],
'-z' => [ false, 'Do not interact with the session after successful exploitation.' ]
))
)
def parse_check_opts(args)
help_cmd = proc do |_result|

View File

@ -16,12 +16,17 @@ module Rex
# Initializes the format list with an array of formats like:
#
# Arguments.new(
# '-b' => [ false, "some text" ]
# '-b' => [ false, "some text" ],
# ['-b'] => [ false, "some text" ],
# '--sample' => [ false, "sample long arg" ],
# '--also-a-sample' => [ false, "sample longer arg" ],
# ['-x', '--execute'] => [ true, "mixing long and short args" ]
# )
#
def initialize(fmt)
self.fmt = fmt
self.longest = fmt.keys.max_by(&:length)
normalised_fmt = fmt.map { |key, metadata| [Array(key), metadata] }.to_h
self.fmt = normalised_fmt
self.longest = normalised_fmt.keys.map { |key| key.flatten.join(', ') }.max_by(&:length)
end
#
@ -35,6 +40,11 @@ module Rex
# Parses the supplied arguments into a set of options.
#
def parse(args, &_block)
# e.g. '-x' or '-xyz'
short_flag = /^-[a-zA-Z]+$/
# e.g. '--verbose', '--very-verbose' or '--very-very-verbose'
long_flag = /^--([a-zA-Z]+)((-[a-zA-Z]+)*)$/
skip_next = false
args.each_with_index do |arg, idx|
@ -43,22 +53,27 @@ module Rex
next
end
if arg.length > 1 && arg[0] == '-' && arg[1] != '-'
arg.split('').each do |flag|
fmt.each_pair do |fmtspec, val|
next if fmtspec != "-#{flag}"
param = nil
if arg =~ short_flag
arg.split('')[1..-1].each do |letter|
next unless include?("-#{letter}")
param = nil
if val[0]
param = args[idx + 1]
skip_next = true
end
yield fmtspec, idx, param
if arg_required?("-#{letter}")
param = args[idx + 1]
skip_next = true
end
yield "-#{letter}", idx, param
end
elsif arg =~ long_flag && include?(arg)
if arg_required?(arg)
param = args[idx + 1]
skip_next = true
end
yield arg, idx, param
else
# else treat the passed in flag as argument
yield nil, idx, arg
end
end
@ -70,10 +85,15 @@ module Rex
def usage
txt = ["\nOPTIONS:\n"]
fmt.sort.each do |entry|
fmtspec, val = entry
fmt.sort_by { |key, _metadata| key.to_s.downcase }.each do |key, val|
opt = val[0] ? " <opt> " : " "
txt << " #{fmtspec.ljust(longest.length)}#{opt}#{val[1]}"
# Get all arguments for a command
output = key.join(', ')
output += opt
# Left align the fmt options and <opt> string
txt << " #{(output).ljust((longest + opt).length)}#{val[1]}"
end
txt << ""
@ -81,15 +101,38 @@ module Rex
end
def include?(search)
fmt.include?(search)
fmt.keys.flatten.include?(search)
end
def arg_required?(opt)
fmt[opt][0] if fmt[opt]
value = select_value_from_fmt_option(opt)
return false if value.nil?
value.first
end
def option_keys
fmt.keys.flatten
end
# Return new Parser object featuring options from the base object and including the options hash that was passed in
def merge(to_merge)
return fmt unless to_merge.is_a?(Hash)
Rex::Parser::Arguments.new(fmt.clone.merge(to_merge))
end
private
attr_accessor :fmt # :nodoc:
attr_accessor :longest # :nodoc:
def select_value_from_fmt_option(option)
fmt_option = fmt.find { |key, value| value if key.include?(option) }
return if fmt_option.nil?
fmt_option[1]
end
end
end
end

View File

@ -166,7 +166,7 @@ class Console::CommandDispatcher::Core
end
def cmd_pivot_tabs(str, words)
return %w[list add remove] + @@pivot_opts.fmt.keys if words.length == 1
return %w[list add remove] + @@pivot_opts.option_keys if words.length == 1
case words[-1]
when '-a'
@ -180,7 +180,7 @@ class Console::CommandDispatcher::Core
when '-t'
return ['pipe']
when 'add', 'remove'
return @@pivot_opts.fmt.keys
return @@pivot_opts.option_keys
end
[]
@ -436,7 +436,7 @@ class Console::CommandDispatcher::Core
def cmd_channel_tabs(str, words)
case words.length
when 1
@@channel_opts.fmt.keys
@@channel_opts.option_keys
when 2
case words[1]
when '-k', '-c', '-i', '-r', '-w'
@ -558,7 +558,7 @@ class Console::CommandDispatcher::Core
def cmd_irb_tabs(str, words)
return [] if words.length > 1
@@irb_opts.fmt.keys
@@irb_opts.option_keys
end
#
@ -645,7 +645,7 @@ class Console::CommandDispatcher::Core
def cmd_set_timeouts_tabs(str, words)
return [] if words.length > 1
@@set_timeouts_opts.fmt.keys
@@set_timeouts_opts.option_keys
end
def cmd_set_timeouts(*args)
@ -887,7 +887,7 @@ class Console::CommandDispatcher::Core
end
def cmd_transport_tabs(str, words)
return %w[list change add next prev remove] + @@transport_opts.fmt.keys if words.length == 1
return %w[list change add next prev remove] + @@transport_opts.option_keys if words.length == 1
case words[-1]
when '-c'
@ -899,7 +899,7 @@ class Console::CommandDispatcher::Core
when '-t'
return %w[reverse_tcp reverse_http reverse_https bind_tcp]
when 'add', 'remove', 'change'
return @@transport_opts.fmt.keys
return @@transport_opts.option_keys
end
[]

View File

@ -359,7 +359,7 @@ class Console::CommandDispatcher::Stdapi::Net
end
def cmd_route_tabs(str, words)
return %w[add delete list] + @@route_opts.fmt.keys if words.length == 1
return %w[add delete list] + @@route_opts.option_keys if words.length == 1
end
#
@ -577,7 +577,7 @@ class Console::CommandDispatcher::Stdapi::Net
end
def cmd_portfwd_tabs(str, words)
return %w[add delete list flush] + @@portfwd_opts.fmt.keys if words.length == 1
return %w[add delete list flush] + @@portfwd_opts.option_keys if words.length == 1
case words[-1]
when '-L'
@ -587,7 +587,7 @@ class Console::CommandDispatcher::Stdapi::Net
return (1..client.pfservice.each_tcp_relay { |lh, lp, rh, rp, opts| }.length).to_a.map!(&:to_s)
end
when 'add', 'delete', 'list', 'flush'
return @@portfwd_opts.fmt.keys
return @@portfwd_opts.option_keys
end
[]
@ -671,4 +671,3 @@ end
end
end
end

View File

@ -37,9 +37,9 @@ class Console::CommandDispatcher::Stdapi::Sys
"-p" => [ false, "Execute process in a pty (if available on target platform)" ],
"-s" => [ true, "Execute process in a given session as the session user" ])
@@execute_opts_with_raw_mode = Rex::Parser::Arguments.new(@@execute_opts.fmt.merge(
@@execute_opts_with_raw_mode = @@execute_opts.merge(
{ '-r' => [ false, 'Raw mode'] }
))
)
#
# Options used by the 'shell' command.
@ -49,9 +49,9 @@ class Console::CommandDispatcher::Stdapi::Sys
"-l" => [ false, "List available shells (/etc/shells)." ],
"-t" => [ true, "Spawn a PTY shell (/bin/bash if no argument given)." ]) # ssh(1) -t
@@shell_opts_with_fully_interactive_shell = Rex::Parser::Arguments.new(@@shell_opts.fmt.merge(
@@shell_opts_with_fully_interactive_shell = @@shell_opts.merge(
{ '-i' => [ false, 'Drop into a fully interactive shell. (Only used in conjunction with `-t`).'] }
))
)
#
# Options used by the 'reboot' command.
@ -298,7 +298,7 @@ class Console::CommandDispatcher::Stdapi::Sys
end
def cmd_execute_tabs(str, words)
return execute_opts.fmt.keys if words.length == 1
return execute_opts.option_keys if words.length == 1
[]
end
@ -310,7 +310,7 @@ class Console::CommandDispatcher::Stdapi::Sys
end
def cmd_shell_tabs(str, words)
return shell_opts.fmt.keys if words.length == 1
return shell_opts.option_keys if words.length == 1
[]
end
@ -819,7 +819,7 @@ class Console::CommandDispatcher::Stdapi::Sys
# Tab completion for the ps command
#
def cmd_ps_tabs(str, words)
return @@ps_opts.fmt.keys if words.length == 1
return @@ps_opts.option_keys if words.length == 1
case words[-1]
when '-A'
@ -1104,7 +1104,7 @@ class Console::CommandDispatcher::Stdapi::Sys
#
def cmd_reg_tabs(str, words)
if words.length == 1
return %w[enumkey createkey deletekey queryclass setval deleteval queryval] + @@reg_opts.fmt.keys
return %w[enumkey createkey deletekey queryclass setval deleteval queryval] + @@reg_opts.option_keys
end
case words[-1]
@ -1123,7 +1123,7 @@ class Console::CommandDispatcher::Stdapi::Sys
when '-w'
return %w[32 64]
when 'enumkey', 'createkey', 'deletekey', 'queryclass', 'setval', 'deleteval', 'queryval'
return @@reg_opts.fmt.keys
return @@reg_opts.option_keys
end
[]
@ -1247,7 +1247,7 @@ class Console::CommandDispatcher::Stdapi::Sys
end
def cmd_shutdown_tabs(str, words)
return @@shutdown_opts.fmt.keys if words.length == 1
return @@shutdown_opts.option_keys if words.length == 1
case words[-1]
when '-f'
@ -1336,7 +1336,7 @@ class Console::CommandDispatcher::Stdapi::Sys
# Tab completion for the suspend command
#
def cmd_suspend_tabs(str, words)
return @@suspend_opts.fmt.keys if words.length == 1
return @@suspend_opts.option_keys if words.length == 1
[]
end

View File

@ -154,7 +154,7 @@ class Plugin::Alias < Msf::Plugin
def cmd_alias_tabs(str, words)
if words.length <= 1
#puts "1 word or less"
return @@alias_opts.fmt.keys + tab_complete_aliases_and_commands
return @@alias_opts.option_keys + tab_complete_aliases_and_commands
else
#puts "more than 1 word"
return tab_complete_aliases_and_commands

View File

@ -3,10 +3,11 @@ require 'rspec'
RSpec.describe Rex::Parser::Arguments do
let(:subject) do
Rex::Parser::Arguments.new(
'-h' => [false, 'Help banner.'],
'-j' => [false, 'Run in the context of a job.'],
'-o' => [true, 'A comma separated list of options in VAR=VAL format.'],
'-q' => [false, 'Run the module in quiet mode with no output']
['-h', '--help'] => [false, 'Help banner.'],
['-j', '--job'] => [false, 'Run in the context of a job.'],
'--long-flag-with-no-corresponding-short-option-name' => [false, 'A long flag with no corresponding short option name'],
['-o', '--options'] => [true, 'A comma separated list of options in VAR=VAL format.'],
['-q', '--quiet'] => [false, 'Run the module in quiet mode with no output']
)
end
@ -41,23 +42,89 @@ RSpec.describe Rex::Parser::Arguments do
it 'ignores unknown flags' do
input = ['-a', 'action_name']
# Not sure if this flag dropping is intentional behavior
expected_yields = [
# '-a' is dropped, 'action_name' is used as an argument
[nil, 1, 'action_name'],
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
it 'ignores combined flags that do not exist' do
it 'treats combined flags that do not exist as an argument' do
input = ['-unknown-flags']
# Not sure if this flag dropping is intentional behavior
expected_yields = [
['-o', 0, nil],
[nil, 0, '-unknown-flags']
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
end
it 'parses a single long flag correctly' do
input = ['--help']
expected_yields = [
['--help', 0, nil]
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
it 'parses multiple long flags correctly' do
input = ['--help', '--job']
expected_yields = [
['--help', 0, nil],
['--job', 1, nil]
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
it 'parses a long flag and short flag correctly' do
input = ['--help', '-h']
expected_yields = [
['--help', 0, nil],
['-h', 1, nil]
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
it 'parses a short flag when Rex Arguments are in an array correctly' do
input = ['-o']
expected_yields = [
['-o', 0, nil]
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
it 'parses a long flag when in arguments array correctly' do
input = ['--options', 'option-arg']
expected_yields = [
['--options', 0, 'option-arg']
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
it 'parses multiple long flags when in an arguments array correctly' do
input = ['--quiet', '--options', 'sample-option']
expected_yields = [
['--quiet', 0, nil],
['--options', 1, 'sample-option']
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
it 'parses a non-existent long flag correctly' do
input = ['--ultra-quiet']
expected_yields = [
[nil, 0, '--ultra-quiet']
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
it 'parses a long flag that is not in an array correctly' do
input = ['--long-flag-with-no-corresponding-short-option-name']
expected_yields = [
['--long-flag-with-no-corresponding-short-option-name', 0, nil]
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
context 'when arguments are supplied' do
it 'treats an ip address as an argument' do
input = ['127.0.0.1']
@ -82,15 +149,22 @@ RSpec.describe Rex::Parser::Arguments do
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
end
end
it 'treats long-form flags as arguments' do
input = ['--foo', '123']
expected_yields = [
[nil, 0, '--foo'],
[nil, 1, '123'],
]
expect { |b| subject.parse(input, &b) }.to yield_successive_args(*expected_yields)
end
describe '#inspect' do
it 'prints usage in a sorted order correctly' do
expected_output = <<~EXPECTED
OPTIONS:
--long-flag-with-no-corresponding-short-option-name A long flag with no corresponding short option name
-h, --help Help banner.
-j, --job Run in the context of a job.
-o, --options <opt> A comma separated list of options in VAR=VAL format.
-q, --quiet Run the module in quiet mode with no output
EXPECTED
expect(subject.usage).to eq(expected_output)
end
end
end