#!/usr/bin/env python3 # # AutoReconR attempts to automate parts of the network reconnaissance process and creates a respective findings report. # # This program can be redistributed and/or modified under the terms of the # GNU General Public License, either version 3 of the License, or (at your # option) any later version. # import argparse import asyncio from colorama import Fore, Style from concurrent.futures import ProcessPoolExecutor, as_completed, FIRST_COMPLETED import ipaddress import os import re import socket import string from datetime import datetime import sys import toml import glob import pprint __version__ = '0.0.1' verbose = 0 nmap_default_options = '--reason -Pn' srvname = '' # number of possible complexity levels for scanners max_level = 3 port_scan_profile = None port_scan_profiles_config = None service_scans_config = None port_scan_profiles_config = {} service_scans_config = {} global_patterns = [] program_paths = {} # defines how sections in configuration files need to be labelled in order to be considered markup = { 'port_scan_profile' : 'port-scan', 'service_scan_profile' : 'service-scan', 'pattern_profile' : 'pattern', } files = { 'config' : 'config.toml', 'commands' : '_commands.log', 'manual_commands' : '_manual_commands.log', 'errors' : '_errors.log', 'notes' : '_notes.txt', 'patterns' : '_patterns.txt', 'report' : 'report.pdf', } username_wordlist = '/usr/share/seclists/Usernames/top-usernames-shortlist.txt' password_wordlist = '/usr/share/seclists/Passwords/darkweb2017-top100.txt' rootdir = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) def e(*args, frame_index=1, **kvargs): frame = sys._getframe(frame_index) vals = {} vals.update(frame.f_globals) vals.update(frame.f_locals) vals.update(kvargs) return string.Formatter().vformat(' '.join(args), args, vals) def cprint(*args, color=Fore.RESET, char='*', sep=' ', end='\n', frame_index=1, file=sys.stdout, **kvargs): frame = sys._getframe(frame_index) vals = { 'bgreen': Fore.GREEN + Style.BRIGHT, 'bred': Fore.RED + Style.BRIGHT, 'bblue': Fore.BLUE + Style.BRIGHT, 'byellow': Fore.YELLOW + Style.BRIGHT, 'bmagenta': Fore.MAGENTA + Style.BRIGHT, 'green': Fore.GREEN, 'red': Fore.RED, 'blue': Fore.BLUE, 'yellow': Fore.YELLOW, 'magenta': Fore.MAGENTA, 'bright': Style.BRIGHT, 'srst': Style.NORMAL, 'crst': Fore.RESET, 'rst': Style.NORMAL + Fore.RESET } vals.update(frame.f_globals) vals.update(frame.f_locals) vals.update(kvargs) clock = datetime.now().strftime('%H:%M:%S') clock = sep + '[' + Style.BRIGHT + Fore.YELLOW + clock + Style.NORMAL + Fore.RESET + ']' unfmt = '' if char is not None: unfmt += color + '[' + Style.BRIGHT + char + Style.NORMAL + ']' + Fore.RESET + clock + sep unfmt += sep.join(args) fmted = unfmt for attempt in range(10): try: fmted = string.Formatter().vformat(unfmt, args, vals) break except KeyError as err: key = err.args[0] unfmt = unfmt.replace('{' + key + '}', '{{' + key + '}}') print(fmted, sep=sep, end=end, file=file) def debug(*args, color=Fore.BLUE, sep=' ', end='\n', file=sys.stdout, **kvargs): if verbose >= 2: cprint(*args, color=color, char='-', sep=sep, end=end, file=file, frame_index=2, **kvargs) def info(*args, sep=' ', end='\n', file=sys.stdout, **kvargs): cprint(*args, color=Fore.GREEN, char='*', sep=sep, end=end, file=file, frame_index=2, **kvargs) def warn(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): cprint(*args, color=Fore.YELLOW, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) def error(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) def fail(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) sys.exit(-1) # TODO: can be deleted later when 'read_configuration' handles all configuration files def read_configuration_file(filename, replace_values = {}): data = {} try: with open(os.path.join(rootdir, 'config', filename), 'r') as f: data = f.read() for entry in replace_values: data = re.sub('{' +entry + '}', replace_values[entry], data) data = toml.loads(data) except (OSError, toml.decoder.TomlDecodeError) as e: fail('Error: The configuration file {filename} could not be read.') return data def read_configuration(filename): data = {} port_scan_profiles = {} service_scan_profiles = {} patterns = [] try: with open(filename, 'r') as f: data = f.read() # TODO: insert program path at a later point before the program is executed for entry in program_paths: data = re.sub('{' +entry + '}', program_paths[entry], data) data = toml.loads(data) for profile in data: if profile.lower() == markup['port_scan_profile']: port_scan_profiles = data[profile] elif profile.lower() == markup['service_scan_profile']: service_scan_profiles = data[profile] elif profile.lower() == markup['pattern_profile']: patterns = data[profile] else: warn('Warning: Unknown markup {profile} discovered in configuration file {filename}.') except (OSError, toml.decoder.TomlDecodeError) as e: fail('Error: The configuration file {filename} could not be read.') return port_scan_profiles, service_scan_profiles, patterns def get_configuration(): applications_config = read_configuration_file('config.toml') if len(applications_config) > 0 and 'applications' in applications_config: global program_paths program_paths = applications_config['applications'] # check whether applications exist for application in program_paths: if not os.path.isfile(program_paths[application]): warn('Warning: The application {application} was not found on the system in the specified path.') else: warn('Warning: The section for application paths was not found in the {application_config_file} configuration file.') filenames = glob.glob(os.path.join(rootdir, 'config', '**', '*.toml'), recursive = True) for filename in filenames: if os.path.basename(filename) == files['config']: continue port_scan_profiles, service_scan_profiles, patterns = read_configuration(filename) global port_scan_profiles_config port_scan_profiles_config = {**port_scan_profiles_config, **port_scan_profiles} global service_scans_config service_scans_config = {**service_scans_config, **service_scan_profiles} global global_patterns global_patterns += patterns if len(port_scan_profiles_config) == 0: fail('There do not appear to be any port scan profiles configured in the {port_scan_profiles_config_file} config file.') return False if 'username_wordlist' in service_scans_config: if isinstance(service_scans_config['username_wordlist'], str): username_wordlist = service_scans_config['username_wordlist'] if 'password_wordlist' in service_scans_config: if isinstance(service_scans_config['password_wordlist'], str): password_wordlist = service_scans_config['password_wordlist'] return True async def read_stream(stream, target, tag='?', patterns=[], color=Fore.BLUE): address = target.address while True: line = await stream.readline() if line: line = str(line.rstrip(), 'utf8', 'ignore') debug(color + '[' + Style.BRIGHT + address + ' ' + tag + Style.NORMAL + '] ' + Fore.RESET + '{line}', color=color) for p in global_patterns: matches = re.findall(p['pattern'], line) if 'description' in p: for match in matches: if verbose >= 1: info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}' + p['description'].replace('{match}', '{bblue}{match}{crst}{bmagenta}') + '{rst}') async with target.lock: with open(os.path.join(target.scandir, files['patterns']), 'a') as file: file.writelines(e('{tag} - ' + p['description'] + '\n\n')) else: for match in matches: if verbose >= 1: info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}Matched Pattern: {bblue}{match}{rst}') async with target.lock: with open(os.path.join(target.scandir, files['patterns']), 'a') as file: file.writelines(e('{tag} - Matched Pattern: {match}\n\n')) for p in patterns: matches = re.findall(p['pattern'], line) if 'description' in p: for match in matches: if verbose >= 1: info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}' + p['description'].replace('{match}', '{bblue}{match}{crst}{bmagenta}') + '{rst}') async with target.lock: with open(os.path.join(target.scandir, files['patterns']), 'a') as file: file.writelines(e('{tag} - ' + p['description'] + '\n\n')) else: for match in matches: if verbose >= 1: info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}Matched Pattern: {bblue}{match}{rst}') async with target.lock: with open(os.path.join(target.scandir, files['patterns']), 'a') as file: file.writelines(e('{tag} - Matched Pattern: {match}\n\n')) else: break async def run_cmd(semaphore, cmd, target, category='?', tag='?', patterns=[]): async with semaphore: address = target.address scandir = target.scandir if len(category) == 0: category = 'all' category = category.strip('/') info('Running task {bgreen}{tag}{rst} on {byellow}{address}{rst}' + (' with {bblue}{cmd}{rst}.' if verbose >= 1 else '.')) async with target.lock: with open(os.path.join(scandir, files['commands']), 'a') as file: file.writelines(e('{category} - {cmd}\n\n')) # skip extended service scanning if only respective commands should be documented if args.skip_service_scan: return {'returncode': 0, 'name': 'run_cmd'} process = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, executable='/bin/bash') await asyncio.wait([ read_stream(process.stdout, target, tag=tag, patterns=patterns), read_stream(process.stderr, target, tag=tag, patterns=patterns, color=Fore.RED) ]) await process.wait() if process.returncode != 0: error('Task {bred}{tag}{rst} on {byellow}{address}{rst} returned non-zero exit code: {process.returncode}.') async with target.lock: with open(os.path.join(scandir, files['errors']), 'a') as file: file.writelines(e('[*] Task {tag} returned non-zero exit code: {process.returncode}. Command: {cmd}\n')) else: info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} finished successfully.') return {'returncode': process.returncode, 'name': 'run_cmd'} async def parse_port_scan(stream, tag, target, pattern): address = target.address ports = [] while True: line = await stream.readline() if line: line = str(line.rstrip(), 'utf8', 'ignore') debug(Fore.BLUE + '[' + Style.BRIGHT + address + ' ' + tag + Style.NORMAL + '] ' + Fore.RESET + '{line}', color=Fore.BLUE) parse_match = re.search(pattern, line) if parse_match: ports.append(parse_match.group('port')) for p in global_patterns: matches = re.findall(p['pattern'], line) if 'description' in p: for match in matches: if verbose >= 1: info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}' + p['description'].replace('{match}', '{bblue}{match}{crst}{bmagenta}') + '{rst}') async with target.lock: with open(os.path.join(target.scandir, files['patterns']), 'a') as file: file.writelines(e('{tag} - ' + p['description'] + '\n\n')) else: for match in matches: if verbose >= 1: info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}Matched Pattern: {bblue}{match}{rst}') async with target.lock: with open(os.path.join(target.scandir, files['patterns']), 'a') as file: file.writelines(e('{tag} - Matched Pattern: {match}\n\n')) else: break return ports async def parse_service_detection(stream, tag, target, pattern): address = target.address services = [] while True: line = await stream.readline() if line: line = str(line.rstrip(), 'utf8', 'ignore') debug(Fore.BLUE + '[' + Style.BRIGHT + address + ' ' + tag + Style.NORMAL + '] ' + Fore.RESET + '{line}', color=Fore.BLUE) parse_match = re.search(pattern, line) if parse_match: services.append((parse_match.group('protocol').lower(), int(parse_match.group('port')), parse_match.group('service'))) for p in global_patterns: matches = re.findall(p['pattern'], line) if 'description' in p: for match in matches: if verbose >= 1: info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}' + p['description'].replace('{match}', '{bblue}{match}{crst}{bmagenta}') + '{rst}') async with target.lock: with open(os.path.join(target.scandir, files['patterns']), 'a') as file: file.writelines(e('{tag} - ' + p['description'] + '\n\n')) else: for match in matches: if verbose >= 1: info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}Matched Pattern: {bblue}{match}{rst}') async with target.lock: with open(os.path.join(target.scandir, files['patterns']), 'a') as file: file.writelines(e('{tag} - Matched Pattern: {match}\n\n')) else: break return services async def run_portscan(semaphore, tag, target, service_detection, port_scan=None): async with semaphore: address = target.address scandir = target.scandir nmap_extra = nmap_default_options ports = '' if port_scan is not None: command = e(port_scan[0]) pattern = port_scan[1] info('Running port scan {bgreen}{tag}{rst} on {byellow}{address}{rst}' + (' with {bblue}{command}{rst}.' if verbose >= 1 else '.')) async with target.lock: with open(os.path.join(scandir, files['commands']), 'a') as file: file.writelines(e('{command}\n\n')) process = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, executable='/bin/bash') output = [ parse_port_scan(process.stdout, tag, target, pattern), read_stream(process.stderr, target, tag=tag, color=Fore.RED) ] results = await asyncio.gather(*output) await process.wait() if process.returncode != 0: error('Port scan {bred}{tag}{rst} on {byellow}{address}{rst} returned non-zero exit code: {process.returncode}') async with target.lock: with open(os.path.join(scandir, files['errors']), 'a') as file: file.writelines(e('[*] Port scan {tag} returned non-zero exit code: {process.returncode}. Command: {command}\n')) return {'returncode': process.returncode} else: info('Port scan {bgreen}{tag}{rst} on {byellow}{address}{rst} finished successfully') ports = results[0] if len(ports) == 0: return {'returncode': -1} ports = ','.join(ports) command = e(service_detection[0]) pattern = service_detection[1] info('Running service detection {bgreen}{tag}{rst} on {byellow}{address}{rst}' + (' with {bblue}{command}{rst}.' if verbose >= 1 else '.')) async with target.lock: with open(os.path.join(scandir, files['commands']), 'a') as file: file.writelines(e('{command}\n\n')) process = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, executable='/bin/bash') output = [ parse_service_detection(process.stdout, tag, target, pattern), read_stream(process.stderr, target, tag=tag, color=Fore.RED) ] results = await asyncio.gather(*output) await process.wait() if process.returncode != 0: error('Service detection {bred}{tag}{rst} on {byellow}{address}{rst} returned non-zero exit code: {process.returncode}') async with target.lock: with open(os.path.join(scandir, files['errors']), 'a') as file: file.writelines(e('[*] Service detection {tag} returned non-zero exit code: {process.returncode}. Command: {command}\n')) else: info('Service detection {bgreen}{tag}{rst} on {byellow}{address}{rst} finished successfully.') services = results[0] return {'returncode': process.returncode, 'name': 'run_portscan', 'services': services} async def scan_services(loop, semaphore, target): address = target.address scandir = target.scandir pending = [] for profile in port_scan_profiles_config: if profile == port_scan_profile: for scan in port_scan_profiles_config[profile]: service_detection = (port_scan_profiles_config[profile][scan]['service-detection']['command'], port_scan_profiles_config[profile][scan]['service-detection']['pattern']) if 'port-scan' in port_scan_profiles_config[profile][scan]: port_scan = (port_scan_profiles_config[profile][scan]['port-scan']['command'], port_scan_profiles_config[profile][scan]['port-scan']['pattern']) pending.append(run_portscan(semaphore, scan, target, service_detection, port_scan)) else: pending.append(run_portscan(semaphore, scan, target, service_detection)) break services = [] while True: if not pending: break done, pending = await asyncio.wait(pending, return_when=FIRST_COMPLETED) for task in done: result = task.result() if result['returncode'] == 0: if result['name'] == 'run_portscan': for service_tuple in result['services']: if service_tuple not in services: services.append(service_tuple) else: continue protocol = service_tuple[0] port = service_tuple[1] service = service_tuple[2] info('Port {bmagenta}{protocol} {port}{rst} ({bmagenta}{service}{rst}) open on target {byellow}{address}{rst}.') with open(os.path.join(target.scandir, files['notes']), 'a') as file: file.writelines(e('[*] Port {protocol} {port} ({service}) open on {address}.\n\n')) if protocol == 'udp': nmap_extra = nmap_default_options + " -sU" else: nmap_extra = nmap_default_options secure = True if 'ssl' in service or 'tls' in service else False # Special cases for HTTP. scheme = 'https' if 'https' in service or 'ssl' in service or 'tls' in service else 'http' if service.startswith('ssl/') or service.startswith('tls/'): service = service[4:] for service_scan in service_scans_config: # Skip over configurable variables since the python toml parser cannot iterate over tables only. if service_scan in ['username_wordlist', 'password_wordlist']: continue ignore_service = False if 'ignore-service-names' in service_scans_config[service_scan]: for ignore_service_name in service_scans_config[service_scan]['ignore-service-names']: if re.search(ignore_service_name, service): ignore_service = True break if ignore_service: continue matched_service = False if 'service-names' in service_scans_config[service_scan]: for service_name in service_scans_config[service_scan]['service-names']: if re.search(service_name, service): matched_service = True break if not matched_service: continue # INFO: change for saving results in directories per service if not service_scan == 'all-services': category = '{0}/'.format(service_scan) else: category = '' try: servicedir = os.path.join(scandir, category) if not os.path.exists(servicedir): os.mkdir(servicedir) xmldir = os.path.join(scandir, 'xml', category) if not os.path.exists(xmldir): os.mkdir(xmldir) except OSError: category = '' if 'manual' in service_scans_config[service_scan]: heading = False with open(os.path.join(scandir, files['manual_commands']), 'a') as file: for manual in service_scans_config[service_scan]['manual']: if 'description' in manual: if not heading: file.writelines(e('[*] {service} on {protocol}/{port}\n\n')) heading = True description = manual['description'] file.writelines(e('\t[-] {description}\n\n')) if 'commands' in manual: if not heading: file.writelines(e('[*] {service} on {protocol}/{port}\n\n')) heading = True for manual_command in manual['commands']: manual_command = e(manual_command) file.writelines('\t\t' + e('{manual_command}\n\n')) if heading: file.writelines('\n') if 'scan' in service_scans_config[service_scan]: for scan in service_scans_config[service_scan]['scan']: if 'name' in scan: name = scan['name'] # INFO: change for supporting different complexity levels during service scanning run_level = scan['level'] if 'level' in scan else 0 if (not args.run_only and run_level > max(args.run_level)) or (args.run_only and not run_level in args.run_level): if verbose >= 1: info('Scan profile {bgreen}{name}{rst} is at a {bgree}different complexity level{rst} and is ignored.') continue if 'command' in scan: tag = e('{protocol}/{port}/{name}') command = scan['command'] if 'ports' in scan: port_match = False if protocol == 'tcp': if 'tcp' in scan['ports']: for tcp_port in scan['ports']['tcp']: if port == tcp_port: port_match = True break elif protocol == 'udp': if 'udp' in scan['ports']: for udp_port in scan['ports']['udp']: if port == udp_port: port_match = True break if port_match == False: warn(Fore.YELLOW + '[' + Style.BRIGHT + tag + Style.NORMAL + '] Scan cannot be run against {protocol} port {port}. Skipping.' + Fore.RESET) continue if 'run_once' in scan and scan['run_once'] == True: scan_tuple = (name,) if scan_tuple in target.scans: warn(Fore.YELLOW + '[' + Style.BRIGHT + tag + ' on ' + address + Style.NORMAL + '] Scan should only be run once and it appears to have already been queued. Skipping.' + Fore.RESET) continue else: target.scans.append(scan_tuple) else: scan_tuple = (protocol, port, service, name) if scan_tuple in target.scans: warn(Fore.YELLOW + '[' + Style.BRIGHT + tag + ' on ' + address + Style.NORMAL + '] Scan appears to have already been queued, but it is not marked as run_once in service-scans.toml. Possible duplicate tag? Skipping.' + Fore.RESET) continue else: target.scans.append(scan_tuple) patterns = [] if 'pattern' in scan: patterns = scan['pattern'] try: pending.add(asyncio.ensure_future(run_cmd(semaphore, e(command), target, category=category, tag=tag, patterns=patterns))) except KeyError: error('Service detection {bred}{tag}{rst} on {byellow}{address}{rst} could not be started' + (' with {bblue}{cmd}{rst}.' if verbose >= 1 else '.')) def scan_host(target, concurrent_scans): info('Scanning target {byellow}{target.address}{rst}.') basedir = os.path.abspath(os.path.join(outdir, target.address + srvname)) target.basedir = basedir os.makedirs(basedir, exist_ok=True) exploitdir = os.path.abspath(os.path.join(basedir, 'exploit')) os.makedirs(exploitdir, exist_ok=True) exploitdir = os.path.abspath(os.path.join(basedir, 'privilege_escalation')) os.makedirs(exploitdir, exist_ok=True) lootdir = os.path.abspath(os.path.join(basedir, 'loot')) os.makedirs(lootdir, exist_ok=True) reportdir = os.path.abspath(os.path.join(basedir, 'report')) target.reportdir = reportdir os.makedirs(reportdir, exist_ok=True) screenshotdir = os.path.abspath(os.path.join(reportdir, 'screenshots')) os.makedirs(screenshotdir, exist_ok=True) scandir = os.path.abspath(os.path.join(basedir, 'scans')) target.scandir = scandir os.makedirs(scandir, exist_ok=True) prepare_log_files(scandir, target) os.makedirs(os.path.abspath(os.path.join(scandir, 'xml')), exist_ok=True) open(os.path.abspath(os.path.join(reportdir, 'local.txt')), 'a').close() open(os.path.abspath(os.path.join(reportdir, 'proof.txt')), 'a').close() # Use a lock when writing to specific files that may be written to by other asynchronous functions. target.lock = asyncio.Lock() # Get event loop for current process. loop = asyncio.get_event_loop() # Create a semaphore to limit number of concurrent scans. semaphore = asyncio.Semaphore(concurrent_scans) try: loop.run_until_complete(scan_services(loop, semaphore, target)) info('Finished scanning target {byellow}{target.address}{rst}.') if not args.no_report: loop.run_until_complete(create_report(target)) except KeyboardInterrupt: sys.exit(1) async def create_report(target): address = target.address scandir = target.scandir reportdir = target.reportdir #types = ('*.txt') #filenames = [] #[filenames.extend(glob.glob(os.path.join(scandir, '*', filetype), recursive=True)) for filetype in types] filenames = glob.glob(os.path.join(scandir, '**', '*.txt'), recursive=True) filenames.sort() report_order = ' '.join(filenames) # TODO: make use of config file cmd = '/usr/bin/enscript {0} -o - | /usr/bin/ps2pdf - {1}'.format(report_order, os.path.join(reportdir, files['report'])) info('Creating report for target {byellow}{address}{rst}.') process = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, executable='/bin/bash') await process.communicate() if process.returncode != 0: error('{bred}Report creation{rst} for target {byellow}{address}{rst} returned non-zero exit code: {process.returncode}.') else: info('Report for target {byellow}{address}{rst} was created successfully.') def prepare_log_files(scandir, target): for filename in files: try: # TODO: files dictionary needs to be reorganized, otherwise some unnecessary files will be created in the scan directory if not filename.startswith('_'): continue caption = 'Log session started for host {0} - {1}\n'.format(target.address, datetime.now().strftime('%B %d, %Y - %H:%M:%S')) with open(os.path.join(scandir, files[filename]), 'a') as f: f.write('\n{}\n'.format('=' * len(caption))) f.write(caption) f.write('{}\n\n'.format('=' * len(caption))) except OSError: fail('Error while setting up log file {filename}.') def read_targets_from_file(filename, targets, disable_sanity_checks): if not os.path.isfile(filename): error('The file {filename} with target information was not found.') return (targets, True) try: with open(filename, 'r') as f: entries = f.read() except OSError: error('The file {filename} with target information could not be read.') return (targets, True) error = False for ip in entries.split('\n'): if ip.startswith('#') or len(ip) == 0: continue targets, failed = get_ip_address(ip, targets, disable_sanity_checks) if failed: error = True return (targets, error) def get_ip_address(target, targets, disable_sanity_checks): errors = False try: ip = str(ipaddress.ip_address(target)) if ip not in targets: targets.append(ip) except ValueError: try: target_range = ipaddress.ip_network(target, strict=False) if not disable_sanity_checks and target_range.num_addresses > 256: error(target + ' contains ' + str(target_range.num_addresses) + ' addresses. Check that your CIDR notation is correct. If it is, re-run with the --disable-sanity-checks option to suppress this check.') errors = True else: for ip in target_range.hosts(): ip = str(ip) if ip not in targets: targets.append(ip) except ValueError: try: ip = socket.gethostbyname(target) if target not in targets: targets.append(target) except socket.gaierror: warn(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.') return (targets, errors) def get_header(): logo = r''' _____ __ __________ __________ / _ \ __ ___/ |_ ____\______ \ ____ ____ ____ ____\______ \ / /_\ \| | \ __\/ _ \| _// __ \_/ ___\/ _ \ / \| _/ / | \ | /| | ( <_> ) | \ ___/\ \__( <_> ) | \ | \ \____|__ /____/ |__| \____/|____|_ /\___ >\___ >____/|___| /____|_ / \/ \/ \/ \/ \/ \/ ''' print('\n{0}'.format('-' * 90)) print('{0}'.format(logo)) print('{0} v{1}'.format(' ' * (90 - len(__version__) - 2), __version__)) print('\n\tAutomated network reconnaissance and reporting.') print('\n{0}\n'.format('-' * 90)) class Target: def __init__(self, address): self.address = address self.basedir = '' self.reportdir = '' self.scandir = '' self.scans = [] self.lock = None if __name__ == '__main__': parser = argparse.ArgumentParser(description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.', epilog = get_header()) 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('-ct', '--concurrent-targets', action='store', metavar='', type=int, default=5, help='The maximum number of target hosts to scan concurrently. Default: %(default)s') parser.add_argument('-cs', '--concurrent-scans', action='store', metavar='', type=int, default=10, help='The maximum number of scans to perform per target host. Default: %(default)s') parser.add_argument('--profile', action='store', default='default', help='The port scanning profile to use (defined in port-scan-profiles.toml). Default: %(default)s') parser.add_argument('-o', '--output', action='store', default='results', help='The output directory for results. Default: %(default)s') nmap_group = parser.add_mutually_exclusive_group() nmap_group.add_argument('--nmap', action='store', default=nmap_default_options, help='Override the {nmap_extra} variable in scans. Default: %(default)s') nmap_group.add_argument('--nmap-append', action='store', default='', help='Append to the default {nmap_extra} variable in scans.') parser.add_argument('--skip-service-scan', action='store_true', default=False, help='Do not perfom extended service scanning but only document commands.') parser.add_argument('--run-level', action='store', type=int, default=[0], nargs="+", help='During extended service scanning, only run scanners of a certain complexity level or below.') parser.add_argument('--run-only', action='store_true', default=False, help='If enabled, only run scanners of the specified complexity level during extended service scanning.') parser.add_argument('-r', '--read', action='store', type=str, default='', dest='target_file', help='Read targets from file.') parser.add_argument('--no-report', action='store_true', default=False, help='Do not create a summary report after completing scanning a target.') parser.add_argument('-v', '--verbose', action='count', default=0, help='Enable verbose output. Repeat for more verbosity.') parser.add_argument('--disable-sanity-checks', action='store_true', default=False, help='Disable sanity checks that would otherwise prevent the scans from running.') parser.error = lambda s: fail(s[0].upper() + s[1:]) args = parser.parse_args() if not os.getuid() == 0: warn('Warning: You are not running the program with superuser privileges. Service scanning may be impacted.') config_loaded = get_configuration() if not config_loaded: sys.exit(-1) errors = False if args.concurrent_targets <= 0: error('Argument -ch/--concurrent-targets: must be at least 1.') errors = True concurrent_scans = args.concurrent_scans if concurrent_scans <= 0: error('Argument -ct/--concurrent-scans: must be at least 1.') errors = True if min(args.run_level) < 0 or max(args.run_level) > max_level: error('Argument --run-level: must be between 0 (default) and {}.'.format(max_level)) errors = True port_scan_profile = args.profile found_scan_profile = False for profile in port_scan_profiles_config: if profile == port_scan_profile: found_scan_profile = True for scan in port_scan_profiles_config[profile]: if 'service-detection' not in port_scan_profiles_config[profile][scan]: error('The {profile}.{scan} scan does not have a defined service-detection section. Every scan must at least have a service-detection section defined with a command and a corresponding pattern that extracts the protocol (TCP/UDP), port, and service from the result.') errors = True else: if 'command' not in port_scan_profiles_config[profile][scan]['service-detection']: error('The {profile}.{scan}.service-detection section does not have a command defined. Every service-detection section must have a command and a corresponding pattern that extracts the protocol (TCP/UDP), port, and service from the results.') errors = True else: if '{ports}' in port_scan_profiles_config[profile][scan]['service-detection']['command'] and 'port-scan' not in port_scan_profiles_config[profile][scan]: error('The {profile}.{scan}.service-detection command appears to reference a port list but there is no port-scan section defined in {profile}.{scan}. Define a port-scan section with a command and corresponding pattern that extracts port numbers from the result, or replace the reference with a static list of ports.') errors = True if 'pattern' not in port_scan_profiles_config[profile][scan]['service-detection']: error('The {profile}.{scan}.service-detection section does not have a pattern defined. Every service-detection section must have a command and a corresponding pattern that extracts the protocol (TCP/UDP), port, and service from the results.') errors = True else: if not all(x in port_scan_profiles_config[profile][scan]['service-detection']['pattern'] for x in ['(?P', '(?P', '(?P']): error('The {profile}.{scan}.service-detection pattern does not contain one or more of the following matching groups: port, protocol, service. Ensure that all three of these matching groups are defined and capture the relevant data, e.g. (?P\d+)') errors = True if 'port-scan' in port_scan_profiles_config[profile][scan]: if 'command' not in port_scan_profiles_config[profile][scan]['port-scan']: error('The {profile}.{scan}.port-scan section does not have a command defined. Every port-scan section must have a command and a corresponding pattern that extracts the port from the results.') errors = True if 'pattern' not in port_scan_profiles_config[profile][scan]['port-scan']: error('The {profile}.{scan}.port-scan section does not have a pattern defined. Every port-scan section must have a command and a corresponding pattern that extracts the port from the results.') errors = True else: if '(?P' not in port_scan_profiles_config[profile][scan]['port-scan']['pattern']: error('The {profile}.{scan}.port-scan pattern does not contain a port matching group. Ensure that the port matching group is defined and captures the relevant data, e.g. (?P\d+)') errors = True break if not found_scan_profile: error('Argument --profile: must reference a port scan profile defined in {port_scan_profiles_config_file}. No such profile found: {port_scan_profile}') errors = True nmap_default_options = args.nmap if args.nmap_append: nmap_default_options += " " + args.nmap_append outdir = args.output srvname = '' verbose = args.verbose if len(args.targets) == 0 and not len(args.target_file): error('You must specify at least one target to scan!') errors = True targets = [] for target in args.targets: targets, failed = get_ip_address(target, targets, args.disable_sanity_checks) if failed: errors = True if len(args.target_file) > 0: targets, errors = read_targets_from_file(args.target_file, targets, args.disable_sanity_checks) if not args.disable_sanity_checks and len(targets) > 256: error('A total of ' + str(len(targets)) + ' targets would be scanned. If this is correct, re-run with the --disable-sanity-checks option to suppress this check.') errors = True if errors: sys.exit(1) start_timer = datetime.now().strftime('%H:%M:%S') with ProcessPoolExecutor(max_workers=args.concurrent_targets) as executor: futures = [] for address in targets: target = Target(address) futures.append(executor.submit(scan_host, target, concurrent_scans)) try: for future in as_completed(futures): future.result() except KeyboardInterrupt: for future in futures: future.cancel() executor.shutdown(wait=False) sys.exit(1) end_timer = datetime.now().strftime('%H:%M:%S') tdelta = datetime.strptime(end_timer, '%H:%M:%S') - datetime.strptime(start_timer, '%H:%M:%S') print('\nScanning completed in {}.'.format(tdelta))