Land #7655, Refactor/cleanup core command dispatcher

bug/bundler_fix
Brent Cook 2016-12-06 16:38:42 -06:00
commit f734031804
No known key found for this signature in database
GPG Key ID: 1FFAA0B24B708F96
10 changed files with 1777 additions and 1594 deletions

View File

@ -1,7 +1,7 @@
Feature: Help command
Background:
Given I run `msfconsole --defer-module-loads -x help -x exit`
Given I run `msfconsole --defer-module-loads -q -x help -x exit`
Scenario: The 'help' command's output
Then the output should contain:
@ -12,51 +12,72 @@ Feature: Help command
Command Description
------- -----------
? Help menu
advanced Displays advanced options for one or more modules
back Move back from the current context
banner Display an awesome metasploit banner
cd Change the current working directory
color Toggle color
connect Communicate with a host
edit Edit the current module with $VISUAL or $EDITOR
exit Exit the console
get Gets the value of a context-specific variable
getg Gets the value of a global variable
grep Grep the output of another command
help Help menu
info Displays information about one or more modules
irb Drop into irb scripting mode
jobs Displays and manages jobs
kill Kill a job
load Load a framework plugin
loadpath Searches for and loads modules from a path
makerc Save commands entered since start to a file
options Displays global options or for one or more modules
popm Pops the latest module off the stack and makes it active
previous Sets the previously loaded module as the current module
pushm Pushes the active or list of modules onto the module stack
quit Exit the console
reload_all Reloads all modules from all defined module paths
rename_job Rename a job
resource Run the commands stored in a file
route Route traffic through a session
save Saves the active datastores
search Searches module names and descriptions
sess Interact with a given session
sessions Dump session listings and display information about sessions
set Sets a context-specific variable to a value
setg Sets a global variable to a value
show Displays modules of a given type, or all modules
sleep Do nothing for the specified number of seconds
spool Write console output into a file as well the screen
threads View and manipulate background threads
unload Unload a framework plugin
unset Unsets one or more context-specific variables
unsetg Unsets one or more global variables
use Selects a module by name
version Show the framework and console library version numbers
Module Commands
===============
Command Description
------- -----------
advanced Displays advanced options for one or more modules
back Move back from the current context
edit Edit the current module with $VISUAL or $EDITOR
info Displays information about one or more modules
loadpath Searches for and loads modules from a path
options Displays global options or for one or more modules
popm Pops the latest module off the stack and makes it active
previous Sets the previously loaded module as the current module
pushm Pushes the active or list of modules onto the module stack
reload_all Reloads all modules from all defined module paths
search Searches module names and descriptions
show Displays modules of a given type, or all modules
use Selects a module by name
Job Commands
============
Command Description
------- -----------
jobs Displays and manages jobs
kill Kill a job
rename_job Rename a job
Resource Script Commands
========================
Command Description
------- -----------
makerc Save commands entered since start to a file
resource Run the commands stored in a file
Database Backend Commands
=========================

View File

