Skip to content

Commit 6caba52

Browse files
committed
Land rapid7#9424, Add SharknAT&To external scanner
2 parents a947f89 + d085105 commit 6caba52

File tree

11 files changed

+373
-26
lines changed

11 files changed

+373
-26
lines changed

LICENSE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ Files: lib/metasm.rb lib/metasm/* data/cpuinfo/*
7575
Copyright: 2006-2010 Yoann GUILLOT
7676
License: LGPL-2.1
7777

78+
Files: lib/msf/core/modules/external/python/async_timeout/*
79+
Copyright: 2016-2017 Andrew Svetlov
80+
License: Apache 2.0
81+
7882
Files: lib/net/dns.rb lib/net/dns/*
7983
Copyright: 2006 Marco Ceresa
8084
License: Ruby

lib/msf/core/data_store.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@ def to_h
170170
datastore_hash
171171
end
172172

173+
# Hack on a hack for the external modules
174+
def to_nested_values
175+
datastore_hash = {}
176+
self.keys.each do |k|
177+
# TODO arbitrary depth
178+
if self[k].is_a? Array
179+
datastore_hash[k.to_s] = self[k].map(&:to_s)
180+
else
181+
datastore_hash[k.to_s] = self[k].to_s
182+
end
183+
end
184+
datastore_hash
185+
end
186+
173187
#
174188
# Persists the contents of the data store to a file
175189
#

lib/msf/core/module/external.rb

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,15 @@ module Msf::Module::External
33

44
def wait_status(mod)
55
begin
6-
while mod.running
7-
m = mod.get_status
8-
if m
9-
case m.method
10-
when :message
11-
log_output(m)
12-
when :report
13-
process_report(m)
14-
when :reply
15-
# we're done
16-
break
17-
end
6+
while m = mod.get_status
7+
case m.method
8+
when :message
9+
log_output(m)
10+
when :report
11+
process_report(m)
12+
when :reply
13+
# we're done
14+
break
1815
end
1916
end
2017
rescue Interrupt => e
@@ -72,6 +69,20 @@ def process_report(m)
7269
service[:name] = data['name'] if data['name']
7370

7471
report_service(service)
72+
when 'vuln'
73+
# Required
74+
vuln = {host: data['host'], name: data['name']}
75+
76+
# Optional
77+
vuln[:info] = data['info'] if data['info']
78+
vuln[:refs] = data['refs'] if data['refs']
79+
vuln[:port] = data['port'] if data['port']
80+
vuln[:proto] = data['port'] if data['port']
81+
82+
# Metasploit magic
83+
vuln[:refs] = self.references
84+
85+
report_vuln(vuln)
7586
else
7687
print_warning "Skipping unrecognized report type #{m.params['type']}"
7788
end

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ def run(datastore)
2626
end
2727

2828
def get_status
29-
if self.running
29+
if self.running || !self.messages.empty?
3030
m = receive_notification
3131
if m.nil?
3232
close_ios
33+
self.messages.close
3334
self.running = false
3435
end
3536

@@ -130,8 +131,9 @@ def recv(filter_id=nil, timeout=600)
130131
raise EOFError.new
131132
else
132133
fds = res[0]
133-
# Preferentially drain and log stderr
134-
if fds.include? err
134+
# Preferentially drain and log stderr, EOF counts as activity, but
135+
# stdout might have some buffered data left, so carry on
136+
if fds.include?(err) && !err.eof?
135137
errbuf = err.readpartial(4096)
136138
elog "Unexpected output running #{self.path}:\n#{errbuf}"
137139
end

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ def initialize(m)
2929
end
3030

3131
def to_json
32-
JSON.generate({jsonrpc: '2.0', id: self.id, method: self.method, params: self.params.to_h})
32+
params =
33+
if self.params.respond_to? :to_nested_values
34+
self.params.to_nested_values
35+
else
36+
self.params.to_h
37+
end
38+
JSON.generate({jsonrpc: '2.0', id: self.id, method: self.method, params: params})
3339
end
3440

3541
protected
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Vendored from https://github.com/aio-libs/async-timeout
2+
# Copyright: 2016-2017 Andrew Svetlov
3+
# License: Apache 2.0
4+
5+
import asyncio
6+
7+
8+
__version__ = '2.0.0'
9+
10+
11+
class timeout:
12+
"""timeout context manager.
13+
14+
Useful in cases when you want to apply timeout logic around block
15+
of code or in cases when asyncio.wait_for is not suitable. For example:
16+
17+
>>> async with timeout(0.001):
18+
... async with aiohttp.get('https://github.com') as r:
19+
... await r.text()
20+
21+
22+
timeout - value in seconds or None to disable timeout logic
23+
loop - asyncio compatible event loop
24+
"""
25+
def __init__(self, timeout, *, loop=None):
26+
self._timeout = timeout
27+
if loop is None:
28+
loop = asyncio.get_event_loop()
29+
self._loop = loop
30+
self._task = None
31+
self._cancelled = False
32+
self._cancel_handler = None
33+
self._cancel_at = None
34+
35+
def __enter__(self):
36+
return self._do_enter()
37+
38+
def __exit__(self, exc_type, exc_val, exc_tb):
39+
self._do_exit(exc_type)
40+
41+
@asyncio.coroutine
42+
def __aenter__(self):
43+
return self._do_enter()
44+
45+
@asyncio.coroutine
46+
def __aexit__(self, exc_type, exc_val, exc_tb):
47+
self._do_exit(exc_type)
48+
49+
@property
50+
def expired(self):
51+
return self._cancelled
52+
53+
@property
54+
def remaining(self):
55+
if self._cancel_at is not None:
56+
return max(self._cancel_at - self._loop.time(), 0.0)
57+
else:
58+
return None
59+
60+
def _do_enter(self):
61+
# Support Tornado 5- without timeout
62+
# Details: https://github.com/python/asyncio/issues/392
63+
if self._timeout is None:
64+
return self
65+
66+
self._task = current_task(self._loop)
67+
if self._task is None:
68+
raise RuntimeError('Timeout context manager should be used '
69+
'inside a task')
70+
71+
if self._timeout <= 0:
72+
self._loop.call_soon(self._cancel_task)
73+
return self
74+
75+
self._cancel_at = self._loop.time() + self._timeout
76+
self._cancel_handler = self._loop.call_at(
77+
self._cancel_at, self._cancel_task)
78+
return self
79+
80+
def _do_exit(self, exc_type):
81+
if exc_type is asyncio.CancelledError and self._cancelled:
82+
self._cancel_handler = None
83+
self._task = None
84+
raise asyncio.TimeoutError
85+
if self._timeout is not None and self._cancel_handler is not None:
86+
self._cancel_handler.cancel()
87+
self._cancel_handler = None
88+
self._task = None
89+
90+
def _cancel_task(self):
91+
self._task.cancel()
92+
self._cancelled = True
93+
94+
95+
def current_task(loop):
96+
task = asyncio.Task.current_task(loop=loop)
97+
if task is None:
98+
if hasattr(loop, 'current_task'):
99+
task = loop.current_task()
100+
101+
return task
Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
1-
import sys, os, json
1+
import json
2+
import os
3+
import sys
4+
25

36
def log(message, level='info'):
47
rpc_send({'jsonrpc': '2.0', 'method': 'message', 'params': {
58
'level': level,
69
'message': message
710
}})
811

9-
def report_host(ip, opts={}):
12+
13+
def report_host(ip, **opts):
1014
host = opts.copy()
1115
host.update({'host': ip})
12-
rpc_send({'jsonrpc': '2.0', 'method': 'report', 'params': {
13-
'type': 'host', 'data': host
14-
}})
16+
report('host', host)
1517

16-
def report_service(ip, opts={}):
18+
19+
def report_service(ip, **opts):
1720
service = opts.copy()
1821
service.update({'host': ip})
19-
rpc_send({'jsonrpc': '2.0', 'method': 'report', 'params': {
20-
'type': 'service', 'data': service
21-
}})
22+
report('service', service)
23+
24+
25+
def report_vuln(ip, name, **opts):
26+
vuln = opts.copy()
27+
vuln.update({'host': ip, 'name': name})
28+
report('vuln', vuln)
2229

2330

2431
def run(metadata, module_callback):
@@ -32,6 +39,13 @@ def run(metadata, module_callback):
3239
'message': 'Module completed'
3340
}})
3441

42+
43+
def report(kind, data):
44+
rpc_send({'jsonrpc': '2.0', 'method': 'report', 'params': {
45+
'type': kind, 'data': data
46+
}})
47+
48+
3549
def rpc_send(req):
3650
print(json.dumps(req))
3751
sys.stdout.flush()
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import asyncio
2+
import functools
3+
import re
4+
5+
from async_timeout import timeout
6+
from metasploit import module
7+
8+
9+
def make_scanner(payload='', pattern='', onmatch=None, connect_timeout=3, read_timeout=10):
10+
return lambda args: start_scanner(payload, pattern, args, onmatch, connect_timeout=connect_timeout, read_timeout=read_timeout)
11+
12+
13+
def start_scanner(payload, pattern, args, onmatch, **timeouts):
14+
loop = asyncio.get_event_loop()
15+
loop.run_until_complete(run_scanner(payload, pattern, args, onmatch, **timeouts))
16+
17+
18+
async def run_scanner(payload, pattern, args, onmatch, **timeouts):
19+
probes = [probe_host(host, int(args['rport']), payload, **timeouts) for host in args['rhosts']]
20+
async for (target, res) in Scan(probes):
21+
if isinstance(res, Exception):
22+
module.log('{}:{} - Error connecting: {}'.format(*target, res), level='error')
23+
elif res and re.search(pattern, res):
24+
module.log('{}:{} - Matches'.format(*target), level='good')
25+
module.log('{}:{} - Matches with: {}'.format(*target, res), level='debug')
26+
onmatch(target, res)
27+
else:
28+
module.log('{}:{} - Does not match'.format(*target), level='info')
29+
module.log('{}:{} - Does not match with: {}'.format(*target, res), level='debug')
30+
31+
32+
class Scan:
33+
def __init__(self, runs):
34+
self.queue = asyncio.queues.Queue()
35+
self.total = len(runs)
36+
self.done = 0
37+
38+
for r in runs:
39+
f = asyncio.ensure_future(r)
40+
args = r.cr_frame.f_locals
41+
target = (args['host'], args['port'])
42+
f.add_done_callback(functools.partial(self.__queue_result, target))
43+
44+
def __queue_result(self, target, f):
45+
res = None
46+
47+
try:
48+
res = f.result()
49+
except Exception as e:
50+
res = e
51+
52+
self.queue.put_nowait((target, res))
53+
54+
async def __aiter__(self):
55+
return self
56+
57+
async def __anext__(self):
58+
if self.done == self.total:
59+
raise StopAsyncIteration
60+
61+
res = await self.queue.get()
62+
self.done += 1
63+
return res
64+
65+
66+
async def probe_host(host, port, payload, connect_timeout, read_timeout):
67+
buf = bytearray()
68+
69+
try:
70+
async with timeout(connect_timeout):
71+
r, w = await asyncio.open_connection(host, port)
72+
remote = w.get_extra_info('peername')
73+
if remote[0] == host:
74+
module.log('{}:{} - Connected'.format(host, port), level='debug')
75+
else:
76+
module.log('{}({}):{} - Connected'.format(host, *remote), level='debug')
77+
w.write(payload)
78+
await w.drain()
79+
80+
async with timeout(read_timeout):
81+
while len(buf) < 4096:
82+
data = await r.read(4096)
83+
if data:
84+
module.log('{}:{} - Received {} bytes'.format(host, port, len(data)), level='debug')
85+
buf.extend(data)
86+
else:
87+
break
88+
except asyncio.TimeoutError:
89+
if buf:
90+
pass
91+
else:
92+
raise
93+
finally:
94+
try:
95+
w.close()
96+
except Exception:
97+
# Either we got something and the socket got in a bad state, or the
98+
# original error will point to the root cause
99+
pass
100+
101+
return buf

0 commit comments

Comments
 (0)