Skip to content

Commit 39f06a3

Browse files
committed
Land rapid7#8807, template for external module servers
2 parents 602406a + 62aac45 commit 39f06a3

File tree

7 files changed

+202
-49
lines changed

7 files changed

+202
-49
lines changed

lib/msf/core/module/external.rb

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,72 @@
1+
include Msf::Auxiliary::Report
2+
13
module Msf::Module::External
24
def wait_status(mod)
35
while mod.running
46
m = mod.get_status
57
if m
6-
case m['level']
7-
when 'error'
8-
print_error m['message']
9-
when 'warning'
10-
print_warning m['message']
11-
when 'good'
12-
print_good m['message']
13-
when 'info'
14-
print_status m['message']
15-
when 'debug'
16-
vprint_status m['message']
17-
else
18-
print_status m['message']
8+
case m.method
9+
when :message
10+
log_output(m)
11+
when :report
12+
process_report(m)
13+
when :reply
14+
# we're done
15+
break
1916
end
2017
end
2118
end
2219
end
20+
21+
def log_output(m)
22+
message = m.params['message']
23+
24+
case m.params['level']
25+
when 'error'
26+
print_error message
27+
when 'warning'
28+
print_warning message
29+
when 'good'
30+
print_good message
31+
when 'info'
32+
print_status message
33+
when 'debug'
34+
vprint_status message
35+
else
36+
print_status message
37+
end
38+
end
39+
40+
def process_report(m)
41+
data = m.params['data']
42+
43+
case m.params['type']
44+
when 'host'
45+
# Required
46+
host = {host: data['host']}
47+
48+
# Optional
49+
host[:state] = data['state'] if data['state'] # TODO: validate -- one of the Msf::HostState constants (unknown, alive, dead)
50+
host[:os_name] = data['os_name'] if data['os_name']
51+
host[:os_flavor] = data['os_flavor'] if data['os_flavor']
52+
host[:os_sp] = data['os_sp'] if data['os_sp']
53+
host[:os_lang] = data['os_lang'] if data['os_lang']
54+
host[:arch] = data['arch'] if data['arch'] # TODO: validate -- one of the ARCH_* constants
55+
host[:mac] = data['mac'] if data['mac']
56+
host[:scope] = data['scope'] if data['scope']
57+
host[:virtual_host] = data['virtual_host'] if data['virtual_host']
58+
59+
report_host(host)
60+
when 'service'
61+
# Required
62+
service = {host: data['host'], port: data['port'], proto: data['proto']}
63+
64+
# Optional
65+
service[:name] = data['name'] if data['name']
66+
67+
report_service(service)
68+
else
69+
print_warning "Skipping unrecognized report type #{m.params['type']}"
70+
end
71+
end
2372
end

lib/msf/core/module_manager/loading.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ def load_modules(path, options={})
116116

117117
loaders.each do |loader|
118118
if loader.loadable?(path)
119-
count_by_type.merge!(loader.load_modules(path, options)) do |key, old, new|
120-
old + new
119+
count_by_type.merge!(loader.load_modules(path, options)) do |key, prev, now|
120+
prev + now
121121
end
122122
end
123123
end

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

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require 'msf/core/modules/external'
33
require 'msf/core/modules/external/message'
44
require 'open3'
5+
require 'json'
56

67
class Msf::Modules::External::Bridge
78

@@ -26,45 +27,47 @@ def run(datastore)
2627

2728
def get_status
2829
if self.running
29-
n = receive_notification
30-
if n && n['params']
31-
n['params']
32-
else
30+
m = receive_notification
31+
if m.nil?
3332
close_ios
3433
self.running = false
35-
n['response'] if n
3634
end
35+
36+
return m
3737
end
3838
end
3939

4040
def initialize(module_path)
4141
self.env = {}
4242
self.running = false
4343
self.path = module_path
44+
self.cmd = [self.path, self.path]
45+
self.messages = Queue.new
4446
end
4547

4648
protected
4749

4850
attr_writer :path, :running
49-
attr_accessor :env, :ios
51+
attr_accessor :cmd, :env, :ios, :messages
5052

5153
def describe
5254
resp = send_receive(Msf::Modules::External::Message.new(:describe))
5355
close_ios
54-
resp['response']
56+
resp.params
5557
end
5658

57-
# XXX TODO non-blocking writes, check write lengths, non-blocking JSON parse loop read
59+
# XXX TODO non-blocking writes, check write lengths
5860