@ -74,6 +74,36 @@ module CommandDispatcher
dlog("Call stack:\n#{$@.join("\n")}", 'core', LEV_1)
end
#
# Generate an array of job or session IDs from a given range String.
# Always returns an Array.
#
# @param id_list [String] Range or list description such as 1-5 or 1,3,5 etc
# @return [Array<String>] Representing the range
def build_range_array(id_list)
item_list = []
unless id_list.blank?
temp_list = id_list.split(',')
temp_list.each do |ele|
return if ele.count('-') > 1
return if ele.first == '-' || ele[-1] == '-'
return if ele.first == '.' || ele[-1] == '.'
if ele.include? '-'
temp_array = (ele.split("-").inject { |s, e| s.to_i..e.to_i }).to_a
item_list.concat(temp_array)
elsif ele.include? '..'
temp_array = (ele.split("..").inject { |s, e| s.to_i..e.to_i }).to_a
item_list.concat(temp_array)
else
item_list.push(ele.to_i)
end
end
end
item_list.uniq.sort
end
#
# The driver that this command dispatcher is associated with.
#

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,215 @@
# -*- coding: binary -*-
require 'rex/ui/text/output/buffer/stdout'
module Msf
module Ui
module Console
module CommandDispatcher
#
# {CommandDispatcher} for commands related to background jobs in Metasploit Framework.
#
class Jobs
include Msf::Ui::Console::CommandDispatcher
@@jobs_opts = Rex::Parser::Arguments.new(
"-h" => [ false, "Help banner." ],
"-k" => [ true, "Terminate jobs by job ID and/or range." ],
"-K" => [ false, "Terminate all running jobs." ],
"-i" => [ true, "Lists detailed information about a running job."],
"-l" => [ false, "List all running jobs." ],
"-v" => [ false, "Print more detailed info. Use with -i and -l" ]
)
def commands
{
"jobs" => "Displays and manages jobs",
"rename_job" => "Rename a job",
"kill" => "Kill a job",
}
end
#
# Returns the name of the command dispatcher.
#
def name
"Job"
end
def cmd_rename_job_help
print_line "Usage: rename_job [ID] [Name]"
print_line
print_line "Example: rename_job 0 \"meterpreter HTTPS special\""
print_line
print_line "Rename a job that's currently active."
print_line "You may use the jobs command to see what jobs are available."
print_line
end
def cmd_rename_job(*args)
if args.include?('-h') || args.length != 2 || args[0] !~ /^\d+$/
cmd_rename_job_help
return false
end
job_id = args[0].to_s
job_name = args[1].to_s
unless framework.jobs[job_id]
print_error("Job #{job_id} does not exist.")
return false
end
# This is not respecting the Protected access control, but this seems to be the only way
# to rename a job. If you know a more appropriate way, patches accepted.
framework.jobs[job_id].send(:name=, job_name)
print_status("Job #{job_id} updated")
true
end
#
# Tab completion for the rename_job command
#
# @param str [String] the string currently being typed before tab was hit
# @param words [Array<String>] the previously completed words on the command line. words is always
# at least 1 when tab completion has reached this stage since the command itself has been completed
def cmd_rename_job_tabs(str, words)
return [] if words.length > 1
framework.jobs.keys
end
def cmd_jobs_help
print_line "Usage: jobs [options]"
print_line
print_line "Active job manipulation and interaction."
print @@jobs_opts.usage
end
#
# Displays and manages running jobs for the active instance of the
# framework.
#
def cmd_jobs(*args)
# Make the default behavior listing all jobs if there were no options
# or the only option is the verbose flag
args.unshift("-l") if args.length == 0 || args == ["-v"]
verbose = false
dump_list = false
dump_info = false
job_id = nil
# Parse the command options
@@jobs_opts.parse(args) do |opt, idx, val|
case opt
when "-v"
verbose = true
when "-l"
dump_list = true
# Terminate the supplied job ID(s)
when "-k"
job_list = build_range_array(val)
if job_list.blank?
print_error("Please specify valid job identifier(s)")
return false
end
print_status("Stopping the following job(s): #{job_list.join(', ')}")
job_list.map(&:to_s).each do |job|
if framework.jobs.has_key?(job)
print_status("Stopping job #{job}")
framework.jobs.stop_job(job)
else
print_error("Invalid job identifier: #{job}")
end
end
when "-K"
print_line("Stopping all jobs...")
framework.jobs.each_key do |i|
framework.jobs.stop_job(i)
end
when "-i"
# Defer printing anything until the end of option parsing
# so we can check for the verbose flag.
dump_info = true
job_id = val
when "-h"
cmd_jobs_help
return false
end
end
if dump_list
print("\n#{Serializer::ReadableText.dump_jobs(framework, verbose)}\n")
end
if dump_info
if job_id && framework.jobs[job_id.to_s]
job = framework.jobs[job_id.to_s]
mod = job.ctx[0]
output = '\n'
output += "Name: #{mod.name}"
output += ", started at #{job.start_time}" if job.start_time
print_line(output)
show_options(mod) if mod.options.has_options?
if verbose
mod_opt = Serializer::ReadableText.dump_advanced_options(mod, ' ')
if mod_opt && mod_opt.length > 0
print_line("\nModule advanced options:\n\n#{mod_opt}\n")
end
end
else
print_line("Invalid Job ID")
end
end
end
#
# Tab completion for the jobs command
#
# @param str [String] the string currently being typed before tab was hit
# @param words [Array<String>] the previously completed words on the command line. words is always
# at least 1 when tab completion has reached this stage since the command itself has been completed
def cmd_jobs_tabs(str, words)
if words.length == 1
return @@jobs_opts.fmt.keys
end
if words.length == 2 && (@@jobs_opts.fmt[words[1]] || [false])[0]
return framework.jobs.keys
end
[]
end
def cmd_kill_help
print_line "Usage: kill <job1> [job2 ...]"
print_line
print_line "Equivalent to 'jobs -k job1 -k job2 ...'"
print @@jobs_opts.usage
end
def cmd_kill(*args)
cmd_jobs("-k", *args)
end
#
# Tab completion for the kill command
#
# @param str [String] the string currently being typed before tab was hit
# @param words [Array<String>] the previously completed words on the command line. words is always
# at least 1 when tab completion has reached this stage since the command itself has been completed
def cmd_kill_tabs(str, words)
return [] if words.length > 1
framework.jobs.keys
end
end
end
end
end
end

