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 :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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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}]"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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