From 0dd89bf42808439dc61562ee842de9a0df1de6da Mon Sep 17 00:00:00 2001 From: Adam Cammack Date: Tue, 3 Jul 2018 10:10:53 -0500 Subject: [PATCH] Add standalone runner for external modules --- lib/msf/core/modules/external.rb | 1 + lib/msf/core/modules/external/cli.rb | 118 +++++++++++++++++++++++++++ tools/modules/solo.rb | 69 ++++++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 lib/msf/core/modules/external/cli.rb create mode 100755 tools/modules/solo.rb diff --git a/lib/msf/core/modules/external.rb b/lib/msf/core/modules/external.rb index 4784561577..3b5775a58c 100644 --- a/lib/msf/core/modules/external.rb +++ b/lib/msf/core/modules/external.rb @@ -5,6 +5,7 @@ class Msf::Modules::External autoload :Bridge, 'msf/core/modules/external/bridge' autoload :Message, 'msf/core/modules/external/message' + autoload :CLI, 'msf/core/modules/external/cli' attr_reader :path diff --git a/lib/msf/core/modules/external/cli.rb b/lib/msf/core/modules/external/cli.rb new file mode 100644 index 0000000000..6bc871fe21 --- /dev/null +++ b/lib/msf/core/modules/external/cli.rb @@ -0,0 +1,118 @@ +# -*- coding: binary -*- +# CLI for interaction with modules outside of msfconsole + +require 'optparse' + +module Msf::Modules::External::CLI + def self.parse_options(mod) + action = 'run' + actions = ['run'] + mod.meta['capabilities'] + args = mod.meta['options'].reduce({}) do |defaults, (n, opt)| + if opt['default'].nil? + if opt['required'] + defaults + else + defaults[n] = nil + defaults + end + else + defaults[n] = opt['default'] + defaults + end + end + + op = OptionParser.new do |opts| + if $0 != mod.path + opts.banner = "Usage: #{$0} #{mod.path} [OPTIONS] [ACTION]" + end + opts.separator "" + + opts.separator mod.meta['description'] + opts.separator "" + + opts.separator "Postitional arguments:" + opts.separator " ACTION: The action to take (#{actions.inspect})" + opts.separator "" + + opts.separator "Required arguments:" + make_options opts, args, mod.meta['options'].select {|n, o| o['required'] && o['default'].nil?} + opts.separator "" + + opts.separator "Optional arguments:" + make_options opts, args, mod.meta['options'].select {|n, o| !o['required'] || !o['default'].nil?} + + opts.on '-h', '--help', 'Prints this help' do + $stderr.puts opts + exit + end + end + + begin + extra = op.permute *ARGV + # If no extra args are given we use the default action + if extra.length == 1 + action = extra.shift + elsif extra.length > 1 + action = extra.shift + $stderr.puts "WARNING: unrecognized arguments #{extra.inspect}" + end + rescue OptionParser::InvalidArgument => e + $stderr.puts e.message + abort + rescue OptionParser::MissingArgument => e + $stderr.puts e.message + abort + end + + required = mod.meta['options'].select {|_, o| o['required']}.map {|n, _| n}.sort + + # Were we run with any non-module options if we need them? + if args.empty? && !required.empty? + $stderr.puts op + exit + # Did someone forget to add some options we need? + elsif (args.keys & required).sort != required + missing = required - (args.keys & required) + abort "Missing required option(s): #{missing.map {|o| '--' + o}.join ', '}" + end + + unless action == 'run' || mod.meta['capabilities'].include?(action) + $stderr.puts "Invalid ACTION choice #{action.inspect} (choose from #{actions.inspect})" + abort + end + + action = + case action + when 'run'; :run + when 'soft_check'; :soft_check + when 'hard_check'; :hard_check + end + [args, action] + end + + def self.choose_type(t) + if t == 'int' or t == 'port' + Integer + elsif t == 'float' + Float + elsif t.match /range$/ + Array + else # XXX TODO add validation for addresses and other MSF option types + String + end + end + + def self.make_options(parser, out, args) + args.each do |n, opt| + name = n.gsub '_', '-' + desc = if opt['default'] + "#{opt['description']}, (default: #{opt['default']})" + else + opt['description'] + end + parser.on "--#{name} #{n.upcase}", choose_type(opt['type']), desc do |arg| + out[n] = arg + end + end + end +end diff --git a/tools/modules/solo.rb b/tools/modules/solo.rb new file mode 100755 index 0000000000..ef6c64e4c8 --- /dev/null +++ b/tools/modules/solo.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby + +module Msf + module Modules + end +end + +msfbase = __FILE__ +while File.symlink?(msfbase) + msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase)) +end + +$:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', '..', 'lib'))) +require 'msf/core/modules/external' + +require 'json' + +module_path = ARGV.shift + +# Usage when we don't have a module name +def usage(mod='MODULE_FILE', name='Run a module outside of Metasploit Framework') + $stderr.puts "Usage: solo.rb #{mod} [OPTIONS] [ACTION]" + $stderr.puts name +end + +def log_output(m) + message = m.params['message'] + + sigil = case m.params['level'] + when 'error', 'warning' + '!' + when 'good' + '+' + else + '*' + end + + $stderr.puts "[#{sigil}] #{message}" +end + +def process_report(m) + puts "[+] Found #{m.params['type']}: #{JSON.generate m.params['data']}" +end + +if !module_path || module_path[0] == '-' + usage +else + mod = Msf::Modules::External.new module_path + args, method = Msf::Modules::External::CLI.parse_options mod + + success = mod.exec(method: method, args: args) do |m| + begin + case m.method + when :message + log_output(m) + when :report + process_report(m) + when :reply + puts m.params['return'] + end + rescue Interrupt => e + abort 'Exiting...' + rescue Exception => e + abort "Encountered an error: #{e.message}" + end + end + + abort 'Module exited abnormally' if !success +end