File diff suppressed because it is too large Load Diff

View File

@ -3,139 +3,141 @@
require 'rex/parser/arguments'
module Msf
module Ui
module Console
module CommandDispatcher
module Ui
module Console
module CommandDispatcher
###
#
# Payload module command dispatcher.
#
###
class Payload
class Payload
include Msf::Ui::Console::ModuleCommandDispatcher
include Msf::Ui::Console::ModuleCommandDispatcher
# Load supported formats
supported_formats = Msf::Simple::Buffer.transform_formats + Msf::Util::EXE.to_executable_fmt_formats
# Load supported formats
supported_formats = Msf::Simple::Buffer.transform_formats + Msf::Util::EXE.to_executable_fmt_formats
@@generate_opts = Rex::Parser::Arguments.new(
"-b" => [ true, "The list of characters to avoid: '\\x00\\xff'" ],
"-E" => [ false, "Force encoding." ],
"-e" => [ true, "The name of the encoder module to use." ],
"-h" => [ false, "Help banner." ],
"-o" => [ true, "A comma separated list of options in VAR=VAL format." ],
"-s" => [ true, "NOP sled length." ],
"-f" => [ true, "The output file name (otherwise stdout)" ],
"-t" => [ true, "The output format: #{supported_formats.join(',')}" ],
"-p" => [ true, "The Platform for output." ],
"-k" => [ false, "Keep the template executable functional" ],
"-x" => [ true, "The executable template to use" ],
"-i" => [ true, "the number of encoding iterations." ])
@@generate_opts = Rex::Parser::Arguments.new(
"-b" => [ true, "The list of characters to avoid: '\\x00\\xff'" ],
"-E" => [ false, "Force encoding." ],
"-e" => [ true, "The name of the encoder module to use." ],
"-h" => [ false, "Help banner." ],
"-o" => [ true, "A comma separated list of options in VAR=VAL format." ],
"-s" => [ true, "NOP sled length." ],
"-f" => [ true, "The output file name (otherwise stdout)" ],
"-t" => [ true, "The output format: #{supported_formats.join(',')}" ],
"-p" => [ true, "The Platform for output." ],
"-k" => [ false, "Keep the template executable functional" ],
"-x" => [ true, "The executable template to use" ],
"-i" => [ true, "the number of encoding iterations." ])
#
# Returns the hash of commands specific to payload modules.
#
def commands
super.update({
"generate" => "Generates a payload",
})
end
#
# Returns the hash of commands specific to payload modules.
#
def commands
super.update({
"generate" => "Generates a payload",
})
end
#
# Returns the command dispatcher name.
#
def name
return "Payload"
end
#
# Returns the command dispatcher name.
#
def name
return "Payload"
end
#
# Generates a payload.
#
def cmd_generate(*args)
#
# Generates a payload.
#
def cmd_generate(*args)
# Parse the arguments
encoder_name = nil
sled_size = nil
option_str = nil
badchars = nil
type = "ruby"
ofile = nil
iter = 1
force = nil
template = nil
plat = nil
keep = false
# Parse the arguments
encoder_name = nil
sled_size = nil
option_str = nil
badchars = nil
type = "ruby"
ofile = nil
iter = 1
force = nil
template = nil
plat = nil
keep = false
@@generate_opts.parse(args) { |opt, idx, val|
case opt
when '-b'
badchars = Rex::Text.hex_to_raw(val)
when '-e'
encoder_name = val
when '-E'
force = true
when '-o'
option_str = val
when '-s'
sled_size = val.to_i
when '-t'
type = val
when '-f'
ofile = val
when '-i'
iter = val
when '-k'
keep = true
when '-p'
plat = val
when '-x'
template = val
when '-h'
print(
"Usage: generate [options]\n\n" +
"Generates a payload.\n" +
@@generate_opts.usage)
return true
end
}
if (encoder_name.nil? and mod.datastore['ENCODER'])
encoder_name = mod.datastore['ENCODER']
end
# Generate the payload
begin
buf = mod.generate_simple(
'BadChars' => badchars,
'Encoder' => encoder_name,
'Format' => type,
'NopSledSize' => sled_size,
'OptionStr' => option_str,
'ForceEncode' => force,
'Template' => template,
'Platform' => plat,
'KeepTemplateWorking' => keep,
'Iterations' => iter)
rescue
log_error("Payload generation failed: #{$!}")
return false
end
if(not ofile)
# Display generated payload
print(buf)
else
print_status("Writing #{buf.length} bytes to #{ofile}...")
fd = File.open(ofile, "wb")
fd.write(buf)
fd.close
end
@@generate_opts.parse(args) { |opt, idx, val|
case opt
when '-b'
badchars = Rex::Text.hex_to_raw(val)
when '-e'
encoder_name = val
when '-E'
force = true
when '-o'
option_str = val
when '-s'
sled_size = val.to_i
when '-t'
type = val
when '-f'
ofile = val
when '-i'
iter = val
when '-k'
keep = true
when '-p'
plat = val
when '-x'
template = val
when '-h'
print(
"Usage: generate [options]\n\n" +
"Generates a payload.\n" +
@@generate_opts.usage)
return true
end
end
end
}
if (encoder_name.nil? and mod.datastore['ENCODER'])
encoder_name = mod.datastore['ENCODER']
end
# Generate the payload
begin
buf = mod.generate_simple(
'BadChars' => badchars,
'Encoder' => encoder_name,
'Format' => type,
'NopSledSize' => sled_size,
'OptionStr' => option_str,
'ForceEncode' => force,
'Template' => template,
'Platform' => plat,
'KeepTemplateWorking' => keep,
'Iterations' => iter)
rescue
log_error("Payload generation failed: #{$!}")
return false
end
if(not ofile)
# Display generated payload
print(buf)
else
print_status("Writing #{buf.length} bytes to #{ofile}...")
fd = File.open(ofile, "wb")
fd.write(buf)
fd.close
end
return true
end
end
end end end end

