Land #10282, Add support for running external modules outside of msfconsole

GSoC/Meterpreter_Web_Console
Brent Cook 2018-07-18 17:38:40 -05:00
commit 08290b81c0
No known key found for this signature in database
GPG Key ID: 1FFAA0B24B708F96
9 changed files with 331 additions and 33 deletions

View File

@ -5,6 +5,7 @@ class Msf::Modules::External
autoload :Bridge, 'msf/core/modules/external/bridge' autoload :Bridge, 'msf/core/modules/external/bridge'
autoload :Message, 'msf/core/modules/external/message' autoload :Message, 'msf/core/modules/external/message'
autoload :CLI, 'msf/core/modules/external/cli'
attr_reader :path attr_reader :path

View File

@ -114,7 +114,11 @@ module Msf::Modules
# stdout might have some buffered data left, so carry on # stdout might have some buffered data left, so carry on
if fds.include?(err) && !err.eof? if fds.include?(err) && !err.eof?
errbuf = err.readpartial(4096) errbuf = err.readpartial(4096)
if self.framework
elog "Unexpected output running #{self.path}:\n#{errbuf}" elog "Unexpected output running #{self.path}:\n#{errbuf}"
else
$stderr.puts errbuf
end
end end
if fds.include? out if fds.include? out
self.buf << out.readpartial(4096) self.buf << out.readpartial(4096)

118
lib/msf/core/modules/external/cli.rb vendored Normal file
View File

@ -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

View File

@ -0,0 +1,80 @@
from __future__ import print_function
import argparse
import json
import re
import sys
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def log(message, level='info'):
# logging goes to stderr
sigil = '*'
if level == 'warning' or level == 'error':
sigil = '!'
elif level == 'good':
sigil = '+'
eprint('[{}] {}'.format(sigil, message))
def report(kind, data):
# actual results go to stdout
print("[+] Found {}: {}".format(kind, json.dumps(data, separators=(',', ':'))))
def ret(result):
print(result)
def parse(meta):
parser = argparse.ArgumentParser(description=meta['description'])
actions = ['run'] + meta['capabilities']
parser.add_argument(
'action',
nargs='?',
metavar="ACTION",
help="The action to take ({})".format(actions),
default='run',
choices=actions)
required_group = parser.add_argument_group('required arguments')
for opt, props in meta['options'].items():
group = parser
desc = props['description']
required = props['required'] and (props.get('default', None) is None)
if props.get('default', None) is not None:
desc = "{}, (default: {})".format(props['description'], props['default'])
if required:
group = required_group
group.add_argument(
'--' + opt.replace('_', '-'),
help=desc,
default=props.get('default', None),
type=choose_type(props['type']),
required=required,
dest=opt)
opts = parser.parse_args()
args = vars(opts)
action = args['action']
del args['action']
return {'id': '0', 'params': args, 'method': action}
def choose_type(t):
if t == 'int' or t == 'port':
return int
elif t == 'float':
return float
elif re.search('range$', t):
return comma_list
else: # XXX TODO add validation for addresses and other MSF option types
return str
def comma_list(v):
return v.split(',')

View File

@ -8,12 +8,12 @@ def make_scanner(login_callback):
def run_scanner(args, login_callback): def run_scanner(args, login_callback):
userpass = args['userpass'] userpass = args['userpass'] or []
rhost = args['rhost'] rhost = args['rhost']
rport = int(args['rport']) rport = int(args['rport'])
sleep_interval = float(args['sleep_interval']) sleep_interval = float(args['sleep_interval'] or 0)
if isinstance(userpass, str): if isinstance(userpass, str) or isinstance(userpass, unicode):
userpass = [ attempt.split(' ', 1) for attempt in userpass.splitlines() ] userpass = [ attempt.split(' ', 1) for attempt in userpass.splitlines() ]
curr = 0 curr = 0

View File