5961
def send_receive(message)
6062
send(message)
61-
read_json(message.id, self.ios[1])
63+
recv(message.id)
6264
end
6365

6466
def send(message)
65-
input, output, status = ::Open3.popen3(env, [self.path, self.path])
67+
input, output, status = ::Open3.popen3(self.env, self.cmd)
6668
self.ios = [input, output, status]
67-
case Rex::ThreadSafe.select(nil, [input], nil, 0.1)
69+
# We would call Rex::Threadsafe directly, but that would require rex for standalone use
70+
case select(nil, [input], nil, 0.1)
6871
when nil
6972
raise "Cannot run module #{self.path}"
7073
when [[], [input], []]
@@ -76,23 +79,57 @@ def send(message)
7679
end
7780

7881
def receive_notification
79-
input, output, status = self.ios
80-
case Rex::ThreadSafe.select([output], nil, nil, 10)
81-
when nil
82-
nil
83-
when [[output], [], []]
84-
read_json(nil, output)
82+
if self.messages.empty?
83+
recv
84+
else
85+
self.messages.pop
8586
end
8687
end
8788

8889
def write_message(fd, json)
8990
fd.write(json)
9091
end
9192

92-
def read_json(id, fd)
93+
def recv(filter_id=nil, timeout=600)
94+
_, fd, _ = self.ios
95+
96+
# Multiple messages can come over the wire all at once, and since yajl
97+
# doesn't play nice with windows, we have to emulate a state machine to
98+
# read just enough off the wire to get one request at a time. Since
99+
# Windows cannot do a nonblocking read on a pipe, we are forced to do a
100+
# whole lot of `select` syscalls :(
101+
buf = ""
93102
begin
94-
resp = fd.readpartial(10_000)
95-
JSON.parse(resp)
103+
loop do
104+
# We would call Rex::Threadsafe directly, but that would require Rex for standalone use
105+
case select([fd], nil, nil, timeout)
106+
when nil
107+
# This is what we would have gotten without Rex and what `readpartial` can also raise
108+
raise EOFError.new
109+
when [[fd], [], []]
110+
c = fd.readpartial(1)
111+
buf << c
112+
113+
# This is so we don't end up calling JSON.parse on every char and
114+
# having to catch an exception. Windows can't do nonblock on pipes,
115+
# so we still have to do the select each time.
116+
break if c == '}'
117+
end
118+
end
119+
120+
m = Msf::Modules::External::Message.from_module(JSON.parse(buf))
121+
if filter_id && m.id != filter_id
122+
# We are filtering for a response to a particular message, but we got
123+
# something else, store the message and try again
124+
self.messages.push m
125+
read_json(filter_id, timeout)
126+
else
127+
# Either we weren't filtering, or we got what we were looking for
128+
m
129+
end
130+
rescue JSON::ParserError
131+
# Probably an incomplete response, but no way to really tell
132+
retry
96133
rescue EOFError => e
97134
{}
98135
end

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,25 @@
22
require 'msf/core/modules/external'
33
require 'base64'
44
require 'json'
5+
require 'securerandom'
56

67
class Msf::Modules::External::Message
78

8-
attr_reader :method, :id
9-
attr_accessor :params
9+
attr_reader :method
10+
attr_accessor :params, :id
11+
12+
def self.from_module(j)
13+
if j['method']
14+
m = self.new(j['method'].to_sym)
15+
m.params = j['params']
16+
m
17+
elsif j['response']
18+
m = self.new(:reply)
19+
m.params = j['response']
20+
m.id = j['id']
21+
m
22+
end
23+
end
1024

1125
def initialize(m)
1226
self.method = m
@@ -20,5 +34,5 @@ def to_json
2034

2135
protected
2236

23-
attr_writer :method, :id
37+
attr_writer :method
2438
end
Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
import sys, os, json
22

33
def log(message, level='info'):
4-
print(json.dumps({'jsonrpc': '2.0', 'method': 'message', 'params': {
4+
rpc_send({'jsonrpc': '2.0', 'method': 'message', 'params': {
55
'level': level,
66
'message': message
7-
}}))
8-
sys.stdout.flush()
7+
}})
8+
9+
def report_host(ip, opts={}):
10+
host = opts.copy()
11+
host.update({'host': ip})
12+
rpc_send({'jsonrpc': '2.0', 'method': 'report', 'params': {
13+
'type': 'host', 'data': host
14+
}})
15+
16+
def report_service(ip, opts={}):
17+
service = opts.copy()
18+
service.update({'host': ip})
19+
rpc_send({'jsonrpc': '2.0', 'method': 'report', 'params': {
20+
'type': 'service', 'data': service
21+
}})
22+
923

