AutoRecon/autorecon.py

1397 lines
52 KiB
Python

import asyncio, os, sys, re, signal, pkgutil, inspect, importlib, unidecode, argparse, string, ipaddress, socket, toml, time, math
from datetime import datetime
import colorama
from typing import final
from colorama import Fore, Style
import traceback
colorama.init()
class Pattern:
def __init__(self, pattern, description=None):
self.pattern = pattern
self.description = description
class Target:
def __init__(self, address, autorecon):
self.address = address
self.autorecon = autorecon
self.basedir = ''
self.reportdir = ''
self.scandir = ''
self.lock = asyncio.Lock()
self.pending_services = []
self.services = []
self.scans = []
self.running_tasks = {}
async def add_service(self, protocol, port, name, secure=False):
async with self.lock:
self.pending_services.append(Service(protocol, port, name, secure))
def extract_service(self, line, regex=None):
return self.autorecon.extract_service(line, regex)
async def extract_services(self, stream, regex=None):
return await self.autorecon.extract_services(stream, regex)
async def execute(self, cmd, blocking=True, outfile=None, errfile=None):
target = self
# Create variables for command references.
address = target.address
scandir = target.scandir
nmap_extra = self.autorecon.args.nmap
if self.autorecon.args.nmap_append:
nmap_extra += ' ' + self.autorecon.args.nmap_append
plugin = inspect.currentframe().f_back.f_locals['self']
cmd = e(cmd)
tag = plugin.slug
if target.autorecon.config['verbose'] >= 1:
info('Port scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd)
if outfile is not None:
outfile = os.path.abspath(os.path.join(target.scandir, e(outfile)))
if errfile is not None:
errfile = os.path.abspath(os.path.join(target.scandir, e(errfile)))
async with target.lock:
with open(os.path.join(target.scandir, '_commands.log'), 'a') as file:
file.writelines(cmd + '\n\n')
process, stdout, stderr = await self.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile)
target.running_tasks[tag]['processes'].append({'process':process, 'stderr': stderr, 'cmd': cmd})
if blocking:
while (not (stdout.ended and stderr.ended)):
await asyncio.sleep(0.1)
await process.wait()
return process, stdout, stderr
class Service:
def __init__(self, protocol, port, name, secure=False):
self.target = None
self.protocol = protocol.lower()
self.port = int(port)
self.name = name
self.secure = secure
@final
def tag(self):
return self.protocol + '/' + str(self.port) + '/' + self.name
@final
def full_tag(self):
return self.protocol + '/' + str(self.port) + '/' + self.name + '/' + ('secure' if self.secure else 'insecure')
@final
async def execute(self, cmd, blocking=True, outfile=None, errfile=None):
target = self.target
# Create variables for command references.
address = target.address
scandir = target.scandir
protocol = self.protocol
port = self.port
name = self.name
# Special cases for HTTP.
http_scheme = 'https' if 'https' in self.name or self.secure is True else 'http'
nmap_extra = self.target.autorecon.args.nmap
if self.target.autorecon.args.nmap_append:
nmap_extra += ' ' + self.target.autorecon.args.nmap_append
if protocol == 'udp':
nmap_extra += ' -sU'
plugin = inspect.currentframe().f_back.f_locals['self']
cmd = e(cmd)
tag = self.tag() + '/' + plugin.slug
if target.autorecon.config['verbose'] >= 1:
info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd)
if outfile is not None:
outfile = os.path.abspath(os.path.join(target.scandir, e(outfile)))
if errfile is not None:
errfile = os.path.abspath(os.path.join(target.scandir, e(errfile)))
async with target.lock:
with open(os.path.join(target.scandir, '_commands.log'), 'a') as file:
file.writelines(e('{cmd}\n\n'))
process, stdout, stderr = await target.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile)
target.running_tasks[tag]['processes'].append({'process':process, 'stderr': stderr, 'cmd': cmd})
if blocking:
while (not (stdout.ended and stderr.ended)):
await asyncio.sleep(0.1)
await process.wait()
return process, stdout, stderr
class CommandStreamReader(object):
def __init__(self, stream, target, tag,patterns=None, outfile=None):
self.stream = stream
self.target = target
self.tag = tag
self.lines = []
self.patterns = patterns or []
self.outfile = outfile
self.ended = False
async def _read(self):
while True:
if self.stream.at_eof():
break
line = (await self.stream.readline()).decode('utf8').rstrip()
if self.target.autorecon.config['verbose'] >= 2:
if line != '':
info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}]{crst} ' + line.replace('{', '{{').replace('}', '}}'))
for p in self.patterns:
matches = p.pattern.findall(line)
for match in matches:
async with self.target.lock:
with open(os.path.join(self.target.scandir, '_patterns.log'), 'a') as file:
if p.description:
if self.target.autorecon.config['verbose'] >= 1:
info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}] {crst}{bmagenta}' + p.description.replace('{match}', '{bblue}' + match + '{crst}{bmagenta}') + '{rst}')
file.writelines(p.description.replace('{match}', match) + '\n\n')
else:
if self.target.autorecon.config['verbose'] >= 1:
info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}] {crst}{bmagenta}Matched Pattern: {bblue}' + match + '{rst}')
file.writelines('Matched Pattern: ' + match + '\n\n')
if self.outfile is not None:
with open(self.outfile, 'a') as writer:
writer.write(line + '\n')
self.lines.append(line)
self.ended = True
async def readline(self):
while True:
try:
return self.lines.pop(0)
except IndexError:
if self.ended:
return None
else:
await asyncio.sleep(0.1)
class Plugin(object):
def __init__(self):
self.name = None
self.slug = None
self.description = None
self.tags = ['default']
self.priority = 1
self.patterns = []
self.manual_commands = {}
self.autorecon = None
self.disabled = False
@final
def add_option(self, name, default=None, help=None):
self.autorecon.add_argument(self, name, metavar='VALUE', default=default, help=help)
@final
def add_constant_option(self, name, const, default=None, help=None):
self.autorecon.add_argument(self, name, action='store_const', const=const, default=default, help=help)
@final
def add_true_option(self, name, help=None):
self.autorecon.add_argument(self, name, action='store_true', help=help)
@final
def add_false_option(self, name, help=None):
self.autorecon.add_argument(self, name, action='store_false', help=help)
@final
def add_list_option(self, name, default=None, help=None):
self.autorecon.add_argument(self, name, action='append', metavar='VALUE', default=default, help=help)
@final
def add_choice_option(self, name, choices, default=None, help=None):
if not isinstance(choices, list):
fail('The choices argument for ' + self.name + '\'s ' + name + ' choice option should be a list.')
self.autorecon.add_argument(self, name, choices=choices, default=default, help=help)
@final
def get_option(self, name):
# TODO: make sure name is simple.
name = self.slug.replace('-', '_') + '.' + slugify(name).replace('-', '_')
if name in vars(self.autorecon.args):
return vars(self.autorecon.args)[name]
else:
return None
@final
def get_global_option(self, name):
name = 'global.' + slugify(name).replace('-', '_')
if name in vars(self.autorecon.args):
return vars(self.autorecon.args)[name]
else:
return None
@final
def get_global(self, name):
return self.get_global_option(name)
@final
def add_manual_commands(self, description, commands):
if not isinstance(commands, list):
commands = [commands]
self.manual_commands[description] = commands
@final
def add_manual_command(self, description, command):
self.add_manual_commands(description, command)
@final
def add_pattern(self, pattern, description=None):
try:
compiled = re.compile(pattern)
if description:
self.patterns.append(Pattern(compiled, description=description))
else:
self.patterns.append(Pattern(compiled))
except re.error:
fail('Error: The pattern "' + pattern + '" in the plugin "' + self.name + '" is invalid regex.')
class PortScan(Plugin):
def __init__(self):
super().__init__()
async def run(self, target):
raise NotImplementedError
class ServiceScan(Plugin):
def __init__(self):
super().__init__()
self.ports = {'tcp':[], 'udp':[]}
self.ignore_ports = {'tcp':[], 'udp':[]}
self.service_names = []
self.ignore_service_names = []
self.run_once_boolean = False
self.require_ssl_boolean = False
@final
def match_port(self, protocol, port, negative_match=False):
protocol = protocol.lower()
if protocol not in ['tcp', 'udp']:
print('Invalid protocol.')
sys.exit(1)
else:
if not isinstance(port, list):
port = [port]
port = list(map(int, port))
if negative_match:
self.ignore_ports[protocol] = list(set(self.ignore_ports[protocol] + port))
else:
self.ports[protocol] = list(set(self.ports[protocol] + port))
@final
def match_service_name(self, name, negative_match=False):
if not isinstance(name, list):
name = [name]
valid_regex = True
for r in name:
try:
re.compile(r)
except re.error:
print('Invalid regex: ' + r)
valid_regex = False
if valid_regex:
if negative_match:
self.ignore_service_names = list(set(self.ignore_service_names + name))
else:
self.service_names = list(set(self.service_names + name))
else:
sys.exit(1)
@final
def require_ssl(self, boolean):
self.require_ssl_boolean = boolean
@final
def run_once(self, boolean):
self.run_once_boolean = boolean
class AutoRecon(object):
def __init__(self):
self.pending_targets = []
self.scanning_targets = []
self.plugins = {}
self.__slug_regex = re.compile('^[a-z0-9\-]+$')
self.plugin_types = {'port':[], 'service':[]}
self.port_scan_semaphore = None
self.service_scan_semaphore = None
self.argparse = None
self.argparse_group = None
self.args = None
self.tags = []
self.excluded_tags = []
self.patterns = []
self.configurable_keys = ['max_scans', 'max_port_scans', 'single_target', 'outdir', 'only_scans_dir', 'heartbeat', 'timeout', 'target_timeout', 'accessible', 'verbose']
self.config = {
'protected_classes': ['autorecon', 'target', 'service', 'commandstreamreader', 'plugin', 'portscan', 'servicescan', 'global', 'pattern'],
'global_file': os.path.dirname(os.path.realpath(__file__)) + '/global.toml',
'max_scans': 50,
'max_port_scans': None,
'single_target': False,
'outdir': 'results',
'only_scans_dir': False,
'heartbeat': 60,
'timeout': None,
'target_timeout': None,
'accessible': False,
'verbose': 0
}
self.lock = asyncio.Lock()
self.load_slug = None
self.load_module = None
def add_argument(self, plugin, name, **kwargs):
# TODO: make sure name is simple.
name = '--' + plugin.slug + '.' + slugify(name)
if self.argparse_group is None:
self.argparse_group = self.argparse.add_argument_group('plugin arguments', description='These are optional arguments for certain plugins.')
self.argparse_group.add_argument(name, **kwargs)
def extract_service(self, line, regex):
if regex is None:
regex = '^(?P<port>\d+)\/(?P<protocol>(tcp|udp))(.*)open(\s*)(?P<service>[\w\-\/]+)(\s*)(.*)$'
match = re.search(regex, line)
if match:
protocol = match.group('protocol').lower()
port = int(match.group('port'))
service = match.group('service')
secure = True if 'ssl' in service or 'tls' in service else False
if service.startswith('ssl/') or service.startswith('tls/'):
service = service[4:]
from autorecon import Service
return Service(protocol, port, service, secure)
else:
return None
async def extract_services(self, stream, regex):
if not isinstance(stream, CommandStreamReader):
print('Error: extract_services must be passed an instance of a CommandStreamReader.')
sys.exit(1)
services = []
while True:
line = await stream.readline()
if line is not None:
service = self.extract_service(line, regex)
if service:
services.append(service)
else:
break
return services
def register(self, plugin):
if plugin.disabled:
return
for _, loaded_plugin in self.plugins.items():
if plugin.name == loaded_plugin.name:
fail('Error: Duplicate plugin name "' + plugin.name + '" detected.', file=sys.stderr)
if plugin.slug is None:
plugin.slug = slugify(plugin.name)
elif not self.__slug_regex.match(plugin.slug):
fail('Error: provided slug "' + plugin.slug + '" is not valid (must only contain lowercase letters, numbers, and hyphens).', file=sys.stderr)
if plugin.slug in self.config['protected_classes']:
fail('Error: plugin slug "' + plugin.slug + '" is a protected string. Please change.')
if plugin.slug not in self.plugins:
for _, loaded_plugin in self.plugins.items():
if plugin is loaded_plugin:
fail('Error: plugin "' + plugin.name + '" already loaded as "' + loaded_plugin.name + '" (' + str(loaded_plugin) + ')', file=sys.stderr)
if plugin.description is None:
plugin.description = ''
configure_function_found = False
run_coroutine_found = False
manual_function_found = False
for member_name, member_value in inspect.getmembers(plugin, predicate=inspect.ismethod):
if member_name == 'configure':
configure_function_found = True
elif member_name == 'run' and inspect.iscoroutinefunction(member_value):
run_coroutine_found = True
elif member_name == 'manual':
manual_function_found = True
if not run_coroutine_found and not manual_function_found:
fail('Error: the plugin "' + plugin.name + '" needs either a "manual" function, a "run" coroutine, or both.', file=sys.stderr)
from autorecon import PortScan, ServiceScan
if issubclass(plugin.__class__, PortScan):
self.plugin_types["port"].append(plugin)
elif issubclass(plugin.__class__, ServiceScan):
self.plugin_types["service"].append(plugin)
else:
fail('Plugin "' + plugin.name + '" is neither a PortScan nor a ServiceScan.', file=sys.stderr)
plugin.tags = [tag.lower() for tag in plugin.tags]
plugin.autorecon = self
if configure_function_found:
plugin.configure()
self.plugins[plugin.slug] = plugin
else:
fail('Error: plugin slug "' + plugin.slug + '" is already assigned.', file=sys.stderr)
async def execute(self, cmd, target, tag, patterns=None, outfile=None, errfile=None):
if patterns:
combined_patterns = self.patterns + patterns
else:
combined_patterns = self.patterns
process = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
executable='/bin/bash')
cout = CommandStreamReader(process.stdout, target, tag, patterns=combined_patterns, outfile=outfile)
cerr = CommandStreamReader(process.stderr, target, tag, patterns=combined_patterns, outfile=errfile)
asyncio.create_task(cout._read())
asyncio.create_task(cerr._read())
return process, cout, cerr
# Since this file is run as the main method and also imported by plugins,
# we need to make sure that only one instance of the AutoRecon is
# created. This cannot be done with Singletons unfortunately, which is
# why this hack is here.
if 'autorecon' not in sys.modules: # If this file is not yet imported, create the AutoRecon object
autorecon = AutoRecon()
else: # Otherwise, assign it from the __main__ module.
autorecon = sys.modules['__main__'].autorecon
def e(*args, frame_index=1, **kvargs):
frame = sys._getframe(frame_index)
vals = {}
vals.update(frame.f_globals)
vals.update(frame.f_locals)
vals.update(kvargs)
return string.Formatter().vformat(' '.join(args), args, vals)
def cprint(*args, color=Fore.RESET, char='*', sep=' ', end='\n', frame_index=1, file=sys.stdout, printmsg=True, **kvargs):
frame = sys._getframe(frame_index)
vals = {
'bgreen': Fore.GREEN + Style.BRIGHT,
'bred': Fore.RED + Style.BRIGHT,
'bblue': Fore.BLUE + Style.BRIGHT,
'byellow': Fore.YELLOW + Style.BRIGHT,
'bmagenta': Fore.MAGENTA + Style.BRIGHT,
'green': Fore.GREEN,
'red': Fore.RED,
'blue': Fore.BLUE,
'yellow': Fore.YELLOW,
'magenta': Fore.MAGENTA,
'bright': Style.BRIGHT,
'srst': Style.NORMAL,
'crst': Fore.RESET,
'rst': Style.NORMAL + Fore.RESET
}
if autorecon.config['accessible']:
vals = {'bgreen':'', 'bred':'', 'bblue':'', 'byellow':'', 'bmagenta':'', 'green':'', 'red':'', 'blue':'', 'yellow':'', 'magenta':'', 'bright':'', 'srst':'', 'crst':'', 'rst':''}
vals.update(frame.f_globals)
vals.update(frame.f_locals)
vals.update(kvargs)
unfmt = ''
if char is not None and not autorecon.config['accessible']:
unfmt += color + '[' + Style.BRIGHT + char + Style.NORMAL + ']' + Fore.RESET + sep
unfmt += sep.join(args)
fmted = unfmt
for attempt in range(10):
try:
fmted = string.Formatter().vformat(unfmt, args, vals)
break
except KeyError as err:
key = err.args[0]
unfmt = unfmt.replace('{' + key + '}', '{{' + key + '}}')
if printmsg:
print(fmted, sep=sep, end=end, file=file)
else:
return fmted
def debug(*args, color=Fore.GREEN, sep=' ', end='\n', file=sys.stdout, **kvargs):
if verbose >= 2:
if autorecon.config['accessible']:
args = ('Debug:',) + args
cprint(*args, color=color, char='-', sep=sep, end=end, file=file, frame_index=2, **kvargs)
def info(*args, sep=' ', end='\n', file=sys.stdout, **kvargs):
cprint(*args, color=Fore.BLUE, char='*', sep=sep, end=end, file=file, frame_index=2, **kvargs)
def warn(*args, sep=' ', end='\n', file=sys.stderr,**kvargs):
if autorecon.config['accessible']:
args = ('Warning:',) + args
cprint(*args, color=Fore.YELLOW, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs)
def error(*args, sep=' ', end='\n', file=sys.stderr, **kvargs):
if autorecon.config['accessible']:
args = ('Error:',) + args
cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs)
def fail(*args, sep=' ', end='\n', file=sys.stderr, **kvargs):
if autorecon.config['accessible']:
args = ('Failure:',) + args
cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs)
exit(-1)
def calculate_elapsed_time(start_time):
elapsed_seconds = round(time.time() - start_time)
m, s = divmod(elapsed_seconds, 60)
h, m = divmod(m, 60)
elapsed_time = []
if h == 1:
elapsed_time.append(str(h) + ' hour')
elif h > 1:
elapsed_time.append(str(h) + ' hours')
if m == 1:
elapsed_time.append(str(m) + ' minute')
elif m > 1:
elapsed_time.append(str(m) + ' minutes')
if s == 1:
elapsed_time.append(str(s) + ' second')
elif s > 1:
elapsed_time.append(str(s) + ' seconds')
else:
elapsed_time.append('less than a second')
return ', '.join(elapsed_time)
def slugify(name):
return re.sub(r'[\W_]+', '-', unidecode.unidecode(name).lower()).strip('-')
def cancel_all_tasks(signal, frame):
for task in asyncio.all_tasks():
task.cancel()
for target in autorecon.scanning_targets:
for process_list in target.running_tasks.values():
for process_dict in process_list['processes']:
try:
process_dict['process'].kill()
except ProcessLookupError: # Will get raised if the process finishes before we get to killing it.
pass
async def start_heartbeat(target, period=60):
while True:
await asyncio.sleep(period)
async with target.lock:
count = len(target.running_tasks)
tasks_list = ''
if target.autorecon.config['verbose'] >= 1:
tasks_list = ': {bblue}' + ', '.join(target.running_tasks.keys()) + '{rst}'
current_time = datetime.now().strftime('%H:%M:%S')
if count > 1:
info('{bgreen}' + current_time + '{rst} - There are {byellow}' + str(count) + '{rst} scans still running against {byellow}' + target.address + '{rst}' + tasks_list)
elif count == 1:
info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list)
async def port_scan(plugin, target):
async with target.autorecon.port_scan_semaphore:
info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}')
async with target.lock:
target.running_tasks[plugin.slug] = {'plugin': plugin, 'processes':[]}
start_time = time.time()
try:
result = await plugin.run(target)
except Exception as ex:
exc_type, exc_value, exc_tb = sys.exc_info()
error_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)[-2:])
raise Exception(cprint('Error: Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False))
for process_dict in target.running_tasks[plugin.slug]['processes']:
if process_dict['process'].returncode is None:
warn('A process was left running after port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.')
await process_dict['process'].wait()
if process_dict['process'].returncode != 0:
errors = []
while True:
line = await process_dict['stderr'].readline()
if line is not None:
errors.append(line + '\n')
else:
break
error('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} ran a command against {byellow}' + target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + target.scandir + '/_errors.log for more details.')
async with target.lock:
with open(os.path.join(target.scandir, '_errors.log'), 'a') as file:
file.writelines('[*] Port scan ' + plugin.name + ' (' + plugin.slug + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n')
file.writelines('[-] Command: ' + process_dict['cmd'] + '\n')
if errors:
file.writelines(['[-] Error Output:\n'] + errors + ['\n'])
else:
file.writelines('\n')
elapsed_time = calculate_elapsed_time(start_time)
async with target.lock:
target.running_tasks.pop(plugin.slug, None)
info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished in ' + elapsed_time)
return {'type':'port', 'plugin':plugin, 'result':result}
async def service_scan(plugin, service):
from autorecon import PortScan
semaphore = service.target.autorecon.service_scan_semaphore
# If service scan semaphore is locked, see if we can use port scan semaphore.
while True:
if semaphore.locked():
if semaphore != service.target.autorecon.port_scan_semaphore: # This will be true unless user sets max_scans == max_port_scans
port_scan_task_count = 0
for targ in service.target.autorecon.scanning_targets:
for process_list in targ.running_tasks.values():
if issubclass(process_list['plugin'].__class__, PortScan):
port_scan_task_count += 1
if not service.target.autorecon.pending_targets and (service.target.autorecon.config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore.
if service.target.autorecon.port_scan_semaphore.locked():
await asyncio.sleep(1)
continue
semaphore = service.target.autorecon.port_scan_semaphore
break
else: # Do some math to see if we can use the port scan semaphore.
if (service.target.autorecon.config['max_port_scans'] - (port_scan_task_count + (len(service.target.autorecon.pending_targets) * service.target.autorecon.config['port_scan_plugin_count']))) >= 1:
if service.target.autorecon.port_scan_semaphore.locked():
await asyncio.sleep(1)
continue
semaphore = service.target.autorecon.port_scan_semaphore
break
else:
await asyncio.sleep(1)
else:
break
else:
break
async with semaphore:
tag = service.tag() + '/' + plugin.slug
info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}')
async with service.target.lock:
service.target.running_tasks[tag] = {'plugin': plugin, 'processes':[]}
start_time = time.time()
try:
result = await plugin.run(service)
except Exception as ex:
exc_type, exc_value, exc_tb = sys.exc_info()
error_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)[-2:])
raise Exception(cprint('Error: Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False))
for process_dict in service.target.running_tasks[tag]['processes']:
if process_dict['process'].returncode is None:
warn('A process was left running after service scan {bblue}' + plugin.name + ' (' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.')
await process_dict['process'].wait()
if process_dict['process'].returncode != 0:
errors = []
while True:
line = await process_dict['stderr'].readline()
if line is not None:
errors.append(line + '\n')
else:
break
error('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} ran a command against {byellow}' + service.target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + service.target.scandir + '/_errors.log for more details.')
async with service.target.lock:
with open(os.path.join(service.target.scandir, '_errors.log'), 'a') as file:
file.writelines('[*] Service scan ' + plugin.name + ' (' + tag + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n')
file.writelines('[-] Command: ' + process_dict['cmd'] + '\n')
if errors:
file.writelines(['[-] Error Output:\n'] + errors + ['\n'])
else:
file.writelines('\n')
elapsed_time = calculate_elapsed_time(start_time)
async with service.target.lock:
service.target.running_tasks.pop(tag, None)
info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished in ' + elapsed_time)
return {'type':'service', 'plugin':plugin, 'result':result}
async def scan_target(target):
if target.autorecon.config['single_target']:
basedir = os.path.abspath(target.autorecon.config['outdir'])
else:
basedir = os.path.abspath(os.path.join(target.autorecon.config['outdir'], target.address))
target.basedir = basedir
os.makedirs(basedir, exist_ok=True)
if not target.autorecon.config['only_scans_dir']:
exploitdir = os.path.abspath(os.path.join(basedir, 'exploit'))
os.makedirs(exploitdir, exist_ok=True)
lootdir = os.path.abspath(os.path.join(basedir, 'loot'))
os.makedirs(lootdir, exist_ok=True)
reportdir = os.path.abspath(os.path.join(basedir, 'report'))
target.reportdir = reportdir
os.makedirs(reportdir, exist_ok=True)
open(os.path.abspath(os.path.join(reportdir, 'local.txt')), 'a').close()
open(os.path.abspath(os.path.join(reportdir, 'proof.txt')), 'a').close()
screenshotdir = os.path.abspath(os.path.join(reportdir, 'screenshots'))
os.makedirs(screenshotdir, exist_ok=True)
scandir = os.path.abspath(os.path.join(basedir, 'scans'))
target.scandir = scandir
os.makedirs(scandir, exist_ok=True)
os.makedirs(os.path.abspath(os.path.join(scandir, 'xml')), exist_ok=True)
pending = []
heartbeat = asyncio.create_task(start_heartbeat(target, period=target.autorecon.config['heartbeat']))
for plugin in target.autorecon.plugin_types['port']:
plugin_tag_set = set(plugin.tags)
matching_tags = False
for tag_group in target.autorecon.tags:
if set(tag_group).issubset(plugin_tag_set):
matching_tags = True
break
excluded_tags = False
for tag_group in target.autorecon.excluded_tags:
if set(tag_group).issubset(plugin_tag_set):
excluded_tags = True
break
if matching_tags and not excluded_tags:
pending.append(asyncio.create_task(port_scan(plugin, target)))
async with autorecon.lock:
autorecon.scanning_targets.append(target)
start_time = time.time()
info('Scanning target {byellow}' + target.address + '{rst}')
timed_out = False
while pending:
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1)
# Check if global timeout has occurred.
if autorecon.config['target_timeout'] is not None:
elapsed_seconds = round(time.time() - start_time)
m, s = divmod(elapsed_seconds, 60)
if m >= autorecon.config['target_timeout']:
timed_out = True
break
# Extract Services
services = []
async with target.lock:
while target.pending_services:
services.append(target.pending_services.pop(0))
for task in done:
try:
if task.exception():
print(task.exception())
continue
except asyncio.InvalidStateError:
pass
if task.result()['type'] == 'port':
for service in (task.result()['result'] or []):
services.append(service)
for service in services:
if service.full_tag() not in target.services:
target.services.append(service.full_tag())
else:
continue
info('Found {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}')
service.target = target
# Create variables for command references.
address = target.address
scandir = target.scandir
protocol = service.protocol
port = service.port
# Special cases for HTTP.
http_scheme = 'https' if 'https' in service.name or service.secure is True else 'http'
nmap_extra = target.autorecon.args.nmap
if target.autorecon.args.nmap_append:
nmap_extra += ' ' + target.autorecon.args.nmap_append
if protocol == 'udp':
nmap_extra += ' -sU'
matching_plugins = []
heading = False
for plugin in target.autorecon.plugin_types['service']:
plugin_tag = service.tag() + '/' + plugin.slug
for s in plugin.service_names:
if re.search(s, service.name):
plugin_tag_set = set(plugin.tags)
matching_tags = False
for tag_group in target.autorecon.tags:
if set(tag_group).issubset(plugin_tag_set):
matching_tags = True
break
excluded_tags = False
for tag_group in target.autorecon.excluded_tags:
if set(tag_group).issubset(plugin_tag_set):
excluded_tags = True
break
# TODO: Maybe make this less messy, keep manual-only plugins separate?
plugin_is_runnable = False
for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod):
if member_name == 'run':
plugin_is_runnable = True
break
if plugin_is_runnable and matching_tags and not excluded_tags:
# Skip plugin if run_once_boolean and plugin already in target scans
if plugin.run_once_boolean and (plugin.slug,) in target.scans:
warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin should only be run once and it appears to have already been queued. Skipping.{rst}')
continue
# Skip plugin if require_ssl_boolean and port is not secure
if plugin.require_ssl_boolean and not service.secure:
continue
# Skip plugin if service port is in ignore_ports:
if port in plugin.ignore_ports[protocol]:
warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}')
continue
# Skip plugin if plugin has required ports and service port is not in them:
if plugin.ports[protocol] and port not in plugin.ports[protocol]:
warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin can only run on specific ports. Skipping.{rst}')
continue
for i in plugin.ignore_service_names:
if re.search(i, service.name):
warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin cannot be run against this service. Skipping.{rst}')
continue
# TODO: check if plugin matches tags, BUT run manual commands anyway!
matching_plugins.append(plugin)
if plugin.manual_commands and (not plugin.run_once_boolean or (plugin.run_once_boolean and (plugin.slug,) not in target.scans)):
with open(os.path.join(scandir, '_manual_commands.txt'), 'a') as file:
if not heading:
file.write(e('[*] {service.name} on {service.protocol}/{service.port}\n\n'))
heading = True
for description, commands in plugin.manual_commands.items():
file.write('\t[-] ' + e(description) + '\n\n')
for command in commands:
file.write('\t\t' + e(command) + '\n\n')
file.flush()
break
for plugin in matching_plugins:
plugin_tag = service.tag() + '/' + plugin.slug
scan_tuple = (service.protocol, service.port, service.name, plugin.slug)
if plugin.run_once_boolean:
scan_tuple = (plugin.slug,)
if scan_tuple in target.scans:
warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin appears to have already been queued, but it is not marked as run_once. Possible duplicate service tag? Skipping.{rst}')
continue
else:
target.scans.append(scan_tuple)
pending.add(asyncio.create_task(service_scan(plugin, service)))
heartbeat.cancel()
elapsed_time = calculate_elapsed_time(start_time)
if timed_out:
for task in pending:
task.cancel()
for process_list in target.running_tasks.values():
for process_dict in process_list['processes']:
process_dict['process'].kill()
warn('{byellow}Scanning target ' + target.address + ' took longer than the specified target period (' + str(autorecon.config['target_timeout']) + ' min). Cancelling scans and moving to next target.{rst}')
else:
info('Finished scanning target {byellow}' + target.address + '{rst} in ' + elapsed_time)
async with autorecon.lock:
autorecon.scanning_targets.remove(target)
async def main():
from autorecon import Plugin, PortScan, ServiceScan, Target # We have to do this to get around issubclass weirdness when loading plugins.
parser = argparse.ArgumentParser(add_help=False, description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.')
parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs='*')
parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.')
parser.add_argument('-m', '--max-scans', action='store', type=int, help='The maximum number of concurrent scans to run. Default: 50')
parser.add_argument('-mp', '--max-port-scans', action='store', type=int, help='The maximum number of concurrent port scans to run. Default: 10 (approx 20%% of max-scans unless specified)')
parser.add_argument('-c', '--config', action='store', type=str, default=os.path.dirname(os.path.realpath(__file__)) + '/config.toml', dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s')
parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.dirname(os.path.realpath(__file__)) + '/global.toml')
parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group.')
parser.add_argument('--exclude-tags', action='store', type=str, default='', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group.')
parser.add_argument('--plugins-dir', action='store', type=str, default=os.path.dirname(os.path.abspath(__file__)) + '/plugins', help='')
parser.add_argument('-o', '--output', action='store', dest='outdir', help='The output directory for results. Default: results')
parser.add_argument('--single-target', action='store_true', help='Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the output directory. Default: false')
parser.add_argument('--only-scans-dir', action='store_true', help='Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: false')
parser.add_argument('--heartbeat', action='store', type=int, help='Specifies the heartbeat interval (in seconds) for scan status messages. Default: 60')
parser.add_argument('--timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: no timeout')
parser.add_argument('--target-timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. Default: no timeout')
nmap_group = parser.add_mutually_exclusive_group()
nmap_group.add_argument('--nmap', action='store', default='-vv --reason -Pn', help='Override the {nmap_extra} variable in scans. Default: %(default)s')
nmap_group.add_argument('--nmap-append', action='store', default='', help='Append to the default {nmap_extra} variable in scans.')
parser.add_argument('--disable-sanity-checks', action='store_true', default=False, help='Disable sanity checks that would otherwise prevent the scans from running. Default: false')
parser.add_argument('--accessible', action='store_true', help='Attempts to make AutoRecon output more accessible to screenreaders.')
parser.add_argument('-v', '--verbose', action='count', help='Enable verbose output. Repeat for more verbosity.')
parser.error = lambda s: fail(s[0].upper() + s[1:])
args, unknown = parser.parse_known_args()
errors = False
autorecon.argparse = parser
# Parse config file and args for global.toml first.
if not os.path.isfile(args.config_file):
fail('Error: Specified config file "' + args.config_file + '" does not exist.')
with open(args.config_file) as c:
try:
config_toml = toml.load(c)
for key, val in config_toml.items():
if key.replace('-', '_') == 'global_file':
autorecon.config['global_file'] = val
break
except toml.decoder.TomlDecodeError:
fail('Error: Couldn\'t parse ' + args.config_file + ' config file. Check syntax.')
args_dict = vars(args)
for key in args_dict:
if key == 'global_file' and args_dict['global_file'] is not None:
autorecon.config['global_file'] = args_dict['global_file']
break
if not os.path.isdir(args.plugins_dir):
fail('Error: Specified plugins directory "' + args.plugins_dir + '" does not exist.')
for plugin_file in os.listdir(args.plugins_dir):
if not plugin_file.startswith('_') and plugin_file.endswith('.py'):
dirname, filename = os.path.split(plugin_file)
dirname = os.path.abspath(dirname)
try:
plugin = importlib.import_module('.' + filename[:-3], os.path.basename(args.plugins_dir))
clsmembers = inspect.getmembers(plugin, predicate=inspect.isclass)
for (_, c) in clsmembers:
if c.__module__ == 'autorecon':
continue
if c.__name__.lower() in autorecon.config['protected_classes']:
print('Plugin "' + c.__name__ + '" in ' + filename + ' is using a protected class name. Please change it.')
sys.exit(1)
# Only add classes that are a sub class of either PortScan or ServiceScan
if issubclass(c, PortScan) or issubclass(c, ServiceScan):
autorecon.register(c())
else:
print('Plugin "' + c.__name__ + '" in ' + filename + ' is not a subclass of either PortScan or ServiceScan.')
except (ImportError, SyntaxError) as ex:
print('cannot import ' + filename + ' plugin')
print(ex)
sys.exit(1)
if len(autorecon.plugin_types['port']) == 0:
fail('Error: There are no valid PortScan plugins in the plugins directory "' + args.plugins_dir + '".')
# Sort plugins by priority.
autorecon.plugin_types['port'].sort(key=lambda x: x.priority)
autorecon.plugin_types['service'].sort(key=lambda x: x.priority)
if not os.path.isfile(autorecon.config['global_file']):
fail('Error: Specified global file "' + autorecon.config['global_file'] + '" does not exist.')
global_plugin_args = None
with open(autorecon.config['global_file']) as g:
try:
global_toml = toml.load(g)
for key, val in global_toml.items():
if key == 'global' and isinstance(val, dict): # Process global plugin options.
for gkey, gvals in global_toml['global'].items():
if isinstance(gvals, dict):
options = {'metavar':'VALUE'}
if 'default' in gvals:
options['default'] = gvals['default']
if 'metavar' in gvals:
options['metavar'] = gvals['metavar']
if 'help' in gvals:
options['help'] = gvals['help']
if 'type' in gvals:
gtype = gvals['type'].lower()
if gtype == 'constant':
if 'constant' not in gvals:
fail('Global constant option ' + gkey + ' has no constant value set.')
else:
options['action'] = 'store_const'
options['const'] = gvals['constant']
elif gtype == 'true':
options['action'] = 'store_true'
options.pop('metavar', None)
options.pop('default', None)
elif gtype == 'false':
options['action'] = 'store_false'
options.pop('metavar', None)
options.pop('default', None)
elif gtype == 'list':
options['action'] = 'append'
elif gtype == 'choice':
if 'choices' not in gvals:
fail('Global choice option ' + gkey + ' has no choices value set.')
else:
if not isinstance(gvals['choices'], list):
fail('The \'choices\' value for global choice option ' + gkey + ' should be a list.')
options['choices'] = gvals['choices']
options.pop('metavar', None)
if global_plugin_args is None:
global_plugin_args = parser.add_argument_group("global plugin arguments", description="These are optional arguments that can be used by all plugins.")
global_plugin_args.add_argument('--global.' + slugify(gkey), **options)
except toml.decoder.TomlDecodeError:
fail('Error: Couldn\'t parse ' + g.name + ' file. Check syntax.')
for key, val in config_toml.items():
if key == 'global' and isinstance(val, dict): # Process global plugin options.
for gkey, gval in config_toml['global'].items():
if isinstance(gval, bool):
for action in autorecon.argparse._actions:
if action.dest == 'global.' + slugify(gkey).replace('-', '_'):
if action.const is True:
action.__setattr__('default', gval)
break
else:
if autorecon.argparse.get_default('global.' + slugify(gkey).replace('-', '_')):
autorecon.argparse.set_defaults(**{'global.' + slugify(gkey).replace('-', '_'): gval})
elif key == 'pattern' and isinstance(val, list): # Process global patterns.
for pattern in val:
if 'pattern' in pattern:
try:
compiled = re.compile(pattern['pattern'])
if 'description' in pattern:
autorecon.patterns.append(Pattern(compiled, description=pattern['description']))
else:
autorecon.patterns.append(Pattern(compiled))
except re.error:
fail('Error: The pattern "' + pattern['pattern'] + '" in the config file is invalid regex.')
else:
fail('Error: A [[pattern]] in the config file doesn\'t have a required pattern variable.')
elif isinstance(val, dict): # Process potential plugin arguments.
for pkey, pval in config_toml[key].items():
if autorecon.argparse.get_default(slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_')):
autorecon.argparse.set_defaults(**{slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_'): pval})
else: # Process potential other options.
if key.replace('-', '_') in autorecon.configurable_keys:
autorecon.config[key.replace('-', '_')] = val
autorecon.argparse.set_defaults(**{key.replace('-', '_'): val})
parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, help='Show this help message and exit.')
args = parser.parse_args()
args_dict = vars(args)
for key in args_dict:
if key in autorecon.configurable_keys and args_dict[key] is not None:
# Special case for booleans
if key in ['accessible', 'single_target', 'only_scans_dir'] and autorecon.config[key]:
continue
autorecon.config[key] = args_dict[key]
autorecon.args = args
if autorecon.config['max_scans'] <= 0:
error('Argument -m/--max-scans must be at least 1.')
errors = True
if autorecon.config['max_port_scans'] is None:
autorecon.config['max_port_scans'] = max(1, round(autorecon.config['max_scans'] * 0.2))
else:
if autorecon.config['max_port_scans'] <= 0:
error('Argument -mp/--max-port-scans must be at least 1.')
errors = True
if autorecon.config['max_port_scans'] > autorecon.config['max_scans']:
error('Argument -mp/--max-port-scans cannot be greater than argument -m/--max-scans.')
errors = True
if autorecon.config['heartbeat'] <= 0:
error('Argument --heartbeat must be at least 1.')
errors = True
if autorecon.config['timeout'] is not None and autorecon.config['timeout'] <= 0:
error('Argument --timeout must be at least 1.')
errors = True
if autorecon.config['target_timeout'] is not None and autorecon.config['target_timeout'] <= 0:
error('Argument --target-timeout must be at least 1.')
errors = True
if autorecon.config['timeout'] is not None and autorecon.config['target_timeout'] is not None and autorecon.config['timeout'] < autorecon.config['target_timeout']:
error('Argument --timeout cannot be less than --target-timeout.')
errors = True
if not errors:
autorecon.port_scan_semaphore = asyncio.Semaphore(autorecon.config['max_port_scans'])
# If max scans and max port scans is the same, the service scan semaphore and port scan semaphore should be the same object
if autorecon.config['max_scans'] == autorecon.config['max_port_scans']:
autorecon.service_scan_semaphore = autorecon.port_scan_semaphore
else:
autorecon.service_scan_semaphore = asyncio.Semaphore(autorecon.config['max_scans'] - autorecon.config['max_port_scans'])
tags = []
for tag_group in list(set(filter(None, args.tags.lower().split(',')))):
tags.append(list(set(filter(None, tag_group.split('+')))))
# Remove duplicate lists from list.
[autorecon.tags.append(t) for t in tags if t not in autorecon.tags]
excluded_tags = []
if args.exclude_tags != '':
for tag_group in list(set(filter(None, args.exclude_tags.lower().split(',')))):
excluded_tags.append(list(set(filter(None, tag_group.split('+')))))
# Remove duplicate lists from list.
[autorecon.excluded_tags.append(t) for t in excluded_tags if t not in autorecon.excluded_tags]
# Generate manual commands.
for _, plugin in autorecon.plugins.items():
for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod):
if member_name == 'manual':
plugin.manual()
raw_targets = args.targets
if len(args.target_file) > 0:
if not os.path.isfile(args.target_file):
error('The target file ' + args.target_file + ' was not found.')
sys.exit(1)
try:
with open(args.target_file, 'r') as f:
lines = f.read()
for line in lines.splitlines():
line = line.strip()
if line.startswith('#') or len(line) == 0: continue
if line not in raw_targets:
raw_targets.append(line)
except OSError:
error('The target file ' + args.target_file + ' could not be read.')
sys.exit(1)
for target in raw_targets:
try:
ip = str(ipaddress.ip_address(target))
if ip not in autorecon.pending_targets:
autorecon.pending_targets.append(ip)
except ValueError:
try:
target_range = ipaddress.ip_network(target, strict=False)
if not args.disable_sanity_checks and target_range.num_addresses > 256:
error(target + ' contains ' + str(target_range.num_addresses) + ' addresses. Check that your CIDR notation is correct. If it is, re-run with the --disable-sanity-checks option to suppress this check.')
errors = True
else:
for ip in target_range.hosts():
ip = str(ip)
if ip not in autorecon.pending_targets:
autorecon.pending_targets.append(ip)
except ValueError:
try:
ip = socket.gethostbyname(target)
if target not in autorecon.pending_targets:
autorecon.pending_targets.append(target)
except socket.gaierror:
error(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.')
errors = True
if len(autorecon.pending_targets) == 0:
error('You must specify at least one target to scan!')
errors = True
if autorecon.config['single_target'] and len(autorecon.pending_targets) != 1:
error('You cannot provide more than one target when scanning in single-target mode.')
errors = True
if not args.disable_sanity_checks and len(autorecon.pending_targets) > 256:
error('A total of ' + str(len(autorecon.pending_targets)) + ' targets would be scanned. If this is correct, re-run with the --disable-sanity-checks option to suppress this check.')
errors = True
port_scan_plugin_count = 0
for plugin in autorecon.plugin_types['port']:
matching_tags = False
for tag_group in autorecon.tags:
if set(tag_group).issubset(set(plugin.tags)):
matching_tags = True
break
excluded_tags = False
for tag_group in autorecon.excluded_tags:
if set(tag_group).issubset(set(plugin.tags)):
excluded_tags = True
break
if matching_tags and not excluded_tags:
port_scan_plugin_count += 1
if port_scan_plugin_count == 0:
error('There are no port scan plugins that match the tags specified.')
errors = True
if errors:
sys.exit(1)
autorecon.config['port_scan_plugin_count'] = port_scan_plugin_count
num_initial_targets = max(1, math.ceil(autorecon.config['max_port_scans'] / port_scan_plugin_count))
start_time = time.time()
pending = []
i = 0
while autorecon.pending_targets:
pending.append(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon))))
i+=1
if i >= num_initial_targets:
break
timed_out = False
while pending:
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1)
# Check if global timeout has occurred.
if autorecon.config['timeout'] is not None:
elapsed_seconds = round(time.time() - start_time)
m, s = divmod(elapsed_seconds, 60)
if m >= autorecon.config['timeout']:
timed_out = True
break
for task in done:
if autorecon.pending_targets:
pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon))))
port_scan_task_count = 0
for targ in autorecon.scanning_targets:
for process_list in targ.running_tasks.values():
if issubclass(process_list['plugin'].__class__, PortScan):
port_scan_task_count += 1
num_new_targets = math.ceil((autorecon.config['max_port_scans'] - port_scan_task_count) / port_scan_plugin_count)
if num_new_targets > 0:
i = 0
while autorecon.pending_targets:
pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon))))
i+=1
if i >= num_new_targets:
break
if timed_out:
cancel_all_tasks(None, None)
elapsed_time = calculate_elapsed_time(start_time)
warn('{byellow}AutoRecon took longer than the specified timeout period (' + str(autorecon.config['timeout']) + ' min). Cancelling all scans and exiting.{rst}')
sys.exit(0)
else:
while len(asyncio.all_tasks()) > 1: # this code runs in the main() task so it will be the only task left running
await asyncio.sleep(1)
elapsed_time = calculate_elapsed_time(start_time)
info('{bright}Finished scanning all targets in ' + elapsed_time + '!{rst}')
if __name__ == '__main__':
signal.signal(signal.SIGINT, cancel_all_tasks)
try:
asyncio.run(main())
except asyncio.exceptions.CancelledError:
pass