View File

@ -0,0 +1,136 @@
# -*- coding: binary -*-
#
# Rex
#
require 'rex/ui/text/output/buffer/stdout'
module Msf
module Ui
module Console
module CommandDispatcher
#
# {CommandDispatcher} for commands related to background jobs in Metasploit Framework.
#
class Resource
include Msf::Ui::Console::CommandDispatcher
def commands
{
"resource" => "Run the commands stored in a file",
"makerc" => "Save commands entered since start to a file",
}
end
#
# Returns the name of the command dispatcher.
#
def name
"Resource Script"
end
def cmd_resource_help
print_line "Usage: resource path1 [path2 ...]"
print_line
print_line "Run the commands stored in the supplied files. Resource files may also contain"
print_line "ruby code between <ruby></ruby> tags."
print_line
print_line "See also: makerc"
print_line
end
def cmd_resource(*args)
if args.empty?
cmd_resource_help
return false
end
args.each do |res|
good_res = nil
if ::File.exist?(res)
good_res = res
elsif
# let's check to see if it's in the scripts/resource dir (like when tab completed)
[
::Msf::Config.script_directory + ::File::SEPARATOR + "resource",
::Msf::Config.user_script_directory + ::File::SEPARATOR + "resource"
].each do |dir|
res_path = dir + ::File::SEPARATOR + res
if ::File.exist?(res_path)
good_res = res_path
break
end
end
end
if good_res
driver.load_resource(good_res)
else
print_error("#{res} is not a valid resource file")
next
end
end
end
#
# Tab completion for the resource command
#
# @param str [String] the string currently being typed before tab was hit
# @param words [Array<String>] the previously completed words on the command line. words is always
# at least 1 when tab completion has reached this stage since the command itself has been completed
def cmd_resource_tabs(str, words)
tabs = []
#return tabs if words.length > 1
if ( str and str =~ /^#{Regexp.escape(File::SEPARATOR)}/ )
# then you are probably specifying a full path so let's just use normal file completion
return tab_complete_filenames(str,words)
elsif (not words[1] or not words[1].match(/^\//))
# then let's start tab completion in the scripts/resource directories
begin
[
::Msf::Config.script_directory + File::SEPARATOR + "resource",
::Msf::Config.user_script_directory + File::SEPARATOR + "resource",
"."
].each do |dir|
next if not ::File.exist? dir
tabs += ::Dir.new(dir).find_all { |e|
path = dir + File::SEPARATOR + e
::File.file?(path) and File.readable?(path)
}
end
rescue Exception
end
else
tabs += tab_complete_filenames(str,words)
end
return tabs
end
def cmd_makerc_help
print_line "Usage: makerc <output rc file>"
print_line
print_line "Save the commands executed since startup to the specified file."
print_line
end
#
# Saves commands executed since the ui started to the specified msfrc file
#
def cmd_makerc(*args)
if args.empty?
cmd_makerc_help
return false
end
driver.save_recent_history(args[0])
end
end
end
end
end
end

View File

@ -26,6 +26,15 @@ class Driver < Msf::Ui::Driver
DefaultPrompt = "%undmsf%clr"
DefaultPromptChar = "%clr>"
#
# Console Command Dispatchers to be loaded after the Core dispatcher.
#
CommandDispatchers = [
CommandDispatcher::Modules,
CommandDispatcher::Jobs,
CommandDispatcher::Resource
]
#
# The console driver processes various framework notified events.
#
@ -108,6 +117,10 @@ class Driver < Msf::Ui::Driver
print_error("***")
end
# Load the other "core" command dispatchers
CommandDispatchers.each do |dispatcher|
enstack_dispatcher(dispatcher)
end
# Add the database dispatcher if it is usable
if (framework.db.usable)

View File

@ -12,90 +12,6 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Core do
described_class.new(driver)
end
context '#search_modules_sql' do
def search_modules_sql
core.search_modules_sql(match)
end
let(:match) do
''
end
it 'should generate Matching Modules table' do
expect(core).to receive(:generate_module_table).with('Matching Modules').and_call_original
search_modules_sql
end
it 'should call Msf::DBManager#search_modules' do
expect(db_manager).to receive(:search_modules).with(match).and_return([])
search_modules_sql
end
context 'with matching Mdm::Module::Details' do
let(:match) do
module_detail.fullname
end
let!(:module_detail) do
FactoryGirl.create(:mdm_module_detail)
end
context 'printed table' do
def cell(table, row, column)
row_line_number = 6 + row
line_number = 0
cell = nil
table.each_line do |line|
if line_number == row_line_number
# strip prefix and postfix
padded_cells = line[3...-1]
cells = padded_cells.split(/\s{2,}/)
cell = cells[column]
break
end
line_number += 1
end
cell
end
let(:printed_table) do
table = ''
expect(core).to receive(:print_line) do |string|
table = string
end
search_modules_sql
table
end
it 'should have fullname in first column' do
expect(cell(printed_table, 0, 0)).to include(module_detail.fullname)
end
it 'should have disclosure date in second column' do
expect(cell(printed_table, 0, 1)).to include(module_detail.disclosure_date.strftime("%Y-%m-%d"))
end
it 'should have rank name in third column' do
expect(cell(printed_table, 0, 2)).to include(Msf::RankingName[module_detail.rank])
end
it 'should have name in fourth column' do
expect(cell(printed_table, 0, 3)).to include(module_detail.name)
end
end
end
end
it { is_expected.to respond_to :cmd_get }
it { is_expected.to respond_to :cmd_getg }

View File

@ -0,0 +1,98 @@
require 'spec_helper'
require 'msf/ui'
require 'msf/ui/console/module_command_dispatcher'
require 'msf/ui/console/command_dispatcher/core'
RSpec.describe Msf::Ui::Console::CommandDispatcher::Modules do
include_context 'Msf::DBManager'
include_context 'Msf::UIDriver'
subject(:core) do
described_class.new(driver)
end
context '#search_modules_sql' do
def search_modules_sql
core.search_modules_sql(match)
end
let(:match) do
''
end
it 'should generate Matching Modules table' do
expect(core).to receive(:generate_module_table).with('Matching Modules').and_call_original
search_modules_sql
end
it 'should call Msf::DBManager#search_modules' do
expect(db_manager).to receive(:search_modules).with(match).and_return([])
search_modules_sql
end
context 'with matching Mdm::Module::Details' do
let(:match) do
module_detail.fullname
end
let!(:module_detail) do
FactoryGirl.create(:mdm_module_detail)
end
context 'printed table' do
def cell(table, row, column)
row_line_number = 6 + row
line_number = 0
cell = nil
table.each_line do |line|
if line_number == row_line_number
# strip prefix and postfix
padded_cells = line[3...-1]
cells = padded_cells.split(/\s{2,}/)
cell = cells[column]
break
end
line_number += 1
end
cell
end
let(:printed_table) do
table = ''
expect(core).to receive(:print_line) do |string|
table = string
end
search_modules_sql
table
end
it 'should have fullname in first column' do
expect(cell(printed_table, 0, 0)).to include(module_detail.fullname)
end
it 'should have disclosure date in second column' do
expect(cell(printed_table, 0, 1)).to include(module_detail.disclosure_date.strftime("%Y-%m-%d"))
end
it 'should have rank name in third column' do
expect(cell(printed_table, 0, 2)).to include(Msf::RankingName[module_detail.rank])
end
it 'should have name in fourth column' do
expect(cell(printed_table, 0, 3)).to include(module_detail.name)
end
end
end
end
end