1024
def run(metadata, exploit):
1125
req = json.loads(os.read(0, 10000))
1226
if req['method'] == 'describe':
13-
print(json.dumps({'jsonrpc': '2.0', 'id': req['id'], 'response': metadata}))
27+
rpc_send({'jsonrpc': '2.0', 'id': req['id'], 'response': metadata})
1428
elif req['method'] == 'run':
1529
args = req['params']
1630
exploit(args)
17-
print(json.dumps({'jsonrpc': '2.0', 'id': req['id'], 'response': {
31+
rpc_send({'jsonrpc': '2.0', 'id': req['id'], 'response': {
1832
'message': 'Exploit completed'
19-
}}))
20-
sys.stdout.flush()
33+
}})
34+
35+
def rpc_send(req):
36+
print(json.dumps(req))
37+
sys.stdout.flush()

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ def self.generate(module_path)
99
case mod.meta['type']
1010
when 'remote_exploit_cmd_stager'
1111
remote_exploit_cmd_stager(mod)
12+
when 'capture_server'
13+
capture_server(mod)
14+
else
15+
# TODO have a nice load error show up in the logs
16+
''
1217
end
1318
end
1419

@@ -26,10 +31,6 @@ def self.mod_meta_common(mod, meta = {})
2631
meta[:name] = mod.meta['name'].dump
2732
meta[:description] = mod.meta['description'].dump
2833
meta[:authors] = mod.meta['authors'].map(&:dump).join(",\n ")
29-
meta[:date] = mod.meta['date'].dump
30-
meta[:references] = mod.meta['references'].map do |r|
31-
"[#{r['type'].upcase.dump}, #{r['ref'].dump}]"
32-
end.join(",\n ")
3334

3435
meta[:options] = mod.meta['options'].map do |n, o|
3536
"Opt#{o['type'].capitalize}.new(#{n.dump},
@@ -39,11 +40,15 @@ def self.mod_meta_common(mod, meta = {})
3940
end
4041

4142
def self.mod_meta_exploit(mod, meta = {})
43+
meta[:date] = mod.meta['date'].dump
4244
meta[:wfsdelay] = mod.meta['wfsdelay'] || 5
4345
meta[:privileged] = mod.meta['privileged'].inspect
4446
meta[:platform] = mod.meta['targets'].map do |t|
4547
t['platform'].dump
4648
end.uniq.join(",\n ")
49+
meta[:references] = mod.meta['references'].map do |r|
50+
"[#{r['type'].upcase.dump}, #{r['ref'].dump}]"
51+
end.join(",\n ")
4752
meta[:targets] = mod.meta['targets'].map do |t|
4853
"[#{t['platform'].dump} + ' ' + #{t['arch'].dump}, {'Arch' => ARCH_#{t['arch'].upcase}, 'Platform' => #{t['platform'].dump} }]"
4954
end.join(",\n ")
@@ -56,4 +61,9 @@ def self.remote_exploit_cmd_stager(mod)
5661
meta[:command_stager_flavor] = mod.meta['payload']['command_stager_flavor'].dump
5762
render_template('remote_exploit_cmd_stager.erb', meta)
5863
end
64+
65+
def self.capture_server(mod)
66+
meta = mod_meta_common(mod)
67+
render_template('capture_server.erb', meta)
68+
end
5969
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require 'msf/core/modules/external/bridge'
2+
require 'msf/core/module/external'
3+
4+
class MetasploitModule < Msf::Auxiliary
5+
include Msf::Module::External
6+
7+
def initialize
8+
super({
9+
<%= common_metadata meta %>
10+
'Actions' => [ ['Capture'] ],
11+
'PassiveActions' => ['Capture'],
12+
'DefaultAction' => 'Capture'
13+
})
14+
15+
register_options([
16+
<%= meta[:options] %>
17+
])
18+
end
19+
20+
def run
21+
print_status("Starting server...")
22+
mod = Msf::Modules::External::Bridge.open(<%= meta[:path] %>)
23+
mod.run(datastore)
24+
wait_status(mod)
25+
end
26+
end

0 commit comments

Comments
 (0)