From 52ba61e6ebbe5809220ebe0291aedc4166e9dd09 Mon Sep 17 00:00:00 2001 From: blockomat2100 Date: Mon, 8 Nov 2021 07:09:39 +0100 Subject: [PATCH] initial version of dependent plugins --- autorecon/helper/__init__.py | 0 autorecon/helper/scan.py | 40 ++ autorecon/io.py | 19 +- autorecon/main.py | 185 +----- autorecon/plugins.py | 768 ++++++++++++++++--------- autorecon/targets.py | 4 +- autorecon/test-plugins/__init__.py | 0 autorecon/test-plugins/http_server2.py | 38 ++ 8 files changed, 601 insertions(+), 453 deletions(-) create mode 100644 autorecon/helper/__init__.py create mode 100644 autorecon/helper/scan.py create mode 100644 autorecon/test-plugins/__init__.py create mode 100644 autorecon/test-plugins/http_server2.py diff --git a/autorecon/helper/__init__.py b/autorecon/helper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autorecon/helper/scan.py b/autorecon/helper/scan.py new file mode 100644 index 0000000..e1a8cb9 --- /dev/null +++ b/autorecon/helper/scan.py @@ -0,0 +1,40 @@ +import time + + +def calculate_elapsed_time(start_time, short=False): + elapsed_seconds = round(time.time() - start_time) + + m, s = divmod(elapsed_seconds, 60) + h, m = divmod(m, 60) + + elapsed_time = [] + if short: + elapsed_time.append(str(h).zfill(2)) + else: + if h == 1: + elapsed_time.append(str(h) + ' hour') + elif h > 1: + elapsed_time.append(str(h) + ' hours') + + if short: + elapsed_time.append(str(m).zfill(2)) + else: + if m == 1: + elapsed_time.append(str(m) + ' minute') + elif m > 1: + elapsed_time.append(str(m) + ' minutes') + + if short: + elapsed_time.append(str(s).zfill(2)) + else: + 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') + + if short: + return ':'.join(elapsed_time) + else: + return ', '.join(elapsed_time) diff --git a/autorecon/io.py b/autorecon/io.py index a6563c7..760a659 100644 --- a/autorecon/io.py +++ b/autorecon/io.py @@ -97,11 +97,12 @@ def fail(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): class CommandStreamReader(object): - def __init__(self, stream, target, tag, patterns=None, outfile=None): + def __init__(self, stream, target, tag, patterns=None, outfile=None, plugin=None): self.stream = stream self.target = target self.tag = tag self.lines = [] + self.plugin = plugin self.patterns = patterns or [] self.outfile = outfile self.ended = False @@ -136,6 +137,22 @@ class CommandStreamReader(object): else: info('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} {bmagenta}Matched Pattern: ' + match + '{rst}', verbosity=2) file.writelines('Matched Pattern: ' + match + '\n\n') + debug(str(self.plugin.__dict__)) + next_plugins = self.target.autorecon.get_next_service_scan_plugins(self.plugin) + info(str(next_plugins)) + for next_plugin in next_plugins: + info("Dict: %s" % str(self.target.__dict__)) + for service, details in self.target.scans.get('services', {}).items(): + for key, value in details.items(): + if value.get('plugin') == self.plugin: + info("Value: %s" % str(value)) + # info("Value: %s" % str(value)) + #info("Service Details: %s" % str(details)) + #new_service = Service() + self.target.autorecon.queue_new_service_scan(next_plugin, service) + #for next_plugin in next_plugins: + # async def service_scan(plugin, service, run_from_service_scan=False): + #autorecon_queue_service_scan(next_plugin, run_fr) if self.outfile is not None: with open(self.outfile, 'a') as writer: diff --git a/autorecon/main.py b/autorecon/main.py index 2ef271e..bd80ae0 100644 --- a/autorecon/main.py +++ b/autorecon/main.py @@ -13,8 +13,9 @@ except ModuleNotFoundError: colorama.init() from autorecon.config import config, configurable_keys, configurable_boolean_keys +from autorecon.helper.scan import calculate_elapsed_time from autorecon.io import slugify, e, fformat, cprint, debug, info, warn, error, fail, CommandStreamReader -from autorecon.plugins import Pattern, PortScan, ServiceScan, Report, AutoRecon +from autorecon.plugins import Pattern, PortScan, ServiceScan, Report, AutoRecon, service_scan, get_semaphore from autorecon.targets import Target, Service VERSION = "2.0.5" @@ -41,43 +42,6 @@ terminal_settings = termios.tcgetattr(sys.stdin.fileno()) autorecon = AutoRecon() -def calculate_elapsed_time(start_time, short=False): - elapsed_seconds = round(time.time() - start_time) - - m, s = divmod(elapsed_seconds, 60) - h, m = divmod(m, 60) - - elapsed_time = [] - if short: - elapsed_time.append(str(h).zfill(2)) - else: - if h == 1: - elapsed_time.append(str(h) + ' hour') - elif h > 1: - elapsed_time.append(str(h) + ' hours') - - if short: - elapsed_time.append(str(m).zfill(2)) - else: - if m == 1: - elapsed_time.append(str(m) + ' minute') - elif m > 1: - elapsed_time.append(str(m) + ' minutes') - - if short: - elapsed_time.append(str(s).zfill(2)) - else: - 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') - - if short: - return ':'.join(elapsed_time) - else: - return ', '.join(elapsed_time) def cancel_all_tasks(signal, frame): for task in asyncio.all_tasks(): @@ -162,40 +126,6 @@ async def keyboard(): input = input[1:] await asyncio.sleep(0.1) -async def get_semaphore(autorecon): - semaphore = autorecon.service_scan_semaphore - while True: - # If service scan semaphore is locked, see if we can use port scan semaphore. - if semaphore.locked(): - if semaphore != autorecon.port_scan_semaphore: # This will be true unless user sets max_scans == max_port_scans - - port_scan_task_count = 0 - for target in autorecon.scanning_targets: - for process_list in target.running_tasks.values(): - if issubclass(process_list['plugin'].__class__, PortScan): - port_scan_task_count += 1 - - if not autorecon.pending_targets and (config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. - if autorecon.port_scan_semaphore.locked(): - await asyncio.sleep(1) - continue - semaphore = autorecon.port_scan_semaphore - break - else: # Do some math to see if we can use the port scan semaphore. - if (config['max_port_scans'] - (port_scan_task_count + (len(autorecon.pending_targets) * config['port_scan_plugin_count']))) >= 1: - if autorecon.port_scan_semaphore.locked(): - await asyncio.sleep(1) - continue - semaphore = autorecon.port_scan_semaphore - break - else: - await asyncio.sleep(1) - else: - break - else: - break - return semaphore - async def port_scan(plugin, target): if config['ports']: if config['ports']['tcp'] or config['ports']['udp']: @@ -261,93 +191,6 @@ async def port_scan(plugin, target): info('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished in ' + elapsed_time, verbosity=2) return {'type':'port', 'plugin':plugin, 'result':result} -async def service_scan(plugin, service): - semaphore = service.target.autorecon.service_scan_semaphore - - if not config['force_services']: - semaphore = await get_semaphore(service.target.autorecon) - - async with semaphore: - # Create variables for fformat references. - address = service.target.address - addressv6 = service.target.address - ipaddress = service.target.ip - ipaddressv6 = service.target.ip - scandir = service.target.scandir - protocol = service.protocol - port = service.port - name = service.name - - if config['create_port_dirs']: - scandir = os.path.join(scandir, protocol + str(port)) - os.makedirs(scandir, exist_ok=True) - os.makedirs(os.path.join(scandir, 'xml'), exist_ok=True) - - # Special cases for HTTP. - http_scheme = 'https' if 'https' in service.name or service.secure is True else 'http' - - nmap_extra = service.target.autorecon.args.nmap - if service.target.autorecon.args.nmap_append: - nmap_extra += ' ' + service.target.autorecon.args.nmap_append - - if protocol == 'udp': - nmap_extra += ' -sU' - - if service.target.ipversion == 'IPv6': - nmap_extra += ' -6' - if addressv6 == service.target.ip: - addressv6 = '[' + addressv6 + ']' - ipaddressv6 = '[' + ipaddressv6 + ']' - - if config['proxychains'] and protocol == 'tcp': - nmap_extra += ' -sT' - - tag = service.tag() + '/' + plugin.slug - - info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}', verbosity=1) - - start_time = time.time() - - async with service.target.lock: - service.target.running_tasks[tag] = {'plugin': plugin, 'processes': [], 'start': start_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 + ' {green}(' + 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 + ' {green}(' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.', verbosity=2) - 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 + ' {green}(' + 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.', verbosity=2) - 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 + ' {green}(' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished in ' + elapsed_time, verbosity=2) - return {'type':'service', 'plugin':plugin, 'result':result} async def generate_report(plugin, targets): semaphore = autorecon.service_scan_semaphore @@ -400,7 +243,8 @@ async def scan_target(target): target.reportdir = reportdir - pending = [] + # pending = [] + autorecon = target.autorecon heartbeat = asyncio.create_task(start_heartbeat(target, period=config['heartbeat'])) @@ -423,7 +267,7 @@ async def scan_target(target): services.append(service) if services: - pending.append(asyncio.create_task(asyncio.sleep(0))) + autorecon.pending.append(asyncio.create_task(asyncio.sleep(0))) else: error('No services were defined. Please check your service syntax: [tcp|udp]///[secure|insecure]') heartbeat.cancel() @@ -454,7 +298,7 @@ async def scan_target(target): if matching_tags and not excluded_tags: target.scans['ports'][plugin.slug] = {'plugin':plugin, 'commands':[]} - pending.append(asyncio.create_task(port_scan(plugin, target))) + autorecon.pending.append(asyncio.create_task(port_scan(plugin, target))) async with autorecon.lock: autorecon.scanning_targets.append(target) @@ -463,8 +307,9 @@ async def scan_target(target): 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) + while autorecon.pending: + done, autorecon.pending = await asyncio.wait(autorecon.pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) + autorecon.pending = list(autorecon.pending) # Check if global timeout has occurred. if config['target_timeout'] is not None: @@ -690,7 +535,7 @@ async def scan_target(target): target.scans['services'][service] = {} target.scans['services'][service][plugin_tag] = {'plugin':plugin, 'commands':[]} - pending.add(asyncio.create_task(service_scan(plugin, service))) + autorecon.pending.append(asyncio.create_task(service_scan(plugin, service))) if not service_match: warn('{byellow}[' + target.address + ']{srst} Service ' + service.full_tag() + ' did not match any plugins based on the service name.{rst}', verbosity=2) @@ -717,17 +562,17 @@ async def scan_target(target): break if matching_tags and not excluded_tags: - pending.add(asyncio.create_task(generate_report(plugin, [target]))) - - while pending: - done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) + autorecon.pending.append(asyncio.create_task(generate_report(plugin, [target]))) + while autorecon.pending: + done, autorecon.pending = await asyncio.wait(autorecon.pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) + autorecon.pending = list(autorecon.pending) heartbeat.cancel() elapsed_time = calculate_elapsed_time(start_time) if timed_out: - for task in pending: + for task in autorecon.pending: task.cancel() for process_list in target.running_tasks.values(): diff --git a/autorecon/plugins.py b/autorecon/plugins.py index e3eb295..e422567 100644 --- a/autorecon/plugins.py +++ b/autorecon/plugins.py @@ -1,355 +1,563 @@ -import asyncio, inspect, os, re, sys +import asyncio, inspect, re from typing import final -from autorecon.config import config -from autorecon.io import slugify, error, fail, CommandStreamReader +from autorecon.io import slugify, fail, CommandStreamReader from autorecon.targets import Service +from autorecon.config import config +from autorecon.io import info, warn, error, cprint +import os +import sys +import time +import traceback +from colorama import Fore +from autorecon.helper.scan import calculate_elapsed_time + + +async def get_semaphore(autorecon): + semaphore = autorecon.service_scan_semaphore + while True: + # If service scan semaphore is locked, see if we can use port scan semaphore. + if semaphore.locked(): + if semaphore != autorecon.port_scan_semaphore: # This will be true unless user sets max_scans == max_port_scans + + port_scan_task_count = 0 + for target in autorecon.scanning_targets: + for process_list in target.running_tasks.values(): + info(str(process_list['plugin'].__dict__)) + info(type(process_list['plugin'])) + if issubclass(process_list['plugin'].__class__, PortScan): + port_scan_task_count += 1 + + if not autorecon.pending_targets and (config[ + 'max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. + if autorecon.port_scan_semaphore.locked(): + await asyncio.sleep(1) + continue + semaphore = autorecon.port_scan_semaphore + break + else: # Do some math to see if we can use the port scan semaphore. + if (config['max_port_scans'] - (port_scan_task_count + ( + len(autorecon.pending_targets) * config['port_scan_plugin_count']))) >= 1: + if autorecon.port_scan_semaphore.locked(): + await asyncio.sleep(1) + continue + semaphore = autorecon.port_scan_semaphore + break + else: + await asyncio.sleep(1) + else: + break + else: + break + return semaphore + + +async def service_scan(plugin, service, run_from_service_scan=False): + # skip running service scan plugins that are meant to be run for specific services + if plugin.has_previous_plugins() and not run_from_service_scan: + return + + semaphore = service.target.autorecon.service_scan_semaphore + + if not config['force_services']: + semaphore = await get_semaphore(service.target.autorecon) + + async with semaphore: + # Create variables for fformat references. + address = service.target.address + addressv6 = service.target.address + ipaddress = service.target.ip + ipaddressv6 = service.target.ip + scandir = service.target.scandir + protocol = service.protocol + port = service.port + name = service.name + + if config['create_port_dirs']: + scandir = os.path.join(scandir, protocol + str(port)) + os.makedirs(scandir, exist_ok=True) + os.makedirs(os.path.join(scandir, 'xml'), exist_ok=True) + + # Special cases for HTTP. + http_scheme = 'https' if 'https' in service.name or service.secure is True else 'http' + + nmap_extra = service.target.autorecon.args.nmap + if service.target.autorecon.args.nmap_append: + nmap_extra += ' ' + service.target.autorecon.args.nmap_append + + if protocol == 'udp': + nmap_extra += ' -sU' + + if service.target.ipversion == 'IPv6': + nmap_extra += ' -6' + if addressv6 == service.target.ip: + addressv6 = '[' + addressv6 + ']' + ipaddressv6 = '[' + ipaddressv6 + ']' + + if config['proxychains'] and protocol == 'tcp': + nmap_extra += ' -sT' + + tag = service.tag() + '/' + plugin.slug + + info( + 'Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}', + verbosity=1) + + start_time = time.time() + + async with service.target.lock: + service.target.running_tasks[tag] = {'plugin': plugin, 'processes': [], 'start': start_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 + ' {green}(' + 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 + ' {green}(' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.', + verbosity=2) + 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 + ' {green}(' + 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.', + verbosity=2) + 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 + ' {green}(' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished in ' + elapsed_time, + verbosity=2) + return {'type': 'service', 'plugin': plugin, 'result': result} + class Pattern: + def __init__(self, pattern, description=None, plugins=None): + self.pattern = pattern + self.description = description + if not plugins: + self.plugins = [] + else: + self.plugins = plugins - def __init__(self, pattern, description=None): - self.pattern = pattern - self.description = description class Plugin(object): - def __init__(self): - self.name = None - self.slug = None - self.description = None - self.tags = ['default'] - self.priority = 1 - self.patterns = [] - self.autorecon = None - self.disabled = False + def __init__(self): + self.name = None + self.slug = None + self.description = None + self.tags = ['default'] + self.priority = 1 + self.patterns = [] + 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_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_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_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_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, nargs='+', metavar='VALUE', default=default, help=help) + @final + def add_list_option(self, name, default=None, help=None): + self.autorecon.add_argument(self, name, nargs='+', 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 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('-', '_') + @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 + if name in vars(self.autorecon.args): + return vars(self.autorecon.args)[name] + else: + return None - @final - def get_global_option(self, name, default=None): - name = 'global.' + slugify(name).replace('-', '_') + @final + def get_global_option(self, name, default=None): + name = 'global.' + slugify(name).replace('-', '_') - if name in vars(self.autorecon.args): - if vars(self.autorecon.args)[name] is None: - if default: - return default - else: - return None - else: - return vars(self.autorecon.args)[name] - else: - if default: - return default - return None + if name in vars(self.autorecon.args): + if vars(self.autorecon.args)[name] is None: + if default: + return default + else: + return None + else: + return vars(self.autorecon.args)[name] + else: + if default: + return default + return None - @final - def get_global(self, name, default=None): - return self.get_global_option(name, default) + @final + def get_global(self, name, default=None): + return self.get_global_option(name, default) + + @final + def add_pattern(self, pattern, description=None, plugins=None): + try: + compiled = re.compile(pattern) + if description: + self.patterns.append(Pattern(compiled, description=description, plugins=plugins)) + else: + self.patterns.append(Pattern(compiled, plugins=plugins)) + except re.error: + fail('Error: The pattern "' + pattern + '" in the plugin "' + self.name + '" is invalid regex.') - @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__() - self.type = None - self.specific_ports = False + def __init__(self): + super().__init__() + self.type = None + self.specific_ports = False + + async def run(self, target): + raise NotImplementedError - 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.services = [] - self.service_names = [] - self.ignore_service_names = [] - self.run_once_boolean = False - self.require_ssl_boolean = False + def __init__(self): + super().__init__() + self.ports = {'tcp': [], 'udp': []} + self.ignore_ports = {'tcp': [], 'udp': []} + self.services = [] + self.service_names = [] + self.ignore_service_names = [] + self.run_once_boolean = False + self.require_ssl_boolean = False - @final - def match_service(self, protocol, port, name, negative_match=False): - protocol = protocol.lower() - if protocol not in ['tcp', 'udp']: - print('Invalid protocol.') - sys.exit(1) + @final + def match_service(self, protocol, port, name, negative_match=False): + protocol = protocol.lower() + if protocol not in ['tcp', 'udp']: + print('Invalid protocol.') + sys.exit(1) - if not isinstance(port, list): - port = [port] + if not isinstance(port, list): + port = [port] - port = list(map(int, port)) + port = list(map(int, port)) - if not isinstance(name, list): - name = [name] + 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 + valid_regex = True + for r in name: + try: + re.compile(r) + except re.error: + print('Invalid regex: ' + r) + valid_regex = False - if not valid_regex: - sys.exit(1) + if not valid_regex: + sys.exit(1) - service = {'protocol': protocol, 'port': port, 'name': name, 'negative_match': negative_match} - self.services.append(service) + service = {'protocol': protocol, 'port': port, 'name': name, 'negative_match': negative_match} + self.services.append(service) - @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] + @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)) + 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)) + 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] + @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 + 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) + 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 require_ssl(self, boolean): + self.require_ssl_boolean = boolean - @final - def run_once(self, boolean): - self.run_once_boolean = boolean + @final + def run_once(self, boolean): + self.run_once_boolean = boolean + + @final + def match_all_service_names(self, boolean): + if boolean: + # Add a "match all" service name. + self.match_service_name('.*') + + def get_previous_plugin_names(self): + return [] + + @final + def has_previous_plugins(self): + if self.get_previous_plugin_names(): + return True + return False - @final - def match_all_service_names(self, boolean): - if boolean: - # Add a "match all" service name. - self.match_service_name('.*') class Report(Plugin): - def __init__(self): - super().__init__() + def __init__(self): + super().__init__() + class AutoRecon(object): - def __init__(self): - self.pending_targets = [] - self.scanning_targets = [] - self.completed_targets = [] - self.plugins = {} - self.__slug_regex = re.compile('^[a-z0-9\-]+$') - self.plugin_types = {'port':[], 'service':[], 'report':[]} - self.port_scan_semaphore = None - self.service_scan_semaphore = None - self.argparse = None - self.argparse_group = None - self.args = None - self.missing_services = [] - self.taglist = [] - self.tags = [] - self.excluded_tags = [] - self.patterns = [] - self.errors = False - self.lock = asyncio.Lock() - self.load_slug = None - self.load_module = None + def __init__(self): + self.pending = [] + self.pending_targets = [] + self.scanning_targets = [] + self.completed_targets = [] + self.plugins = {} + self.__slug_regex = re.compile('^[a-z0-9\-]+$') + self.plugin_types = {'port': [], 'service': [], 'report': []} + self.port_scan_semaphore = None + self.service_scan_semaphore = None + self.argparse = None + self.argparse_group = None + self.args = None + self.missing_services = [] + self.taglist = [] + self.tags = [] + self.excluded_tags = [] + self.patterns = [] + self.errors = False + 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) + 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) + 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\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\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 + def extract_service(self, line, regex): + if regex is None: + regex = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\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:] + if service.startswith('ssl/') or service.startswith('tls/'): + service = service[4:] - return Service(protocol, port, service, secure) - else: - return None + 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) + 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 + 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, filename): - if plugin.disabled: - return + def register(self, plugin, filename): + if plugin.disabled: + return + if plugin.name is None: + fail( + 'Error: Plugin with class name "' + plugin.__class__.__name__ + '" in ' + filename + ' does not have a name.') - if plugin.name is None: - fail('Error: Plugin with class name "' + plugin.__class__.__name__ + '" in ' + filename + ' does not have a name.') + for _, loaded_plugin in self.plugins.items(): + if plugin.name == loaded_plugin.name: + fail('Error: Duplicate plugin name "' + plugin.name + '" detected in ' + filename + '.', + file=sys.stderr) - for _, loaded_plugin in self.plugins.items(): - if plugin.name == loaded_plugin.name: - fail('Error: Duplicate plugin name "' + plugin.name + '" detected in ' + filename + '.', 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 + '" in ' + filename + ' is not valid (must only contain lowercase letters, numbers, and hyphens).', + 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 + '" in ' + filename + ' is not valid (must only contain lowercase letters, numbers, and hyphens).', file=sys.stderr) + if plugin.slug in config['protected_classes']: + fail('Error: plugin slug "' + plugin.slug + '" in ' + filename + ' is a protected string. Please change.') - if plugin.slug in config['protected_classes']: - fail('Error: plugin slug "' + plugin.slug + '" in ' + filename + ' is a protected string. Please change.') + if plugin.slug not in self.plugins: - if plugin.slug not in self.plugins: + for _, loaded_plugin in self.plugins.items(): + if plugin is loaded_plugin: + fail( + 'Error: plugin "' + plugin.name + '" in ' + filename + ' already loaded as "' + loaded_plugin.name + '" (' + str( + loaded_plugin) + ')', file=sys.stderr) - for _, loaded_plugin in self.plugins.items(): - if plugin is loaded_plugin: - fail('Error: plugin "' + plugin.name + '" in ' + filename + ' already loaded as "' + loaded_plugin.name + '" (' + str(loaded_plugin) + ')', file=sys.stderr) + configure_function_found = False + run_coroutine_found = False + manual_function_found = False - 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): + if len(inspect.getfullargspec(member_value).args) != 2: + fail( + 'Error: the "run" coroutine in the plugin "' + plugin.name + '" in ' + filename + ' should have two arguments.', + file=sys.stderr) + run_coroutine_found = True + elif member_name == 'manual': + if len(inspect.getfullargspec(member_value).args) != 3: + fail( + 'Error: the "manual" function in the plugin "' + plugin.name + '" in ' + filename + ' should have three arguments.', + file=sys.stderr) + manual_function_found = True - 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): - if len(inspect.getfullargspec(member_value).args) != 2: - fail('Error: the "run" coroutine in the plugin "' + plugin.name + '" in ' + filename + ' should have two arguments.', file=sys.stderr) - run_coroutine_found = True - elif member_name == 'manual': - if len(inspect.getfullargspec(member_value).args) != 3: - fail('Error: the "manual" function in the plugin "' + plugin.name + '" in ' + filename + ' should have three arguments.', file=sys.stderr) - manual_function_found = True + if not run_coroutine_found and not manual_function_found: + fail( + 'Error: the plugin "' + plugin.name + '" in ' + filename + ' needs either a "manual" function, a "run" coroutine, or both.', + file=sys.stderr) - if not run_coroutine_found and not manual_function_found: - fail('Error: the plugin "' + plugin.name + '" in ' + filename + ' needs either a "manual" function, a "run" coroutine, or both.', file=sys.stderr) + if issubclass(plugin.__class__, PortScan): + if plugin.type is None: + fail( + 'Error: the PortScan plugin "' + plugin.name + '" in ' + filename + ' requires a type (either tcp or udp).') + else: + plugin.type = plugin.type.lower() + if plugin.type not in ['tcp', 'udp']: + fail( + 'Error: the PortScan plugin "' + plugin.name + '" in ' + filename + ' has an invalid type (should be tcp or udp).') + self.plugin_types["port"].append(plugin) + elif issubclass(plugin.__class__, ServiceScan): + self.plugin_types["service"].append(plugin) + elif issubclass(plugin.__class__, Report): + self.plugin_types["report"].append(plugin) + else: + fail( + 'Plugin "' + plugin.name + '" in ' + filename + ' is neither a PortScan, ServiceScan, nor a Report.', + file=sys.stderr) - if issubclass(plugin.__class__, PortScan): - if plugin.type is None: - fail('Error: the PortScan plugin "' + plugin.name + '" in ' + filename + ' requires a type (either tcp or udp).') - else: - plugin.type = plugin.type.lower() - if plugin.type not in ['tcp', 'udp']: - fail('Error: the PortScan plugin "' + plugin.name + '" in ' + filename + ' has an invalid type (should be tcp or udp).') - self.plugin_types["port"].append(plugin) - elif issubclass(plugin.__class__, ServiceScan): - self.plugin_types["service"].append(plugin) - elif issubclass(plugin.__class__, Report): - self.plugin_types["report"].append(plugin) - else: - fail('Plugin "' + plugin.name + '" in ' + filename + ' is neither a PortScan, ServiceScan, nor a Report.', file=sys.stderr) + plugin.tags = [tag.lower() for tag in plugin.tags] - plugin.tags = [tag.lower() for tag in plugin.tags] + # Add plugin tags to tag list. + [self.taglist.append(t) for t in plugin.tags if t not in self.tags] - # Add plugin tags to tag list. - [self.taglist.append(t) for t in plugin.tags if t not in self.tags] + plugin.autorecon = self + if configure_function_found: + plugin.configure() + self.plugins[plugin.slug] = plugin + else: + fail('Error: plugin slug "' + plugin.slug + '" in ' + filename + ' is already assigned.', file=sys.stderr) - plugin.autorecon = self - if configure_function_found: - plugin.configure() - self.plugins[plugin.slug] = plugin - else: - fail('Error: plugin slug "' + plugin.slug + '" in ' + filename + ' is already assigned.', file=sys.stderr) + async def execute(self, cmd, target, tag, patterns=None, outfile=None, errfile=None, plugin=None): + if patterns: + combined_patterns = self.patterns + patterns + else: + combined_patterns = self.patterns - 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, + stdin=open('/dev/null'), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + cout = CommandStreamReader(process.stdout, target, tag, patterns=combined_patterns, outfile=outfile, + plugin=plugin) + cerr = CommandStreamReader(process.stderr, target, tag, patterns=combined_patterns, outfile=errfile, + plugin=plugin) - process = await asyncio.create_subprocess_shell( - cmd, - stdin=open('/dev/null'), - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE) + asyncio.create_task(cout._read()) + asyncio.create_task(cerr._read()) - cout = CommandStreamReader(process.stdout, target, tag, patterns=combined_patterns, outfile=outfile) - cerr = CommandStreamReader(process.stderr, target, tag, patterns=combined_patterns, outfile=errfile) + return process, cout, cerr - asyncio.create_task(cout._read()) - asyncio.create_task(cerr._read()) + def get_plugin_by_name(self, name): + for key, value in self.plugins.items(): + if value.name == name: + return self.plugins[key] - return process, cout, cerr + def get_next_service_scan_plugins(self, current_plugin): + next_plugins = [] + for plugin in self.plugin_types['service']: + if not plugin.has_previous_plugins(): + continue + for previous_plugin_name in plugin.get_previous_plugin_names(): + if current_plugin.name == previous_plugin_name: + next_plugins.append(plugin) + return next_plugins + + def queue_new_service_scan(self, plugin, service): + # try using append. in the main method "pending" is sometimes set() and sometimes list() + self.pending.append(asyncio.create_task(service_scan(plugin, service, run_from_service_scan=True))) diff --git a/autorecon/targets.py b/autorecon/targets.py index 014ef71..ae39d88 100644 --- a/autorecon/targets.py +++ b/autorecon/targets.py @@ -121,7 +121,7 @@ class Service: self.add_manual_commands(description, command) @final - async def execute(self, cmd, blocking=True, outfile=None, errfile=None, future_outfile=None): + async def execute(self, cmd, blocking=True, outfile=None, errfile=None, future_outfile=None, plugin=None): target = self.target # Create variables for command references. @@ -182,7 +182,7 @@ class Service: with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: file.writelines(cmd + '\n\n') - process, stdout, stderr = await target.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) + process, stdout, stderr = await target.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile, plugin=plugin) target.running_tasks[tag]['processes'].append({'process': process, 'stderr': stderr, 'cmd': cmd}) diff --git a/autorecon/test-plugins/__init__.py b/autorecon/test-plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autorecon/test-plugins/http_server2.py b/autorecon/test-plugins/http_server2.py new file mode 100644 index 0000000..7b7b3a8 --- /dev/null +++ b/autorecon/test-plugins/http_server2.py @@ -0,0 +1,38 @@ +from autorecon.plugins import ServiceScan + + +class DirectoryListing(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Directory Listing" + self.tags = ['default', 'safe', 'http', 'test'] + + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + self.add_pattern('

Directory listing for', description='Directory Listing enabled', + plugin_names=["Directory Listing Verify"]) + + async def run(self, service): + await service.execute('curl {http_scheme}://{addressv6}:{port}') + + +class DirectoryListingVerify(ServiceScan): + """ + this is a useless plugin that is only run, if directory listing was found. + """ + def __init__(self): + super().__init__() + self.name = "Directory Listing verify" + self.tags = ['default', 'safe', 'http', 'test'] + + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + + async def run(self, service): + await service.execute('curl {http_scheme}://{addressv6}:{port}/?id=1') + + def get_previous_plugin_names(self): + return ["Directory Listing"]