@ -3,6 +3,10 @@ import logging
import os import os
import sys import sys
from metasploit import cli
__CLI_MODE__ = False
class LogFormatter(logging.Formatter): class LogFormatter(logging.Formatter):
def __init__(self, prefix, *args, **kwargs): def __init__(self, prefix, *args, **kwargs):
@ -37,12 +41,14 @@ class LogHandler(logging.Handler):
logger.addHandler(handler) logger.addHandler(handler)
return handler return handler
def log(message, level='info'): def log(message, level='info'):
if not __CLI_MODE__:
rpc_send({'jsonrpc': '2.0', 'method': 'message', 'params': { rpc_send({'jsonrpc': '2.0', 'method': 'message', 'params': {
'level': level, 'level': level,
'message': message 'message': message
}}) }})
else:
cli.log(message, level)
def report_host(ip, **opts): def report_host(ip, **opts):
@ -76,9 +82,8 @@ def report_wrong_password(username, password, **opts):
def run(metadata, module_callback, soft_check=None): def run(metadata, module_callback, soft_check=None):
req = json.loads(os.read(0, 10000).decode("utf-8")) global __CLI_MODE__
callback = None
if req['method'] == 'describe':
caps = [] caps = []
if soft_check: if soft_check:
caps.append('soft_check') caps.append('soft_check')
@ -86,6 +91,17 @@ def run(metadata, module_callback, soft_check=None):
meta = metadata.copy() meta = metadata.copy()
meta.update({'capabilities': caps}) meta.update({'capabilities': caps})
if len(sys.argv) > 1:
__CLI_MODE__ = True
req = None
if __CLI_MODE__:
req = cli.parse(meta)
else:
req = json.loads(os.read(0, 10000).decode("utf-8"))
callback = None
if req['method'] == 'describe':
rpc_send({'jsonrpc': '2.0', 'id': req['id'], 'result': meta}) rpc_send({'jsonrpc': '2.0', 'id': req['id'], 'result': meta})
elif req['method'] == 'soft_check': elif req['method'] == 'soft_check':
if soft_check: if soft_check:
@ -98,6 +114,9 @@ def run(metadata, module_callback, soft_check=None):
if callback: if callback:
args = req['params'] args = req['params']
ret = callback(args) ret = callback(args)
if ret and __CLI_MODE__:
cli.ret(ret)
rpc_send({'jsonrpc': '2.0', 'id': req['id'], 'result': { rpc_send({'jsonrpc': '2.0', 'id': req['id'], 'result': {
'message': 'Module completed', 'message': 'Module completed',
'return': ret 'return': ret
@ -105,11 +124,17 @@ def run(metadata, module_callback, soft_check=None):
def report(kind, data): def report(kind, data):
if not __CLI_MODE__:
rpc_send({'jsonrpc': '2.0', 'method': 'report', 'params': { rpc_send({'jsonrpc': '2.0', 'method': 'report', 'params': {
'type': kind, 'data': data 'type': kind, 'data': data
}}) }})
else:
cli.report(kind, data)
def rpc_send(req): def rpc_send(req):
# Silently ignore when run manually, the calling code should know how to
# handle if it is important
if not __CLI_MODE__:
print(json.dumps(req)) print(json.dumps(req))
sys.stdout.flush() sys.stdout.flush()

View File

@ -38,18 +38,14 @@ class Msf::Modules::External::Shim
render_template('common_check.erb', meta) render_template('common_check.erb', meta)
end end
def self.mod_meta_common(mod, meta = {}, drop_rhost: false) def self.mod_meta_common(mod, meta = {}, ignore_options: [])
meta[:path] = mod.path.dump meta[:path] = mod.path.dump
meta[:name] = mod.meta['name'].dump meta[:name] = mod.meta['name'].dump
meta[:description] = mod.meta['description'].dump meta[:description] = mod.meta['description'].dump
meta[:authors] = mod.meta['authors'].map(&:dump).join(",\n ") meta[:authors] = mod.meta['authors'].map(&:dump).join(",\n ")
meta[:license] = mod.meta['license'].nil? ? 'MSF_LICENSE' : mod.meta['license'] meta[:license] = mod.meta['license'].nil? ? 'MSF_LICENSE' : mod.meta['license']
options = if drop_rhost options = mod.meta['options'].reject {|n, _| ignore_options.include? n}
mod.meta['options'].reject {|n, o| n == 'rhost'}
else
mod.meta['options']
end
meta[:options] = options.map do |n, o| meta[:options] = options.map do |n, o|
if o['values'] if o['values']
@ -92,7 +88,7 @@ class Msf::Modules::External::Shim
end end
def self.remote_exploit_cmd_stager(mod) def self.remote_exploit_cmd_stager(mod)
meta = mod_meta_common(mod) meta = mod_meta_common(mod, ignore_options: ['command'])
meta = mod_meta_exploit(mod, meta) meta = mod_meta_exploit(mod, meta)
meta[:command_stager_flavor] = mod.meta['payload']['command_stager_flavor'].dump meta[:command_stager_flavor] = mod.meta['payload']['command_stager_flavor'].dump
render_template('remote_exploit_cmd_stager.erb', meta) render_template('remote_exploit_cmd_stager.erb', meta)
@ -104,7 +100,7 @@ class Msf::Modules::External::Shim
end end
def self.single_scanner(mod) def self.single_scanner(mod)
meta = mod_meta_common(mod, drop_rhost: true) meta = mod_meta_common(mod, ignore_options: ['rhost'])
meta[:date] = mod.meta['date'].dump meta[:date] = mod.meta['date'].dump
meta[:references] = mod.meta['references'].map do |r| meta[:references] = mod.meta['references'].map do |r|
"[#{r['type'].upcase.dump}, #{r['ref'].dump}]" "[#{r['type'].upcase.dump}, #{r['ref'].dump}]"
@ -114,7 +110,7 @@ class Msf::Modules::External::Shim
end end
def self.single_host_login_scanner(mod) def self.single_host_login_scanner(mod)
meta = mod_meta_common(mod, drop_rhost: true) meta = mod_meta_common(mod, ignore_options: ['rhost'])
meta[:date] = mod.meta['date'].dump meta[:date] = mod.meta['date'].dump
meta[:references] = mod.meta['references'].map do |r| meta[:references] = mod.meta['references'].map do |r|
"[#{r['type'].upcase.dump}, #{r['ref'].dump}]" "[#{r['type'].upcase.dump}, #{r['ref'].dump}]"

View File

@ -52,7 +52,8 @@ metadata = {
'email_to': {'type': 'string', 'description': 'Email to send to, must be accepted by the server', 'required': True, 'default': 'admin@localhost'}, 'email_to': {'type': 'string', 'description': 'Email to send to, must be accepted by the server', 'required': True, 'default': 'admin@localhost'},
'email_from': {'type': 'string', 'description': 'Address to send from', 'required': True, 'default': 'foo@example.com'}, 'email_from': {'type': 'string', 'description': 'Address to send from', 'required': True, 'default': 'foo@example.com'},
'rhost': {'type': 'address', 'description': 'Target server', 'required': True, 'default': None}, 'rhost': {'type': 'address', 'description': 'Target server', 'required': True, 'default': None},
'rport': {'type': 'port', 'description': 'Target server port', 'required': True, 'default': 25} 'rport': {'type': 'port', 'description': 'Target server port', 'required': True, 'default': 25},
'command': {'type': 'string', 'description': 'Command to run on the target', 'required': True, 'default': '/bin/echo hello'}
}} }}
@ -110,7 +111,11 @@ def create_zip(cmd="touch /tmp/harakiri"):
def check_banner(args): def check_banner(args):
module.log('{}:{} Starting banner check for Haraka < 2.8.9'.format(args['rhost'], args['rport']), level='debug') module.log('{}:{} Starting banner check for Haraka < 2.8.9'.format(args['rhost'], args['rport']), level='debug')
c = smtplib.SMTP() c = smtplib.SMTP()
try:
(code, banner) = c.connect(args['rhost'], int(args['rport'])) (code, banner) = c.connect(args['rhost'], int(args['rport']))
except:
return 'unknown'
c.quit() c.quit()
if code == 220 and 'Haraka' in banner: if code == 220 and 'Haraka' in banner:

69
tools/modules/solo.rb Executable file
View File

@ -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