Land #10282, Add support for running external modules outside of msfconsole
commit
08290b81c0
|
@ -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
|
||||
|
||||
|
|
|
@ -114,7 +114,11 @@ module Msf::Modules
|
|||
# stdout might have some buffered data left, so carry on
|
||||
if fds.include?(err) && !err.eof?
|
||||
errbuf = err.readpartial(4096)
|
||||
if self.framework
|
||||
elog "Unexpected output running #{self.path}:\n#{errbuf}"
|
||||
else
|
||||
$stderr.puts errbuf
|
||||
end
|
||||
end
|
||||
if fds.include? out
|
||||
self.buf << out.readpartial(4096)
|
||||
|
|
|
@ -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
|
|
@ -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(',')
|
|
@ -8,12 +8,12 @@ def make_scanner(login_callback):
|
|||
|
||||
|
||||
def run_scanner(args, login_callback):
|
||||
userpass = args['userpass']
|
||||
userpass = args['userpass'] or []
|
||||
rhost = args['rhost']
|
||||
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() ]
|
||||
|
||||
curr = 0
|
||||
|
|
|
@ -3,6 +3,10 @@ import logging
|
|||
import os
|
||||
import sys
|
||||
|
||||
from metasploit import cli
|
||||
|
||||
__CLI_MODE__ = False
|
||||
|
||||
|
||||
class LogFormatter(logging.Formatter):
|
||||
def __init__(self, prefix, *args, **kwargs):
|
||||
|
@ -37,12 +41,14 @@ class LogHandler(logging.Handler):
|
|||
logger.addHandler(handler)
|
||||
return handler
|
||||
|
||||
|
||||
def log(message, level='info'):
|
||||
if not __CLI_MODE__:
|
||||
rpc_send({'jsonrpc': '2.0', 'method': 'message', 'params': {
|
||||
'level': level,
|
||||
'message': message
|
||||
}})
|
||||
else:
|
||||
cli.log(message, level)
|
||||
|
||||
|
||||
def report_host(ip, **opts):
|
||||
|
@ -76,9 +82,8 @@ def report_wrong_password(username, password, **opts):
|
|||
|
||||
|
||||
def run(metadata, module_callback, soft_check=None):
|
||||
req = json.loads(os.read(0, 10000).decode("utf-8"))
|
||||
callback = None
|
||||
if req['method'] == 'describe':
|
||||
global __CLI_MODE__
|
||||
|
||||
caps = []
|
||||
if soft_check:
|
||||
caps.append('soft_check')
|
||||
|
@ -86,6 +91,17 @@ def run(metadata, module_callback, soft_check=None):
|
|||
meta = metadata.copy()
|
||||
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})
|
||||
elif req['method'] == 'soft_check':
|
||||
if soft_check:
|
||||
|
@ -98,6 +114,9 @@ def run(metadata, module_callback, soft_check=None):
|
|||
if callback:
|
||||
args = req['params']
|
||||
ret = callback(args)
|
||||
if ret and __CLI_MODE__:
|
||||
cli.ret(ret)
|
||||
|
||||
rpc_send({'jsonrpc': '2.0', 'id': req['id'], 'result': {
|
||||
'message': 'Module completed',
|
||||
'return': ret
|
||||
|
@ -105,11 +124,17 @@ def run(metadata, module_callback, soft_check=None):
|
|||
|
||||
|
||||
def report(kind, data):
|
||||
if not __CLI_MODE__:
|
||||
rpc_send({'jsonrpc': '2.0', 'method': 'report', 'params': {
|
||||
'type': kind, 'data': data
|
||||
}})
|
||||
else:
|
||||
cli.report(kind, data)
|
||||
|
||||
|
||||
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))
|
||||
sys.stdout.flush()
|
||||
|
|
|
@ -38,18 +38,14 @@ class Msf::Modules::External::Shim
|
|||
render_template('common_check.erb', meta)
|
||||
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[:name] = mod.meta['name'].dump
|
||||
meta[:description] = mod.meta['description'].dump
|
||||
meta[:authors] = mod.meta['authors'].map(&:dump).join(",\n ")
|
||||
meta[:license] = mod.meta['license'].nil? ? 'MSF_LICENSE' : mod.meta['license']
|
||||
|
||||
options = if drop_rhost
|
||||
mod.meta['options'].reject {|n, o| n == 'rhost'}
|
||||
else
|
||||
mod.meta['options']
|
||||
end
|
||||
options = mod.meta['options'].reject {|n, _| ignore_options.include? n}
|
||||
|
||||
meta[:options] = options.map do |n, o|
|
||||
if o['values']
|
||||
|
@ -92,7 +88,7 @@ class Msf::Modules::External::Shim
|
|||
end
|
||||
|
||||
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[:command_stager_flavor] = mod.meta['payload']['command_stager_flavor'].dump
|
||||
render_template('remote_exploit_cmd_stager.erb', meta)
|
||||
|
@ -104,7 +100,7 @@ class Msf::Modules::External::Shim
|
|||
end
|
||||
|
||||
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[:references] = mod.meta['references'].map do |r|
|
||||
"[#{r['type'].upcase.dump}, #{r['ref'].dump}]"
|
||||
|
@ -114,7 +110,7 @@ class Msf::Modules::External::Shim
|
|||
end
|
||||
|
||||
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[:references] = mod.meta['references'].map do |r|
|
||||
"[#{r['type'].upcase.dump}, #{r['ref'].dump}]"
|
||||
|
|
|
@ -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_from': {'type': 'string', 'description': 'Address to send from', 'required': True, 'default': 'foo@example.com'},
|
||||
'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):
|
||||
module.log('{}:{} Starting banner check for Haraka < 2.8.9'.format(args['rhost'], args['rport']), level='debug')
|
||||
c = smtplib.SMTP()
|
||||
try:
|
||||
(code, banner) = c.connect(args['rhost'], int(args['rport']))
|
||||
except:
|
||||
return 'unknown'
|
||||
|
||||
c.quit()
|
||||
|
||||
if code == 220 and 'Haraka' in banner:
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue