#!/usr/bin/python3 import argparse, asyncio, importlib.util, inspect, ipaddress, math, os, re, select, shutil, signal, socket, sys, termios, time, traceback, tty import xml.etree.ElementTree as ET from datetime import datetime try: import colorama, impacket, platformdirs, psutil, requests, toml, unidecode from colorama import Fore, Style except ModuleNotFoundError: print('One or more required modules was not installed. Please run or re-run: ' + ('sudo ' if os.getuid() == 0 else '') + 'python3 -m pip install -r requirements.txt') sys.exit(1) colorama.init() from autorecon.config import config, configurable_keys, configurable_boolean_keys 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.targets import Target, Service VERSION = "2.0.36" def latest_mtime(path): """Recursively get the latest modification time in a directory.""" if not os.path.exists(path): return 0 if os.path.isfile(path): return os.path.getmtime(path) latest = os.path.getmtime(path) for root, _, files in os.walk(path): for f in files: fpath = os.path.join(root, f) latest = max(latest, os.path.getmtime(fpath)) return latest def needs_update(src, dst): """Return True if dst doesn't exist or src contains newer files than dst.""" if not os.path.exists(dst): return True return latest_mtime(src) > latest_mtime(dst) def parse_nmap_xml(xml_file): """Parse an nmap XML file and return {ip: [(protocol, port, service_name, secure), ...]}.""" result = {} try: tree = ET.parse(xml_file) root = tree.getroot() except ET.ParseError as exc: fail('Error parsing nmap XML file "' + xml_file + '": ' + str(exc)) return result for host in root.findall('host'): status = host.find('status') if status is not None and status.get('state') != 'up': continue address = None for addr_elem in host.findall('address'): addrtype = addr_elem.get('addrtype', '') if addrtype in ('ipv4', 'ipv6'): address = addr_elem.get('addr') break if address is None: hostnames = host.find('hostnames') if hostnames is not None: for hn in hostnames.findall('hostname'): address = hn.get('name') break if address is None: continue services = [] ports_elem = host.find('ports') if ports_elem is None: continue for port_elem in ports_elem.findall('port'): state = port_elem.find('state') if state is None or state.get('state') != 'open': continue protocol = port_elem.get('protocol', 'tcp').lower() port_num = int(port_elem.get('portid', 0)) svc_elem = port_elem.find('service') if svc_elem is not None: name = svc_elem.get('name', 'unknown') tunnel = svc_elem.get('tunnel', '') secure = tunnel in ('ssl', 'tls') or 'ssl' in name or 'tls' in name else: name = 'unknown' secure = False services.append((protocol, port_num, name, secure)) if services: result[address] = services return result # ----------------------- CONFIG DIR ----------------------- if not os.path.exists(config['config_dir']): shutil.rmtree(config['config_dir'], ignore_errors=True, onerror=None) os.makedirs(config['config_dir'], exist_ok=True) open(os.path.join(config['config_dir'], 'VERSION-' + VERSION), 'a').close() shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config.toml'), os.path.join(config['config_dir'], 'config.toml')) shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'global.toml'), os.path.join(config['config_dir'], 'global.toml')) else: if not os.path.exists(os.path.join(config['config_dir'], 'config.toml')): shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config.toml'), os.path.join(config['config_dir'], 'config.toml')) if not os.path.exists(os.path.join(config['config_dir'], 'global.toml')): shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'global.toml'), os.path.join(config['config_dir'], 'global.toml')) if not os.path.exists(os.path.join(config['config_dir'], 'VERSION-' + VERSION)): warn('It looks like the config in ' + config['config_dir'] + ' is outdated. Please remove the ' + config['config_dir'] + ' directory and re-run AutoRecon to rebuild it.') # ----------------------- DATA DIR ----------------------- plugins_src = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default-plugins') plugins_dst = os.path.join(config['data_dir'], 'plugins') wordlists_src = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'wordlists') wordlists_dst = os.path.join(config['data_dir'], 'wordlists') version_dir = os.path.join(config['data_dir'], f'VERSION-{VERSION}') if not os.path.exists(config['data_dir']): shutil.rmtree(config['data_dir'], ignore_errors=True, onerror=None) os.makedirs(config['data_dir'], exist_ok=True) open(os.path.join(config['data_dir'], 'VERSION-' + VERSION), 'a').close() shutil.copytree(plugins_src, plugins_dst) shutil.copytree(wordlists_src, wordlists_dst) else: develop = False # Copy plugins if develop mode or changes detected if develop or needs_update(plugins_src, plugins_dst): shutil.copytree(plugins_src, plugins_dst, dirs_exist_ok=True) # Copy wordlists if changes detected if needs_update(wordlists_src, wordlists_dst): shutil.copytree(wordlists_src, wordlists_dst, dirs_exist_ok=True) # Warn if version is outdated if not os.path.exists(version_dir): warn('It looks like the plugins in ' + config['data_dir'] + ' are outdated. Please remove the ' + config['data_dir'] + ' directory and re-run AutoRecon to rebuild them.') # Saves current terminal settings so we can restore them. terminal_settings = None 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) # sig and frame args are only present so the function # works with signal.signal() and handles Ctrl-C. # They are not used for any other purpose. def cancel_all_tasks(sig, frame): for task in asyncio.all_tasks(): task.cancel() processes = [] for target in autorecon.scanning_targets: for process_list in target.running_tasks.values(): for process_dict in process_list['processes']: try: parent = psutil.Process(process_dict['process'].pid) processes.extend(parent.children(recursive=True)) processes.append(parent) except psutil.NoSuchProcess: pass for process in processes: try: process.send_signal(signal.SIGKILL) except psutil.NoSuchProcess: # Will get raised if the process finishes before we get to killing it. pass _, alive = psutil.wait_procs(processes, timeout=10) if len(alive) > 0: error('The following process IDs could not be killed: ' + ', '.join([str(x.pid) for x in sorted(alive, key=lambda x: x.pid)])) if not config['disable_keyboard_control']: # Restore original terminal settings. if terminal_settings is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, terminal_settings) async def start_heartbeat(target, period=60): while True: await asyncio.sleep(period) async with target.lock: count = len(target.running_tasks) if config['verbose'] >= 1: tasks_list = [] for tag, task in target.running_tasks.items(): task_str = tag if config['verbose'] >= 2: processes = [] for process_dict in task['processes']: if process_dict['process'].returncode is None: processes.append(str(process_dict['process'].pid)) try: for child in psutil.Process(process_dict['process'].pid).children(recursive=True): processes.append(str(child.pid)) except psutil.NoSuchProcess: pass if processes: task_str += ' (PID' + ('s' if len(processes) > 1 else '') + ': ' + ', '.join(processes) + ')' tasks_list.append(task_str) tasks_list = ': {bblue}' + ', '.join(tasks_list) + '{rst}' else: tasks_list = '' 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 keyboard(): input = '' while True: if select.select([sys.stdin],[],[],0.1)[0]: input += sys.stdin.buffer.read1(-1).decode('utf8') while input != '': if len(input) >= 3: if input[:3] == '\x1b[A': input = '' if config['verbose'] == 3: info('Verbosity is already at the highest level.') else: config['verbose'] += 1 info('Verbosity increased to ' + str(config['verbose'])) elif input[:3] == '\x1b[B': input = '' if config['verbose'] == 0: info('Verbosity is already at the lowest level.') else: config['verbose'] -= 1 info('Verbosity decreased to ' + str(config['verbose'])) else: if input[0] != 's': input = input[1:] if len(input) > 0 and input[0] == 's': input = input[1:] for target in autorecon.scanning_targets: async with target.lock: count = len(target.running_tasks) tasks_list = [] if config['verbose'] >= 1: for tag, task in target.running_tasks.items(): elapsed_time = calculate_elapsed_time(task['start'], short=True) task_str = '{bblue}' + tag + '{rst}' + ' (elapsed: ' + elapsed_time + ')' if config['verbose'] >= 2: processes = [] for process_dict in task['processes']: if process_dict['process'].returncode is None: processes.append(str(process_dict['process'].pid)) try: for child in psutil.Process(process_dict['process'].pid).children(recursive=True): processes.append(str(child.pid)) except psutil.NoSuchProcess: pass if processes: task_str += ' (PID' + ('s' if len(processes) > 1 else '') + ': ' + ', '.join(processes) + ')' tasks_list.append(task_str) tasks_list = ':\n ' + '\n '.join(tasks_list) else: tasks_list = '' 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) else: 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']: target.ports = {'tcp':None, 'udp':None} if config['ports']['tcp']: target.ports['tcp'] = ','.join(config['ports']['tcp']) if config['ports']['udp']: target.ports['udp'] = ','.join(config['ports']['udp']) if plugin.specific_ports is False: warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} cannot be used to scan specific ports, and --ports was used. Skipping.', verbosity=2) return {'type':'port', 'plugin':plugin, 'result':[]} else: if plugin.type == 'tcp' and not config['ports']['tcp']: warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} is a TCP port scan but no TCP ports were set using --ports. Skipping', verbosity=2) return {'type':'port', 'plugin':plugin, 'result':[]} elif plugin.type == 'udp' and not config['ports']['udp']: warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} is a UDP port scan but no UDP ports were set using --ports. Skipping', verbosity=2) return {'type':'port', 'plugin':plugin, 'result':[]} async with target.autorecon.port_scan_semaphore: info('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}', verbosity=1) start_time = time.time() async with target.lock: target.running_tasks[plugin.slug] = {'plugin': plugin, 'processes': [], 'start': start_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 + ' {green}(' + 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 + ' {green}(' + plugin.slug + '){rst} against {byellow}' + 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('Port scan {bblue}' + plugin.name + ' {green}(' + 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.', verbosity=2) 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 + ' {green}(' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished in ' + elapsed_time, verbosity=2) os.system ('touch ' + os.path.join(target.scandir, '.port_scans', f".{plugin.slug}")) 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) plugin_pending = True while plugin_pending: global_plugin_count = 0 target_plugin_count = 0 if plugin.max_global_instances and plugin.max_global_instances > 0: async with service.target.autorecon.lock: # Count currently running plugin instances. for target in service.target.autorecon.scanning_targets: for task in target.running_tasks.values(): if plugin == task['plugin']: global_plugin_count += 1 if global_plugin_count >= plugin.max_global_instances: break if global_plugin_count >= plugin.max_global_instances: break if global_plugin_count >= plugin.max_global_instances: await asyncio.sleep(1) continue if plugin.max_target_instances and plugin.max_target_instances > 0: async with service.target.lock: # Count currently running plugin instances. for task in service.target.running_tasks.values(): if plugin == task['plugin']: target_plugin_count += 1 if target_plugin_count >= plugin.max_target_instances: break if target_plugin_count >= plugin.max_target_instances: await asyncio.sleep(1) continue # If we get here, we can run the plugin. plugin_pending = False 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 not config['no_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 and not (process_dict['cmd'].startswith('curl') and process_dict['process'].returncode == 22): 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) os.system ('touch ' + os.path.join(scandir, '.service_scans', f".{plugin.slug}")) return {'type':'service', 'plugin':plugin, 'result':result} async def generate_report(plugin, targets): semaphore = autorecon.service_scan_semaphore if not config['force_services'] and config.get('imported_nmap_services') is None: semaphore = await get_semaphore(autorecon) async with semaphore: try: result = await plugin.run(targets) 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: Report plugin {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) async def scan_target(target): os.makedirs(os.path.abspath(config['output']), exist_ok=True) if config['single_target']: basedir = os.path.abspath(config['output']) else: basedir = os.path.abspath(os.path.join(config['output'], target.address)) os.makedirs(basedir, exist_ok=True) target.basedir = basedir scandir = os.path.join(basedir, 'scans') target.scandir = scandir os.makedirs(scandir, exist_ok=True) os.makedirs(os.path.join(scandir, 'xml'), exist_ok=True) os.makedirs(os.path.join(scandir, '.port_scans'), exist_ok=True) if not config['only_scans_dir']: #exploitdir = os.path.join(basedir, 'exploit') #os.makedirs(exploitdir, exist_ok=True) lootdir = os.path.join(basedir, 'loot') os.makedirs(lootdir, exist_ok=True) reportdir = os.path.join(basedir, 'report') os.makedirs(reportdir, exist_ok=True) #open(os.path.join(reportdir, 'local.txt'), 'a').close() #open(os.path.join(reportdir, 'proof.txt'), 'a').close() #screenshotdir = os.path.join(reportdir, 'screenshots') #os.makedirs(screenshotdir, exist_ok=True) else: reportdir = scandir target.reportdir = reportdir pending = set() heartbeat = asyncio.create_task(start_heartbeat(target, period=config['heartbeat'])) services = [] if config['force_services']: forced_services = [x.strip().lower() for x in config['force_services']] for forced_service in forced_services: match = re.search(r'(?P(tcp|udp))/(?P\d+)/(?P[\w\-]+)(/(?Psecure|insecure))?', forced_service) if match: protocol = match.group('protocol') if config['proxychains'] and protocol == 'udp': error('The service ' + forced_service + ' uses UDP and --proxychains is enabled. Skipping.', verbosity=2) continue port = int(match.group('port')) service = match.group('service') secure = True if match.group('secure') == 'secure' else False service = Service(protocol, port, service, secure) service.target = target services.append(service) if services: pending.add(asyncio.create_task(asyncio.sleep(0))) else: error('No services were defined. Please check your service syntax: [tcp|udp]///[secure|insecure]') heartbeat.cancel() autorecon.errors = True return elif config.get('imported_nmap_services') is not None: imported_services_data = config['imported_nmap_services'].get(target.ip) or config['imported_nmap_services'].get(target.address) if imported_services_data: for (protocol, port_num, name, secure) in imported_services_data: if config['proxychains'] and protocol == 'udp': warn('Service ' + protocol + '/' + str(port_num) + '/' + name + ' uses UDP and --proxychains is enabled. Skipping.') continue svc = Service(protocol, port_num, name, secure) svc.target = target services.append(svc) if services: pending.add(asyncio.create_task(asyncio.sleep(0))) else: warn('No usable services for ' + target.address + ' in imported nmap data (all skipped).') heartbeat.cancel() return else: warn('Target ' + target.address + ' (' + target.ip + ') was not found in the imported nmap XML. Falling back to port scanning.') for plugin in target.autorecon.plugin_types['port']: if config['proxychains'] and plugin.type == 'udp': continue processed_marker = os.path.join(scandir, '.port_scans', f".{plugin.slug}") if os.path.exists(processed_marker): info(f"Port Plugin {plugin.name} ({plugin.slug}) has already been run against {target.address}. Skipping.") continue if config['port_scans'] and plugin.slug in config['port_scans']: matching_tags = True excluded_tags = False else: 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: target.scans['ports'][plugin.slug] = {'plugin': plugin, 'commands': []} pending.add(asyncio.create_task(port_scan(plugin, target))) else: for plugin in target.autorecon.plugin_types['port']: if config['proxychains'] and plugin.type == 'udp': continue processed_marker = os.path.join(scandir, '.port_scans', f".{plugin.slug}") # If the plugin has already been run against this target, skip it. if os.path.exists(processed_marker): info(f"Port Plugin {plugin.name} ({plugin.slug}) has already been run against {target.address}. Skipping.") continue if config['port_scans'] and plugin.slug in config['port_scans']: matching_tags = True excluded_tags = False else: 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: target.scans['ports'][plugin.slug] = {'plugin':plugin, 'commands':[]} pending.add(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 config['target_timeout'] is not None: elapsed_seconds = round(time.time() - start_time) m, s = divmod(elapsed_seconds, 60) if m >= config['target_timeout']: timed_out = True break if not config['force_services'] and config.get('imported_nmap_services') is None: # 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('Identified service {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}', verbosity=1) if not config['only_scans_dir']: with open(os.path.join(target.reportdir, 'notes.txt'), 'a') as file: file.writelines('[*] ' + service.name + ' found on ' + service.protocol + '/' + str(service.port) + '.\n\n\n\n') service.target = target # Create variables for command references. address = target.address addressv6 = target.address ipaddress = target.ip ipaddressv6 = target.ip scandir = target.scandir protocol = service.protocol port = service.port if not config['no_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) os.makedirs(os.path.join(scandir, '.service_scans'), exist_ok=True) # 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' if target.ipversion == 'IPv6': nmap_extra += ' -6' if addressv6 == target.ip: addressv6 = '[' + addressv6 + ']' ipaddressv6 = '[' + ipaddressv6 + ']' if config['proxychains'] and protocol == 'tcp': nmap_extra += ' -sT' service_match = False matching_plugins = [] heading = False for plugin in target.autorecon.plugin_types['service']: plugin_was_run = False plugin_service_match = False plugin_tag = service.tag() + '/' + plugin.slug processed_marker = os.path.join(scandir, '.service_scans', f".{plugin.slug}") # If the plugin has already been run against this service, skip it. if os.path.exists(processed_marker): info(f"Service Plugin {plugin.name} ({plugin.slug}) has already been run against {service.name} on {target.address}. Skipping.") continue for service_dict in plugin.services: if service_dict['protocol'] == protocol and port in service_dict['port']: for name in service_dict['name']: if service_dict['negative_match']: if name not in plugin.ignore_service_names: plugin.ignore_service_names.append(name) else: if name not in plugin.service_names: plugin.service_names.append(name) else: continue for s in plugin.service_names: if re.search(s, service.name): plugin_service_match = True if plugin_service_match: if config['service_scans'] and plugin.slug in config['service_scans']: matching_tags = True excluded_tags = False else: 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: plugin_queued = False for s in target.scans['services']: if plugin.slug in target.scans['services'][s]: plugin_queued = True warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin should only be run once and it appears to have already been queued. Skipping.{rst}', verbosity=2) break if plugin_queued: break # Skip plugin if require_ssl_boolean and port is not secure if plugin.require_ssl_boolean and not service.secure: plugin_service_match = False break # Skip plugin if service port is in ignore_ports: if port in plugin.ignore_ports[protocol]: plugin_service_match = False warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}', verbosity=2) break # 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]: plugin_service_match = False warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin can only run on specific ports. Skipping.{rst}', verbosity=2) break 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}', verbosity=2) break # TODO: check if plugin matches tags, BUT run manual commands anyway! plugin_was_run = True matching_plugins.append(plugin) for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod): if member_name == 'manual': try: plugin.manual(service, plugin_was_run) 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:]) cprint('Error: Service scan {bblue}' + plugin.name + ' {green}(' + plugin_tag + '){rst} running against {byellow}' + target.address + '{rst} produced an exception when generating manual commands:\n\n' + error_text, color=Fore.RED, char='!', printmsg=True) if service.manual_commands: plugin_run = False for s in target.scans['services']: if plugin.slug in target.scans['services'][s]: plugin_run = True break if not plugin.run_once_boolean or (plugin.run_once_boolean and not plugin_run): with open(os.path.join(target.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 service.manual_commands.items(): try: file.write('\t[-] ' + e(description) + '\n\n') for command in commands: file.write('\t\t' + e(command) + '\n\n') 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:]) cprint('Error: Service scan {bblue}' + plugin.name + ' {green}(' + plugin_tag + '){rst} running against {byellow}' + target.address + '{rst} produced an exception when evaluating manual commands:\n\n' + error_text, color=Fore.RED, char='!', printmsg=True) file.flush() service.manual_commands = {} break break if plugin_service_match: service_match = True for plugin in matching_plugins: plugin_tag = service.tag() + '/' + plugin.slug if plugin.run_once_boolean: plugin_tag = plugin.slug plugin_queued = False if service in target.scans['services']: for s in target.scans['services']: if plugin_tag in target.scans['services'][s]: plugin_queued = True 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}', verbosity=2) break if plugin_queued: continue else: if service not in target.scans['services']: target.scans['services'][service] = {} target.scans['services'][service][plugin_tag] = {'plugin':plugin, 'commands':[]} pending.add(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) if service.name not in config['service_exceptions'] and service.full_tag() not in target.autorecon.missing_services: target.autorecon.missing_services.append(service.full_tag()) for plugin in target.autorecon.plugin_types['report']: if config['reports'] and plugin.slug in config['reports']: matching_tags = True excluded_tags = False else: 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.add(asyncio.create_task(generate_report(plugin, [target]))) while pending: done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) 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']: try: process_dict['process'].kill() except ProcessLookupError: pass warn('{byellow}Scanning target ' + target.address + ' took longer than the specified target period (' + str(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.completed_targets.append(target) autorecon.scanning_targets.remove(target) async def run(): # Find config file. if os.path.isfile(os.path.join(config['config_dir'], 'config.toml')): config_file = os.path.join(config['config_dir'], 'config.toml') else: config_file = None # Find global file. if os.path.isfile(os.path.join(config['config_dir'], 'global.toml')): config['global_file'] = os.path.join(config['config_dir'], 'global.toml') else: config['global_file'] = None # Find plugins. if os.path.isdir(os.path.join(config['data_dir'], 'plugins')): config['plugins_dir'] = os.path.join(config['data_dir'], 'plugins') else: config['plugins_dir'] = None parser = argparse.ArgumentParser(add_help=False, allow_abbrev=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', '--target-file', action='store', type=str, default='', help='Read targets from file.') parser.add_argument('-p', '--ports', action='store', type=str, help='Comma separated list of ports / port ranges to scan. Specify TCP/UDP ports by prepending list with T:/U: To scan both TCP/UDP, put port(s) at start or specify B: e.g. 53,T:21-25,80,U:123,B:123. Default: %(default)s') parser.add_argument('-m', '--max-scans', action='store', type=int, help='The maximum number of concurrent scans to run. Default: %(default)s') 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=config_file, dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') parser.add_argument('-g', '--global-file', action='store', type=str, help='Location of AutoRecon\'s global file. Default: %(default)s') 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. Default: %(default)s') parser.add_argument('--exclude-tags', action='store', type=str, default='', metavar='TAGS', 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. Default: %(default)s') parser.add_argument('--port-scans', action='store', type=str, metavar='PLUGINS', help='Override --tags / --exclude-tags for the listed PortScan plugins (comma separated). Default: %(default)s') parser.add_argument('--service-scans', action='store', type=str, metavar='PLUGINS', help='Override --tags / --exclude-tags for the listed ServiceScan plugins (comma separated). Default: %(default)s') parser.add_argument('--reports', action='store', type=str, metavar='PLUGINS', help='Override --tags / --exclude-tags for the listed Report plugins (comma separated). Default: %(default)s') parser.add_argument('--plugins-dir', action='store', type=str, help='The location of the plugins directory. Default: %(default)s') parser.add_argument('--add-plugins-dir', action='store', type=str, metavar='PLUGINS_DIR', help='The location of an additional plugins directory to add to the main one. Default: %(default)s') parser.add_argument('-l', '--list', action='store', nargs='?', const='plugins', metavar='TYPE', help='List all plugins or plugins of a specific type. e.g. --list, --list port, --list service') parser.add_argument('-o', '--output', action='store', help='The output directory for results. Default: %(default)s') 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: %(default)s') 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: %(default)s') parser.add_argument('--no-port-dirs', action='store_true', help='Don\'t create directories for ports (e.g. scans/tcp80, scans/udp53). Instead store all results in the "scans" directory itself. Default: %(default)s') parser.add_argument('--heartbeat', action='store', type=int, help='Specifies the heartbeat interval (in seconds) for scan status messages. Default: %(default)s') parser.add_argument('--timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: %(default)s') 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: %(default)s') nmap_group = parser.add_mutually_exclusive_group() nmap_group.add_argument('--nmap', action='store', help='Override the {nmap_extra} variable in scans. Default: %(default)s') nmap_group.add_argument('--nmap-append', action='store', help='Append to the default {nmap_extra} variable in scans. Default: %(default)s') parser.add_argument('--proxychains', action='store_true', help='Use if you are running AutoRecon via proxychains. Default: %(default)s') parser.add_argument('--disable-sanity-checks', action='store_true', help='Disable sanity checks that would otherwise prevent the scans from running. Default: %(default)s') parser.add_argument('--disable-keyboard-control', action='store_true', help='Disables keyboard control ([s]tatus, Up, Down) if you are in SSH or Docker.') parser.add_argument('--ignore-plugin-checks', action='store_true', help='Ignores errors from plugin check functions that would otherwise prevent AutoRecon from running. Default: %(default)s') parser.add_argument('--force-services', action='store', nargs='+', metavar='SERVICE', help='A space separated list of services in the following style: tcp/80/http tcp/443/https/secure') parser.add_argument('--import-nmap', action='store', type=str, metavar='XML_FILE', help='Import hosts and open ports from an existing nmap XML file and run service scans without port scanning. Targets found in the XML are added automatically unless targets are specified explicitly.') parser.add_argument('-mpti', '--max-plugin-target-instances', action='store', nargs='+', metavar='PLUGIN:NUMBER', help='A space separated list of plugin slugs with the max number of instances (per target) in the following style: nmap-http:2 dirbuster:1. Default: %(default)s') parser.add_argument('-mpgi', '--max-plugin-global-instances', action='store', nargs='+', metavar='PLUGIN:NUMBER', help='A space separated list of plugin slugs with the max number of global instances in the following style: nmap-http:2 dirbuster:1. Default: %(default)s') parser.add_argument('--accessible', action='store_true', help='Attempts to make AutoRecon output more accessible to screenreaders. Default: %(default)s') parser.add_argument('-v', '--verbose', action='count', help='Enable verbose output. Repeat for more verbosity.') parser.add_argument('--version', action='store_true', help='Prints the AutoRecon version and exits.') parser.error = lambda s: fail(s[0].upper() + s[1:]) args, unknown = parser.parse_known_args() errors = False autorecon.argparse = parser if args.version: print('AutoRecon v' + VERSION) sys.exit(0) def unknown_help(): if '-h' in unknown: parser.print_help() print() # Parse config file and args for global.toml first. if not args.config_file: unknown_help() fail('Error: Could not find config.toml in the current directory or ~/.config/AutoRecon.') if not os.path.isfile(args.config_file): unknown_help() 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(): key = slugify(key) if key == 'global-file': config['global_file'] = val elif key == 'plugins-dir': config['plugins_dir'] = val elif key == 'add-plugins-dir': config['add_plugins_dir'] = val except toml.decoder.TomlDecodeError: unknown_help() fail('Error: Couldn\'t parse ' + args.config_file + ' config file. Check syntax.') args_dict = vars(args) for key in args_dict: key = slugify(key) if key == 'global-file' and args_dict['global_file'] is not None: config['global_file'] = args_dict['global_file'] elif key == 'plugins-dir' and args_dict['plugins_dir'] is not None: config['plugins_dir'] = args_dict['plugins_dir'] elif key == 'add-plugins-dir' and args_dict['add_plugins_dir'] is not None: config['add_plugins_dir'] = args_dict['add_plugins_dir'] if not config['plugins_dir']: unknown_help() fail('Error: Could not find plugins directory in the current directory or ~/.config/AutoRecon.') if not os.path.isdir(config['plugins_dir']): unknown_help() fail('Error: Specified plugins directory "' + config['plugins_dir'] + '" does not exist.') if config['add_plugins_dir'] and not os.path.isdir(config['add_plugins_dir']): unknown_help() fail('Error: Specified additional plugins directory "' + config['add_plugins_dir'] + '" does not exist.') plugins_dirs = [config['plugins_dir']] if config['add_plugins_dir']: plugins_dirs.append(config['add_plugins_dir']) for plugins_dir in plugins_dirs: for plugin_file in sorted(os.listdir(plugins_dir)): if not plugin_file.startswith('_') and plugin_file.endswith('.py'): dirname, filename = os.path.split(os.path.join(plugins_dir, plugin_file)) dirname = os.path.abspath(dirname) try: spec = importlib.util.spec_from_file_location("autorecon." + filename[:-3], os.path.join(dirname, filename)) plugin = importlib.util.module_from_spec(spec) spec.loader.exec_module(plugin) clsmembers = inspect.getmembers(plugin, predicate=inspect.isclass) for (_, c) in clsmembers: if c.__module__ in ['autorecon.plugins', 'autorecon.targets']: continue if c.__name__.lower() in config['protected_classes']: unknown_help() 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, ServiceScan, or Report if issubclass(c, PortScan) or issubclass(c, ServiceScan) or issubclass(c, Report): autorecon.register(c(), filename) else: print('Plugin "' + c.__name__ + '" in ' + filename + ' is not a subclass of either PortScan, ServiceScan, or Report.') except (ImportError, SyntaxError) as ex: unknown_help() print('cannot import ' + filename + ' plugin') print(ex) sys.exit(1) for plugin in autorecon.plugins.values(): if plugin.slug in autorecon.taglist: unknown_help() fail('Plugin ' + plugin.name + ' has a slug (' + plugin.slug + ') with the same name as a tag. Please either change the plugin name or override the slug.') # Add plugin slug to tags. plugin.tags += [plugin.slug] if len(autorecon.plugin_types['port']) == 0: unknown_help() fail('Error: There are no valid PortScan plugins in the plugins directory "' + config['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) autorecon.plugin_types['report'].sort(key=lambda x: x.priority) if not config['global_file']: unknown_help() fail('Error: Could not find global.toml in the current directory or ~/.config/AutoRecon.') if not os.path.isfile(config['global_file']): unknown_help() fail('Error: Specified global file "' + config['global_file'] + '" does not exist.') global_plugin_args = None with open(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['nargs'] = '+' 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) 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: unknown_help() fail('Error: The pattern "' + pattern['pattern'] + '" in the global file is invalid regex.') else: unknown_help() fail('Error: A [[pattern]] in the global file doesn\'t have a required pattern variable.') except toml.decoder.TomlDecodeError: unknown_help() fail('Error: Couldn\'t parse ' + g.name + ' file. Check syntax.') other_options = [] 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 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('-', '_')) is not None: for action in autorecon.argparse._actions: if action.dest == slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_'): if action.const and pval != action.const: if action.const in [True, False]: error('Config option [' + slugify(key) + '] ' + slugify(pkey) + ': invalid value: \'' + pval + '\' (should be ' + str(action.const).lower() + ' {no quotes})') else: error('Config option [' + slugify(key) + '] ' + slugify(pkey) + ': invalid value: \'' + pval + '\' (should be ' + str(action.const) + ')') errors = True elif action.choices and pval not in action.choices: error('Config option [' + slugify(key) + '] ' + slugify(pkey) + ': invalid choice: \'' + pval + '\' (choose from \'' + '\', \''.join(action.choices) + '\')') errors = True elif isinstance(action.default, list) and not isinstance(pval, list): error('Config option [' + slugify(key) + '] ' + slugify(pkey) + ': invalid value: \'' + pval + '\' (should be a list e.g. [\'' + pval + '\'])') errors = True break autorecon.argparse.set_defaults(**{slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_'): pval}) else: # Process potential other options. key = key.replace('-', '_') if key in configurable_keys: other_options.append(key) config[key] = val autorecon.argparse.set_defaults(**{key: val}) for key, val in config.items(): if key not in other_options: autorecon.argparse.set_defaults(**{key: val}) parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, help='Show this help message and exit.') parser.error = lambda s: fail(s[0].upper() + s[1:]) args = parser.parse_args() args_dict = vars(args) for key in args_dict: if key in configurable_keys and args_dict[key] is not None: # Special case for booleans if key in configurable_boolean_keys and config[key]: continue config[key] = args_dict[key] autorecon.args = args if args.list: type = args.list.lower() if type in ['plugin', 'plugins', 'port', 'ports', 'portscan', 'portscans']: for p in autorecon.plugin_types['port']: print('PortScan: ' + p.name + ' (' + p.slug + ')' + (' - ' + p.description if p.description else '')) if type in ['plugin', 'plugins', 'service', 'services', 'servicescan', 'servicescans']: for p in autorecon.plugin_types['service']: print('ServiceScan: ' + p.name + ' (' + p.slug + ')' + (' - ' + p.description if p.description else '')) if type in ['plugin', 'plugins', 'report', 'reports', 'reporting']: for p in autorecon.plugin_types['report']: print('Report: ' + p.name + ' (' + p.slug + ')' + (' - ' + p.description if p.description else '')) sys.exit(0) max_plugin_target_instances = {} if config['max_plugin_target_instances']: for plugin_instance in config['max_plugin_target_instances']: plugin_instance = plugin_instance.split(':', 1) if len(plugin_instance) == 2: if plugin_instance[0] not in autorecon.plugins: error('Invalid plugin slug (' + plugin_instance[0] + ':' + plugin_instance[1] + ') provided to --max-plugin-target-instances.') errors = True elif not plugin_instance[1].isdigit() or int(plugin_instance[1]) == 0: error('Invalid number of instances (' + plugin_instance[0] + ':' + plugin_instance[1] + ') provided to --max-plugin-target-instances. Must be a non-zero positive integer.') errors = True else: max_plugin_target_instances[plugin_instance[0]] = int(plugin_instance[1]) else: error('Invalid value provided to --max-plugin-target-instances. Values must be in the format PLUGIN:NUMBER.') max_plugin_global_instances = {} if config['max_plugin_global_instances']: for plugin_instance in config['max_plugin_global_instances']: plugin_instance = plugin_instance.split(':', 1) if len(plugin_instance) == 2: if plugin_instance[0] not in autorecon.plugins: error('Invalid plugin slug (' + plugin_instance[0] + ':' + plugin_instance[1] + ') provided to --max-plugin-global-instances.') errors = True elif not plugin_instance[1].isdigit() or int(plugin_instance[1]) == 0: error('Invalid number of instances (' + plugin_instance[0] + ':' + plugin_instance[1] + ') provided to --max-plugin-global-instances. Must be a non-zero positive integer.') errors = True else: max_plugin_global_instances[plugin_instance[0]] = int(plugin_instance[1]) else: error('Invalid value provided to --max-plugin-global-instances. Values must be in the format PLUGIN:NUMBER.') failed_check_plugin_slugs = [] for slug, plugin in autorecon.plugins.items(): if hasattr(plugin, 'max_target_instances') and plugin.slug in max_plugin_target_instances: plugin.max_target_instances = max_plugin_target_instances[plugin.slug] if hasattr(plugin, 'max_global_instances') and plugin.slug in max_plugin_global_instances: plugin.max_global_instances = max_plugin_global_instances[plugin.slug] for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod): if member_name == 'check': if plugin.check() == False: failed_check_plugin_slugs.append(slug) continue continue # Check for any failed plugin checks. for slug in failed_check_plugin_slugs: # If plugin checks should be ignored, remove the affected plugins at runtime. if config['ignore_plugin_checks']: autorecon.plugins.pop(slug) else: print() error('The following plugins failed checks that prevent AutoRecon from running: ' + ', '.join(failed_check_plugin_slugs)) error('Check above output to fix these issues, disable relevant plugins, or run AutoRecon with --ignore-plugin-checks to disable failed plugins at runtime.') print() errors = True break if config['ports']: ports_to_scan = {'tcp':[], 'udp':[]} unique = {'tcp':[], 'udp':[]} ports = config['ports'].split(',') mode = 'both' for port in ports: port = port.strip() if port == '': continue if port.startswith('B:'): mode = 'both' port = port.split('B:')[1] elif port.startswith('T:'): mode = 'tcp' port = port.split('T:')[1] elif port.startswith('U:'): mode = 'udp' port = port.split('U:')[1] match = re.search(r'^([0-9]+)-([0-9]+)$', port) if match: num1 = int(match.group(1)) num2 = int(match.group(2)) if num1 > 65535: fail('Error: A provided port number was too high: ' + str(num1)) if num2 > 65535: fail('Error: A provided port number was too high: ' + str(num2)) if num1 == num2: port_range = [num1] if num2 > num1: port_range = list(range(num1, num2 + 1, 1)) else: port_range = list(range(num2, num1 + 1, 1)) num1 = num1 + num2 num2 = num1 - num2 num1 = num1 - num2 if mode == 'tcp' or mode == 'both': for num in port_range: if num in ports_to_scan['tcp']: ports_to_scan['tcp'].remove(num) ports_to_scan['tcp'].append(str(num1) + '-' + str(num2)) unique['tcp'] = list(set(unique['tcp'] + port_range)) if mode == 'udp' or mode == 'both': for num in port_range: if num in ports_to_scan['udp']: ports_to_scan['udp'].remove(num) ports_to_scan['udp'].append(str(num1) + '-' + str(num2)) unique['udp'] = list(set(unique['tcp'] + port_range)) else: match = re.search('^[0-9]+$', port) if match: num = int(port) if num > 65535: fail('Error: A provided port number was too high: ' + str(num)) if mode == 'tcp' or mode == 'both': ports_to_scan['tcp'].append(str(num)) if num not in unique['tcp'] else ports_to_scan['tcp'] unique['tcp'].append(num) if mode == 'udp' or mode == 'both': ports_to_scan['udp'].append(str(num)) if num not in unique['udp'] else ports_to_scan['udp'] unique['udp'].append(num) else: fail('Error: Invalid port number: ' + str(port)) config['ports'] = ports_to_scan if config['max_scans'] <= 0: error('Argument -m/--max-scans must be at least 1.') errors = True if config['max_port_scans'] is None: config['max_port_scans'] = max(1, round(config['max_scans'] * 0.2)) else: if config['max_port_scans'] <= 0: error('Argument -mp/--max-port-scans must be at least 1.') errors = True if config['max_port_scans'] > config['max_scans']: error('Argument -mp/--max-port-scans cannot be greater than argument -m/--max-scans.') errors = True if config['heartbeat'] <= 0: error('Argument --heartbeat must be at least 1.') errors = True if config['timeout'] is not None and config['timeout'] <= 0: error('Argument --timeout must be at least 1.') errors = True if config['target_timeout'] is not None and config['target_timeout'] <= 0: error('Argument --target-timeout must be at least 1.') errors = True if config['timeout'] is not None and config['target_timeout'] is not None and config['timeout'] < config['target_timeout']: error('Argument --timeout cannot be less than --target-timeout.') errors = True if not errors: if config['force_services'] or config.get('imported_nmap_services') is not None: autorecon.service_scan_semaphore = asyncio.Semaphore(config['max_scans']) else: autorecon.port_scan_semaphore = asyncio.Semaphore(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 config['max_scans'] == config['max_port_scans']: autorecon.service_scan_semaphore = autorecon.port_scan_semaphore else: autorecon.service_scan_semaphore = asyncio.Semaphore(config['max_scans'] - 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 is None: args.exclude_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] if config['port_scans']: config['port_scans'] = [x.strip().lower() for x in config['port_scans'].split(',')] if config['service_scans']: config['service_scans'] = [x.strip().lower() for x in config['service_scans'].split(',')] if config['reports']: config['reports'] = [x.strip().lower() for x in config['reports'].split(',')] 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('#'): continue match = re.match('([^#]+)#', line) if match: line = match.group(1).strip() if 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) if args.import_nmap: if not os.path.isfile(args.import_nmap): error('The nmap XML file "' + args.import_nmap + '" was not found.') sys.exit(1) imported = parse_nmap_xml(args.import_nmap) if not imported: error('No hosts with open ports found in nmap XML file "' + args.import_nmap + '".') sys.exit(1) config['imported_nmap_services'] = imported info('Imported ' + str(len(imported)) + ' host(s) from "' + args.import_nmap + '".') # When no explicit targets given, use all hosts from the XML. if not raw_targets: for address in imported: raw_targets.append(address) unresolvable_targets = False for target in raw_targets: try: ip = ipaddress.ip_address(target) ip_str = str(ip) found = False for t in autorecon.pending_targets: if t.address == ip_str: found = True break if found: continue if isinstance(ip, ipaddress.IPv4Address): autorecon.pending_targets.append(Target(ip_str, ip_str, 'IPv4', 'ip', autorecon)) elif isinstance(ip, ipaddress.IPv6Address): autorecon.pending_targets.append(Target(ip_str, ip_str, 'IPv6', 'ip', autorecon)) else: fail('This should never happen unless IPv8 is invented.') except ValueError: try: target_range = ipaddress.ip_network(target, strict=False) if not args.disable_sanity_checks and target_range.num_addresses > 256: fail(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 = str(ip) found = False for t in autorecon.pending_targets: if t.address == ip_str: found = True break if found: continue if isinstance(ip, ipaddress.IPv4Address): autorecon.pending_targets.append(Target(ip_str, ip_str, 'IPv4', 'ip', autorecon)) elif isinstance(ip, ipaddress.IPv6Address): autorecon.pending_targets.append(Target(ip_str, ip_str, 'IPv6', 'ip', autorecon)) else: fail('This should never happen unless IPv8 is invented.') except ValueError: try: addresses = socket.getaddrinfo(target, None, socket.AF_INET) ip = addresses[0][4][0] found = False for t in autorecon.pending_targets: if t.address == target: found = True break if found: continue autorecon.pending_targets.append(Target(target, ip, 'IPv4', 'hostname', autorecon)) except socket.gaierror: try: addresses = socket.getaddrinfo(target, None, socket.AF_INET6) ip = addresses[0][4][0] found = False for t in autorecon.pending_targets: if t.address == target: found = True break if found: continue autorecon.pending_targets.append(Target(target, ip, 'IPv6', 'hostname', autorecon)) except socket.gaierror: unresolvable_targets = True error(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.') if not args.disable_sanity_checks and unresolvable_targets == True: error('AutoRecon will not run if any targets are invalid / unresolvable. To override this, re-run with the --disable-sanity-checks option.') errors = True if len(autorecon.pending_targets) == 0: error('You must specify at least one target to scan!') errors = True if 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 if not config['force_services'] and config.get('imported_nmap_services') is None: port_scan_plugin_count = 0 for plugin in autorecon.plugin_types['port']: if config['port_scans'] and plugin.slug in config['port_scans']: matching_tags = True excluded_tags = False else: 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 else: port_scan_plugin_count = config['max_port_scans'] / 5 if errors: sys.exit(1) config['port_scan_plugin_count'] = port_scan_plugin_count num_initial_targets = max(1, math.ceil(config['max_port_scans'] / port_scan_plugin_count)) start_time = time.time() if not config['disable_keyboard_control']: terminal_settings = termios.tcgetattr(sys.stdin.fileno()) pending = set() i = 0 while autorecon.pending_targets: pending.add(asyncio.create_task(scan_target(autorecon.pending_targets.pop(0)))) i+=1 if i >= num_initial_targets: break if not config['disable_keyboard_control']: tty.setcbreak(sys.stdin.fileno()) keyboard_monitor = asyncio.create_task(keyboard()) timed_out = False while pending: done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) # If something failed in scan_target, autorecon.errors will be true. if autorecon.errors: cancel_all_tasks(None, None) sys.exit(1) # Check if global timeout has occurred. if config['timeout'] is not None: elapsed_seconds = round(time.time() - start_time) m, s = divmod(elapsed_seconds, 60) if m >= config['timeout']: timed_out = True break for task in done: if autorecon.pending_targets: pending.add(asyncio.create_task(scan_target(autorecon.pending_targets.pop(0)))) if task in pending: pending.remove(task) port_scan_task_count = 0 for targ in autorecon.scanning_targets: for process_list in targ.running_tasks.values(): # If we're not scanning ports, count ServiceScans instead. if config['force_services'] or config.get('imported_nmap_services') is not None: if issubclass(process_list['plugin'].__class__, ServiceScan): # TODO should we really count ServiceScans? Test... port_scan_task_count += 1 else: if issubclass(process_list['plugin'].__class__, PortScan): port_scan_task_count += 1 num_new_targets = math.ceil((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(autorecon.pending_targets.pop(0)))) i+=1 if i >= num_new_targets: break if not config['disable_keyboard_control']: keyboard_monitor.cancel() # If there's only one target we don't need a combined report if len(autorecon.completed_targets) > 1: for plugin in autorecon.plugin_types['report']: if config['reports'] and plugin.slug in config['reports']: matching_tags = True excluded_tags = False else: plugin_tag_set = set(plugin.tags) matching_tags = False for tag_group in autorecon.tags: if set(tag_group).issubset(plugin_tag_set): matching_tags = True break excluded_tags = False for tag_group in autorecon.excluded_tags: if set(tag_group).issubset(plugin_tag_set): excluded_tags = True break if matching_tags and not excluded_tags: pending.add(asyncio.create_task(generate_report(plugin, autorecon.completed_targets))) while pending: done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) 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(config['timeout']) + ' min). Cancelling all scans and exiting.{rst}') 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}') info('{bright}Don\'t forget to check out more commands to run manually in the _manual_commands.txt file in each target\'s scans directory!') if autorecon.missing_services: warn('{byellow}AutoRecon identified the following services, but could not match them to any plugins based on the service name. Please report these to Tib3rius: ' + ', '.join(autorecon.missing_services) + '{rst}') if not config['disable_keyboard_control']: # Restore original terminal settings. if terminal_settings is not None: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) def main(): # Capture Ctrl+C and cancel everything. signal.signal(signal.SIGINT, cancel_all_tasks) try: asyncio.run(run()) except asyncio.exceptions.CancelledError: pass except RuntimeError: pass if __name__ == '__main__': main()