Skip to content

Commit 4c0539d

Browse files
author
Brent Cook
committed
Land rapid7#8178, Add support for non-Ruby modules
2 parents a34c01e + 2de8f1b commit 4c0539d

File tree

11 files changed

+452
-28
lines changed

11 files changed

+452
-28
lines changed

lib/metasploit/framework/spec/constants.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module Metasploit::Framework::Spec::Constants
2020
# and not dynamically loaded code
2121
PERSISTENT_CHILD_CONSTANT_NAMES = %w{
2222
Error
23+
External
2324
Loader
2425
MetasploitClassCompatibilityError
2526
Namespace

lib/msf/core/module_manager/cache.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,15 @@ def load_cached_module(type, reference_name)
8181
if module_info
8282
parent_path = module_info[:parent_path]
8383

84+
# XXX borked
8485
loaders.each do |loader|
8586
if loader.loadable?(parent_path)
8687
type = module_info[:type]
8788
reference_name = module_info[:reference_name]
8889

8990
loaded = loader.load_module(parent_path, type, reference_name, :force => true)
9091

91-
break
92+
break if loaded
9293
end
9394
end
9495
end
@@ -162,11 +163,9 @@ def module_info_by_path_from_database!(allowed_paths=[""])
162163
# Skip cached modules that are not in our allowed load paths
163164
next if allowed_paths.select{|x| path.index(x) == 0}.empty?
164165

165-
typed_path = Msf::Modules::Loader::Base.typed_path(type, reference_name)
166-
# join to '' so that typed_path_prefix starts with file separator
167-
typed_path_suffix = File.join('', typed_path)
168-
escaped_typed_path = Regexp.escape(typed_path_suffix)
169-
parent_path = path.gsub(/#{escaped_typed_path}$/, '')
166+
# The load path is assumed to be the next level above the type directory
167+
type_dir = File.join('', Mdm::Module::Detail::DIRECTORY_BY_TYPE[type], '')
168+
parent_path = path.split(type_dir)[0..-2].join(type_dir) # TODO: rewrite
170169

171170
module_info_by_path[path] = {
172171
:reference_name => reference_name,

lib/msf/core/module_manager/loading.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# Project
99
#
1010
require 'msf/core/modules/loader/directory'
11+
require 'msf/core/modules/loader/executable'
1112

1213
# Deals with loading modules for the {Msf::ModuleManager}
1314
module Msf::ModuleManager::Loading
@@ -19,7 +20,8 @@ module Msf::ModuleManager::Loading
1920

2021
# Classes that can be used to load modules.
2122
LOADER_CLASSES = [
22-
Msf::Modules::Loader::Directory
23+
Msf::Modules::Loader::Directory,
24+
Msf::Modules::Loader::Executable # TODO: XXX: When this is the first loader we can load normal exploits, but not payloads
2325
]
2426

2527
def file_changed?(path)
@@ -115,8 +117,6 @@ def load_modules(path, options={})
115117
loaders.each do |loader|
116118
if loader.loadable?(path)
117119
count_by_type = loader.load_modules(path, options)
118-
119-
break
120120
end
121121
end
122122

lib/msf/core/modules/external.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# -*- coding: binary -*-
2+
# Namespace for loading external Metasploit modules
3+
4+
module Msf::Modules::External
5+
6+
end
7+
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# -*- coding: binary -*-
2+
require 'msf/core/modules/external'
3+
require 'msf/core/modules/external/message'
4+
require 'open3'
5+
6+
class Msf::Modules::External::Bridge
7+
8+
attr_reader :path, :running
9+
10+
def meta
11+
@meta ||= describe
12+
end
13+
14+
def run(datastore)
15+
unless self.running
16+
m = Msf::Modules::External::Message.new(:run)
17+
m.params = datastore.dup
18+
send(m)
19+
self.running = true
20+
end
21+
end
22+
23+
def get_status
24+
if self.running
25+
n = receive_notification
26+
if n && n['params']
27+
n['params']
28+
else
29+
close_ios
30+
self.running = false
31+
n['response'] if n
32+
end
33+
end
34+
end
35+
36+
def initialize(module_path)
37+
self.running = false
38+
self.path = module_path
39+
end
40+
41+
protected
42+
43+
attr_writer :path, :running
44+
attr_accessor :ios
45+
46+
def describe
47+
resp = send_receive(Msf::Modules::External::Message.new(:describe))
48+
close_ios
49+
resp['response']
50+
end
51+
52+
# XXX TODO non-blocking writes, check write lengths, non-blocking JSON parse loop read
53+
54+
def send_receive(message)
55+
send(message)
56+
read_json(message.id, self.ios[1])
57+
end
58+
59+
def send(message)
60+
input, output, status = ::Open3.popen3([self.path, self.path])
61+
self.ios = [input, output, status]
62+
case Rex::ThreadSafe.select(nil, [input], nil, 0.1)
63+
when nil
64+
raise "Cannot run module #{self.path}"
65+
when [[], [input], []]
66+
m = message.to_json
67+
write_message(input, m)
68+
else
69+
raise "Error running module #{self.path}"
70+
end
71+
end
72+
73+
def receive_notification
74+
input, output, status = self.ios
75+
case Rex::ThreadSafe.select([output], nil, nil, 10)
76+
when nil
77+
nil
78+
when [[output], [], []]
79+
read_json(nil, output)
80+
end
81+
end
82+
83+
def write_message(fd, json)
84+
fd.write(json)
85+
end
86+
87+
def read_json(id, fd)
88+
begin
89+
resp = fd.readpartial(10_000)
90+
JSON.parse(resp)
91+
rescue EOFError => e
92+
{}
93+
end
94+
end
95+
96+
def close_ios
97+
input, output, status = self.ios
98+
[input, output].each {|fd| fd.close rescue nil} # Yeah, yeah. I know.
99+
end
100+
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# -*- coding: binary -*-
2+
require 'msf/core/modules/external'
3+
require 'base64'
4+
require 'json'
5+
6+
class Msf::Modules::External::Message
7+
8+
attr_reader :method, :id
9+
attr_accessor :params
10+
11+
def initialize(m)
12+
self.method = m
13+
self.params = {}
14+
self.id = Base64.strict_encode64(SecureRandom.random_bytes(16))
15+
end
16+
17+
def to_json
18+
JSON.generate({jsonrpc: '2.0', id: self.id, method: self.method, params: self.params.to_h})
19+
end
20+
21+
protected
22+
23+
attr_writer :method, :id
24+
end

lib/msf/core/modules/external/shim.rb

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# -*- coding: binary -*-
2+
require 'msf/core/modules/external'
3+
require 'msf/core/modules/external/bridge'
4+
5+
class Msf::Modules::External::Shim
6+
def self.generate(module_path)
7+
mod = Msf::Modules::External::Bridge.new(module_path)
8+
return '' unless mod.meta
9+
case mod.meta['type']
10+
when 'remote_exploit.cmd_stager.wget'
11+
s = remote_exploit_cmd_stager(mod)
12+
File.open('/tmp/module', 'w') {|f| f.write(s)}
13+
s
14+
end
15+
end
16+
17+
def self.remote_exploit_cmd_stager(mod)
18+
%Q|
19+
require 'msf/core/modules/external/bridge'
20+
21+
class MetasploitModule < Msf::Exploit::Remote
22+
Rank = ExcellentRanking
23+
24+
include Msf::Exploit::CmdStager
25+
26+
def initialize(info = {})
27+
super(update_info(info,
28+
'Name' => #{mod.meta['name'].dump},
29+
'Description' => #{mod.meta['description'].dump},
30+
'Author' =>
31+
[
32+
#{mod.meta['authors'].map(&:dump).join(', ')}
33+
],
34+
'License' => MSF_LICENSE,
35+
'References' =>
36+
[
37+
#{mod.meta['references'].map do |r|
38+
"[#{r['type'].upcase.dump}, #{r['ref'].dump}]"
39+
end.join(', ')}
40+
],
41+
'DisclosureDate' => #{mod.meta['date'].dump},
42+
'Privileged' => #{mod.meta['privileged'].inspect},
43+
'Platform' => [#{mod.meta['targets'].map{|t| t['platform'].dump}.uniq.join(', ')}],
44+
'Payload' =>
45+
{
46+
'DisableNops' => true
47+
},
48+
'Targets' =>
49+
[
50+
#{mod.meta['targets'].map do |t|
51+
%Q^[#{t['platform'].dump} + ' ' + #{t['arch'].dump},
52+
{'Arch' => ARCH_#{t['arch'].upcase}, 'Platform' => #{t['platform'].dump} }]^
53+
end.join(', ')}
54+
],
55+
'DefaultTarget' => 0,
56+
'DefaultOptions' => { 'WfsDelay' => 5 }
57+
))
58+
59+
register_options([
60+
#{mod.meta['options'].map do |n, o|
61+
"Opt#{o['type'].capitalize}.new(#{n.dump},
62+
[#{o['required']}, #{o['description'].dump}, #{o['default'].inspect}])"
63+
end.join(', ')}
64+
], self.class)
65+
end
66+
67+
def execute_command(cmd, opts)
68+
mod = Msf::Modules::External::Bridge.new(#{mod.path.dump})
69+
mod.run(datastore.merge(command: cmd))
70+
wait_status(mod)
71+
true
72+
end
73+
74+
def exploit
75+
print_status("Exploiting...")
76+
execute_cmdstager({:flavor => :wget})
77+
end
78+
79+
def wait_status(mod)
80+
while mod.running
81+
m = mod.get_status
82+
if m
83+
case m['level']
84+
when 'error'
85+
print_error m['message']
86+
when 'warning'
87+
print_warning m['message']
88+
when 'good'
89+
print_good m['message']
90+
when 'info'
91+
print_status m['message']
92+
when 'debug'
93+
vprint_status m['message']
94+
else
95+
print_status m['message']
96+
end
97+
end
98+
end
99+
end
100+
end
101+
|
102+
end
103+
end

lib/msf/core/modules/loader/directory.rb

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@ class Msf::Modules::Loader::Directory < Msf::Modules::Loader::Base
1111
# @return [true] if path is a directory
1212
# @return [false] otherwise
1313
def loadable?(path)
14-
if File.directory?(path)
15-
true
16-
else
17-
false
18-
end
14+
File.directory?(path)
1915
end
2016

2117
protected
@@ -35,8 +31,7 @@ def each_module_reference_name(path, opts={})
3531
full_entry_path = ::File.join(path, entry)
3632
type = entry.singularize
3733

38-
unless ::File.directory?(full_entry_path) and
39-
module_manager.type_enabled? type
34+
unless ::File.directory?(full_entry_path) && module_manager.type_enabled?(type)
4035
next
4136
end
4237

0 commit comments

Comments
 (0)