From f7ee16c641dd501fc4fa2ce82491e6678a31af9e Mon Sep 17 00:00:00 2001 From: Roman Hergenreder Date: Sun, 23 Feb 2020 12:20:57 +0100 Subject: [PATCH] AutoRecon --- autorecon.py | 829 +++++++++++++++++++++++++++++++++ config/global-patterns.toml | 8 + config/port-scan-profiles.toml | 45 ++ config/service-scans.toml | 577 +++++++++++++++++++++++ portscan.py | 101 ++++ 5 files changed, 1560 insertions(+) create mode 100644 autorecon.py create mode 100644 config/global-patterns.toml create mode 100644 config/port-scan-profiles.toml create mode 100644 config/service-scans.toml create mode 100644 portscan.py diff --git a/autorecon.py b/autorecon.py new file mode 100644 index 0000000..3727d84 --- /dev/null +++ b/autorecon.py @@ -0,0 +1,829 @@ +#!/usr/bin/env python3 +# +# AutoRecon is a multi-threaded network reconnaissance tool which performs automated enumeration of services. +# +# 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 atexit +import argparse +import asyncio +import colorama +from colorama import Fore, Style +from concurrent.futures import ProcessPoolExecutor, as_completed, FIRST_COMPLETED +from datetime import datetime +import ipaddress +import os +import re +import socket +import string +import sys +import time +import toml +import termios + +def _quit(): + termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, TERM_FLAGS) + +atexit.register(_quit) + +TERM_FLAGS = termios.tcgetattr(sys.stdin.fileno()) + +verbose = 0 +nmap = '-vv --reason -Pn' +srvname = '' +heartbeat_interval = 60 +port_scan_profile = None + +port_scan_profiles_config = None +service_scans_config = None +global_patterns = [] + +username_wordlist = '/usr/share/seclists/Usernames/top-usernames-shortlist.txt' +password_wordlist = '/usr/share/seclists/Passwords/darkweb2017-top100.txt' + +rootdir = os.path.dirname(os.path.realpath(__file__)) + +single_target = False +only_scans_dir = False + +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) + + unfmt = '' + if char is not None: + unfmt += color + '[' + Style.BRIGHT + char + Style.NORMAL + ']' + Fore.RESET + sep + unfmt += sep.join(args) + + fmted = unfmt + + for attempt in range(10): + try: + fmted = string.Formatter().vformat(unfmt, args, vals) + break + except KeyError as err: + key = err.args[0] + unfmt = unfmt.replace('{' + key + '}', '{{' + key + '}}') + + 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) + exit(-1) + +def calculate_elapsed_time(start_time): + elapsed_seconds = round(time.time() - start_time) + + m, s = divmod(elapsed_seconds, 60) + h, m = divmod(m, 60) + + elapsed_time = [] + if h == 1: + elapsed_time.append(str(h) + ' hour') + elif h > 1: + elapsed_time.append(str(h) + ' hours') + + if m == 1: + elapsed_time.append(str(m) + ' minute') + elif m > 1: + elapsed_time.append(str(m) + ' minutes') + + if s == 1: + elapsed_time.append(str(s) + ' second') + elif s > 1: + elapsed_time.append(str(s) + ' seconds') + else: + elapsed_time.append('less than a second') + + return ', '.join(elapsed_time) + +port_scan_profiles_config_file = 'port-scan-profiles.toml' +with open(os.path.join(rootdir, 'config', port_scan_profiles_config_file), 'r') as p: + try: + port_scan_profiles_config = toml.load(p) + + 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.') + + except toml.decoder.TomlDecodeError as e: + fail('Error: Couldn\'t parse {port_scan_profiles_config_file} config file. Check syntax and duplicate tags.') + +with open(os.path.join(rootdir, 'config', 'service-scans.toml'), 'r') as c: + try: + service_scans_config = toml.load(c) + except toml.decoder.TomlDecodeError as e: + fail('Error: Couldn\'t parse service-scans.toml config file. Check syntax and duplicate tags.') + +with open(os.path.join(rootdir, 'config', 'global-patterns.toml'), 'r') as p: + try: + global_patterns = toml.load(p) + if 'pattern' in global_patterns: + global_patterns = global_patterns['pattern'] + else: + global_patterns = [] + except toml.decoder.TomlDecodeError as e: + fail('Error: Couldn\'t parse global-patterns.toml config file. Check syntax and duplicate tags.') + +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'] + +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, '_patterns.log'), '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, '_patterns.log'), '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, '_patterns.log'), '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, '_patterns.log'), 'a') as file: + file.writelines(e('{tag} - Matched Pattern: {match}\n\n')) + else: + break + +async def run_cmd(semaphore, cmd, target, tag='?', patterns=[]): + async with semaphore: + address = target.address + scandir = target.scandir + + 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, '_commands.log'), 'a') as file: + file.writelines(e('{cmd}\n\n')) + + start_time = time.time() + process = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, executable='/bin/bash') + async with target.lock: + target.running_tasks.append(tag) + + 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() + async with target.lock: + target.running_tasks.remove(tag) + elapsed_time = calculate_elapsed_time(start_time) + + 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, '_errors.log'), '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 in {elapsed_time}') + + 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, '_patterns.log'), '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, '_patterns.log'), '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, '_patterns.log'), '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, '_patterns.log'), '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 + + 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, '_commands.log'), 'a') as file: + file.writelines(e('{command}\n\n')) + + start_time = time.time() + process = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, executable='/bin/bash') + async with target.lock: + target.running_tasks.append(tag) + + 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() + async with target.lock: + target.running_tasks.remove(tag) + elapsed_time = calculate_elapsed_time(start_time) + + 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, '_errors.log'), '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 in {elapsed_time}') + + 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, '_commands.log'), 'a') as file: + file.writelines(e('{command}\n\n')) + + start_time = time.time() + process = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, executable='/bin/bash') + async with target.lock: + target.running_tasks.append(tag) + + 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() + async with target.lock: + target.running_tasks.remove(tag) + elapsed_time = calculate_elapsed_time(start_time) + + 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, '_errors.log'), '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 in {elapsed_time}') + + services = results[0] + + return {'returncode': process.returncode, 'name': 'run_portscan', 'services': services} + +async def start_heartbeat(target, period=60): + while True: + await asyncio.sleep(period) + async with target.lock: + tasks = target.running_tasks + count = len(tasks) + + tasks_list = '' + if verbose >= 1: + tasks_list = ': {bgreen}' + ', '.join(tasks) + '{rst}' + + current_time = datetime.now().strftime('%H:%M:%S') + + if count > 1: + info('{bgreen}[{current_time}]{rst} - There are {byellow}{count}{rst} tasks still running on {byellow}{target.address}{rst}' + tasks_list) + elif count == 1: + info('{bgreen}[{current_time}]{rst} - There is {byellow}1{rst} task still running on {byellow}{target.address}{rst}' + tasks_list) + +async def scan_services(loop, semaphore, target): + address = target.address + scandir = target.scandir + pending = [] + + heartbeat = loop.create_task(start_heartbeat(target, period=heartbeat_interval)) + + 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: + heartbeat.cancel() + 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('Found {bmagenta}{service}{rst} on {bmagenta}{protocol}/{port}{rst} on target {byellow}{address}{rst}') + + if not only_scans_dir: + with open(os.path.join(target.reportdir, 'notes.txt'), 'a') as file: + file.writelines(e('[*] {service} found on {protocol}/{port}.\n\n\n\n')) + + if protocol == 'udp': + nmap_extra = nmap + " -sU" + else: + nmap_extra = nmap + + 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 + + if 'manual' in service_scans_config[service_scan]: + heading = False + with open(os.path.join(scandir, '_manual_commands.txt'), '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'] + 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'] + + pending.add(asyncio.ensure_future(run_cmd(semaphore, e(command), target, tag=tag, patterns=patterns))) + +def scan_host(target, concurrent_scans): + start_time = time.time() + info('Scanning target {byellow}{target.address}{rst}') + + if single_target: + basedir = os.path.abspath(outdir) + else: + basedir = os.path.abspath(os.path.join(outdir, target.address + srvname)) + target.basedir = basedir + os.makedirs(basedir, exist_ok=True) + + if not only_scans_dir: + exploitdir = os.path.abspath(os.path.join(basedir, 'exploit')) + os.makedirs(exploitdir, exist_ok=True) + + lootdir = os.path.abspath(os.path.join(basedir, 'loot')) + os.makedirs(lootdir, exist_ok=True) + + reportdir = os.path.abspath(os.path.join(basedir, 'report')) + target.reportdir = reportdir + os.makedirs(reportdir, exist_ok=True) + + open(os.path.abspath(os.path.join(reportdir, 'local.txt')), 'a').close() + open(os.path.abspath(os.path.join(reportdir, 'proof.txt')), 'a').close() + + screenshotdir = os.path.abspath(os.path.join(reportdir, 'screenshots')) + os.makedirs(screenshotdir, exist_ok=True) + + scandir = os.path.abspath(os.path.join(basedir, 'scans')) + target.scandir = scandir + os.makedirs(scandir, exist_ok=True) + + os.makedirs(os.path.abspath(os.path.join(scandir, 'xml')), exist_ok=True) + + # 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)) + elapsed_time = calculate_elapsed_time(start_time) + info('Finished scanning target {byellow}{target.address}{rst} in {elapsed_time}') + except KeyboardInterrupt: + sys.exit(1) + +class Target: + def __init__(self, address): + self.address = address + self.basedir = '' + self.reportdir = '' + self.scandir = '' + self.scans = [] + self.lock = None + self.running_tasks = [] + +if __name__ == '__main__': + + parser = argparse.ArgumentParser(description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.') + parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs="*") + parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.') + parser.add_argument('-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', dest='profile_name', 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', dest='output_dir', help='The output directory for results. Default: %(default)s') + parser.add_argument('--single-target', action='store_true', default=False, help='Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the output directory. Default: false') + parser.add_argument('--only-scans-dir', action='store_true', default=False, help='Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: false') + parser.add_argument('--heartbeat', action='store', type=int, default=60, help='Specifies the heartbeat interval (in seconds) for task status messages. Default: %(default)s') + nmap_group = parser.add_mutually_exclusive_group() + nmap_group.add_argument('--nmap', action='store', default='-vv --reason -Pn', help='Override the {nmap_extra} variable in scans. Default: %(default)s') + nmap_group.add_argument('--nmap-append', action='store', default='', help='Append to the default {nmap_extra} variable in scans.') + parser.add_argument('-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. Default: false') + parser.error = lambda s: fail(s[0].upper() + s[1:]) + args = parser.parse_args() + + single_target = args.single_target + only_scans_dir = args.only_scans_dir + + 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 + + port_scan_profile = args.profile_name + + 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 + + heartbeat_interval = args.heartbeat + + nmap = args.nmap + if args.nmap_append: + nmap += " " + args.nmap_append + + outdir = args.output_dir + srvname = '' + verbose = args.verbose + + raw_targets = args.targets + targets = [] + + if len(args.target_file) > 0: + if not os.path.isfile(args.target_file): + error('The target file {args.target_file} was not found.') + sys.exit(1) + try: + with open(args.target_file, 'r') as f: + lines = f.read() + for line in lines.splitlines(): + line = line.strip() + if line.startswith('#') or len(line) == 0: continue + if line not in raw_targets: + raw_targets.append(line) + except OSError: + error('The target file {args.target_file} could not be read.') + sys.exit(1) + + for target in raw_targets: + try: + ip = str(ipaddress.ip_address(target)) + + if ip not in targets: + targets.append(ip) + except ValueError: + + try: + target_range = ipaddress.ip_network(target, strict=False) + if not args.disable_sanity_checks and target_range.num_addresses > 256: + error(target + ' contains ' + str(target_range.num_addresses) + ' addresses. Check that your CIDR notation is correct. If it is, re-run with the --disable-sanity-checks option to suppress this check.') + errors = True + else: + for ip in target_range.hosts(): + ip = str(ip) + if ip not in targets: + targets.append(ip) + except ValueError: + + try: + ip = socket.gethostbyname(target) + + if target not in targets: + targets.append(target) + except socket.gaierror: + error(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.') + errors = True + + if len(targets) == 0: + error('You must specify at least one target to scan!') + errors = True + + if single_target and len(targets) != 1: + error('You cannot provide more than one target when scanning in single-target mode.') + sys.exit(1) + + 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) + + with ProcessPoolExecutor(max_workers=args.concurrent_targets) as executor: + start_time = time.time() + 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) + + elapsed_time = calculate_elapsed_time(start_time) + info('{bgreen}Finished scanning all targets in {elapsed_time}!{rst}') diff --git a/config/global-patterns.toml b/config/global-patterns.toml new file mode 100644 index 0000000..67f6600 --- /dev/null +++ b/config/global-patterns.toml @@ -0,0 +1,8 @@ +# Patterns defined in this file will be checked against every line of output (e.g. port scans and service scans) + +[[pattern]] +description = 'Nmap script found a potential vulnerability. ({match})' +pattern = 'State: (?:(?:LIKELY\_?)?VULNERABLE)' + +[[pattern]] +pattern = '(?i)unauthorized' diff --git a/config/port-scan-profiles.toml b/config/port-scan-profiles.toml new file mode 100644 index 0000000..31cf8ba --- /dev/null +++ b/config/port-scan-profiles.toml @@ -0,0 +1,45 @@ +[default] + + [default.nmap-quick] + + [default.nmap-quick.service-detection] + command = 'nmap {nmap_extra} -sV -sC --version-all -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}' + pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' + + [default.nmap-full-tcp] + + [default.nmap-full-tcp.service-detection] + command = 'nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}' + pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' + + [default.nmap-top-20-udp] + + [default.nmap-top-20-udp.service-detection] + command = 'nmap {nmap_extra} -sU -A --top-ports=20 --version-all -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}' + pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' + +[quick] + + [quick.nmap-quick] + + [quick.nmap-quick.service-detection] + command = 'nmap {nmap_extra} -sV --version-all -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}' + pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' + + [quick.nmap-top-20-udp] + + [quick.nmap-top-20-udp.service-detection] + command = 'nmap {nmap_extra} -sU -A --top-ports=20 --version-all -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}' + pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' + +[udp] + + [udp.udp-top-20] + + [udp.udp-top-20.port-scan] + command = 'unicornscan -mU -p 631,161,137,123,138,1434,445,135,67,53,139,500,68,520,1900,4500,514,49152,162,69 {address} 2>&1 | tee "{scandir}/_top_20_udp_unicornscan.txt"' + pattern = '^UDP open\s*[\w-]+\[\s*(?P\d+)\].*$' + + [udp.udp-top-20.service-detection] + command = 'nmap {nmap_extra} -sU -A -p {ports} --version-all -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}' + pattern = '^(?P\d+)\/(?P(udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' diff --git a/config/service-scans.toml b/config/service-scans.toml new file mode 100644 index 0000000..5210a94 --- /dev/null +++ b/config/service-scans.toml @@ -0,0 +1,577 @@ +# Configurable Variables +username_wordlist = '/usr/share/seclists/Usernames/top-usernames-shortlist.txt' +password_wordlist = '/usr/share/seclists/Passwords/darkweb2017-top100.txt' + +[all-services] # Define scans here that you want to run against all services. + +service-names = [ + '.+' +] + + [[all-services.scan]] + name = 'sslscan' + command = 'if [ "{secure}" == "True" ]; then sslscan --show-certificate --no-colour {address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_sslscan.txt"; fi' + +[cassandra] + +service-names = [ + '^apani1' +] + + [[cassandra.scan]] + name = 'nmap-cassandra' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(cassandra* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cassandra_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cassandra_nmap.xml" {address}' + +[cups] + +service-names = [ + '^ipp' +] + + [[cups.scan]] + name = 'nmap-cups' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(cups* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cups_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cups_nmap.xml" {address}' + +[distcc] + +service-names = [ + '^distccd' +] + + [[distcc.scan]] + name = 'nmap-distcc' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,distcc-cve2004-2687" --script-args="distcc-cve2004-2687.cmd=id" -oN "{scandir}/{protocol}_{port}_distcc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_distcc_nmap.xml" {address}' + +[dns] + +service-names = [ + '^domain' +] + + [[dns.scan]] + name = 'nmap-dns' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_dns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_dns_nmap.xml" {address}' + +[finger] + +service-names = [ + '^finger' +] + + [[finger.scan]] + nmap = 'nmap-finger' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,finger" -oN "{scandir}/{protocol}_{port}_finger_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_finger_nmap.xml" {address}' + +[ftp] + +service-names = [ + '^ftp', + '^ftp\-data' +] + + [[ftp.scan]] + name = 'nmap-ftp' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(ftp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ftp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ftp_nmap.xml" {address}' + + [[ftp.scan.pattern]] + description = 'Anonymous FTP Enabled!' + pattern = 'Anonymous FTP login allowed' + + [[ftp.manual]] + description = 'Bruteforce logins:' + commands = [ + 'hydra -L "{username_wordlist}" -P "{password_wordlist}" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{address}', + 'medusa -U "{username_wordlist}" -P "{password_wordlist}" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {address}' + ] + +[http] + +service-names = [ + '^http', +] + +ignore-service-names = [ + '^nacn_http$' +] + + [[http.scan]] + name = 'nmap-http' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(http* or ssl*) and not (brute or broadcast or dos or external or http-slowloris* or fuzzer)" -oN "{scandir}/{protocol}_{port}_http_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_{scheme}_nmap.xml" {address}' + + [[http.scan.pattern]] + description = 'Identified HTTP Server: {match}' + pattern = 'Server: ([^\n]+)' + + [[http.scan.pattern]] + description = 'WebDAV is enabled' + pattern = 'WebDAV is ENABLED' + + [[http.scan]] + name = 'curl-index' + command = 'curl -sSik {scheme}://{address}:{port}/ -m 10 2>&1 | tee "{scandir}/{protocol}_{port}_{scheme}_index.html"' + + [[http.scan.pattern]] + pattern = '(?i)Powered by [^\n]+' + + [[http.scan]] + name = 'curl-robots' + command = 'curl -sSik {scheme}://{address}:{port}/robots.txt -m 10 2>&1 | tee "{scandir}/{protocol}_{port}_{scheme}_robots.txt"' + + [[http.scan]] + name = 'wkhtmltoimage' + command = 'if hash wkhtmltoimage 2> /dev/null; then wkhtmltoimage --format png {scheme}://{address}:{port}/ {scandir}/{protocol}_{port}_{scheme}_screenshot.png; fi' + + [[http.scan]] + name = 'whatweb' + command = 'whatweb --color=never --no-errors -a 3 -v {scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{scheme}_whatweb.txt"' + + [[http.scan]] + name = 'nikto' + command = 'nikto -ask=no -h {scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{scheme}_nikto.txt"' + + [[http.scan]] + name = 'gobuster' + command = 'if [[ `gobuster -h 2>&1 | grep -F "mode (dir)"` ]]; then gobuster -u {scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/common.txt -e -k -l -s "200,204,301,302,307,401,403" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{scheme}_gobuster.txt"; else gobuster dir -u {scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/common.txt -z -k -l -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{scheme}_gobuster.txt"; fi' + + [[http.manual]] + description = '(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:' + commands = [ + 'dirsearch -u {scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/seclists/Discovery/Web-Content/big.txt --plain-text-report="{scandir}/{protocol}_{port}_{scheme}_dirsearch_big.txt"', + 'dirsearch -u {scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --plain-text-report="{scandir}/{protocol}_{port}_{scheme}_dirsearch_dirbuster.txt"' + ] + + [[http.manual]] + description = '(dirb) Recursive directory/file enumeration for web servers using various wordlists (same as dirsearch above):' + commands = [ + 'dirb {scheme}://{address}:{port}/ /usr/share/seclists/Discovery/Web-Content/big.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{scheme}_dirb_big.txt"', + 'dirb {scheme}://{address}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{scheme}_dirb_dirbuster.txt"' + ] + + [[http.manual]] + description = '(gobuster v3) Directory/file enumeration for web servers using various wordlists (same as dirb above):' + commands = [ + 'gobuster dir -u {scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{scheme}_gobuster_big.txt"', + 'gobuster dir -u {scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{scheme}_gobuster_dirbuster.txt"' + ] + + [[http.manual]] + description = '(gobuster v1 & v2) Directory/file enumeration for web servers using various wordlists (same as dirb above):' + commands = [ + 'gobuster -u {scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{scheme}_gobuster_big.txt"', + 'gobuster -u {scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{scheme}_gobuster_dirbuster.txt"' + ] + + [[http.manual]] + description = '(wpscan) WordPress Security Scanner (useful if WordPress is found):' + commands = [ + 'wpscan --url {scheme}://{address}:{port}/ --no-update -e vp,vt,tt,cb,dbe,u,m --plugins-detection aggressive --plugins-version-detection aggressive -f cli-no-color 2>&1 | tee "{scandir}/{protocol}_{port}_{scheme}_wpscan.txt"' + ] + + [[http.manual]] + description = "Credential bruteforcing commands (don't run these without modifying them):" + commands = [ + 'hydra -L "{username_wordlist}" -P "{password_wordlist}" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{scheme}_auth_hydra.txt" {scheme}-get://{address}/path/to/auth/area', + 'medusa -U "{username_wordlist}" -P "{password_wordlist}" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{scheme}_auth_medusa.txt" -M http -h {address} -m DIR:/path/to/auth/area', + 'hydra -L "{username_wordlist}" -P "{password_wordlist}" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{scheme}_form_hydra.txt" {scheme}-post-form://{address}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', + 'medusa -U "{username_wordlist}" -P "{password_wordlist}" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{scheme}_form_medusa.txt" -M web-form -h {address} -m FORM:/path/to/login.php -m FORM-DATA:"post?username=&password=" -m DENY-SIGNAL:"invalid login message"', + ] + +[imap] + +service-names = [ + '^imap' +] + + [[imap.scan]] + name = 'nmap-imap' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(imap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_imap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_imap_nmap.xml" {address}' + +[kerberos] + +service-names = [ + '^kerberos', + '^kpasswd' +] + + [[kerberos.scan]] + name = 'nmap-kerberos' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}' + +[ldap] + +service-names = [ + '^ldap' +] + + [[ldap.scan]] + name = 'nmap-ldap' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(ldap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ldap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ldap_nmap.xml" {address}' + + [[ldap.scan]] + name = 'enum4linux' + command = 'enum4linux -a -M -l -d {address} 2>&1 | tee "{scandir}/enum4linux.txt"' + run_once = true + ports.tcp = [139, 389, 445] + ports.udp = [137] + + [[ldap.manual]] + description = 'ldapsearch command (modify before running)' + commands = [ + 'ldapsearch -x -D "" -w """ -p {port} -h {address} -b "dc=example,dc=com" -s sub "(objectclass=*) 2>&1 | tee > "{scandir}/{protocol}_{port}_ldap_all-entries.txt"' + ] + +[mongodb] + +service-names = [ + '^mongod' +] + + [[mongodb.scan]] + name = 'nmap-mongodb' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(mongodb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mongodb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mongodb_nmap.xml" {address}' + +[mssql] + +service-names = [ + '^mssql', + '^ms\-sql' +] + + [[mssql.scan]] + name = 'nmap-mssql' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(ms-sql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="mssql.instance-port={port},mssql.username=sa,mssql.password=sa" -oN "{scandir}/{protocol}_{port}_mssql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mssql_nmap.xml" {address}' + + [[mssql.manual]] + description = '(sqsh) interactive database shell' + commands = [ + 'sqsh -U -P -S {address}:{port}' + ] + +[mysql] + +service-names = [ + '^mysql' +] + + [[mysql.scan]] + name = 'nmap-mysql' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(mysql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mysql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mysql_nmap.xml" {address}' + +[nfs] + +service-names = [ + '^nfs', + '^rpcbind' +] + + [[nfs.scan]] + name = 'nmap-nfs' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(rpcinfo or nfs*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_nfs_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nfs_nmap.xml" {address}' + + [[nfs.scan]] + name = 'showmount' + command = 'showmount -e {address} 2>&1 | tee "{scandir}/{protocol}_{port}_showmount.txt"' + +[nntp] + +service-names = [ + '^nntp' +] + + [[nntp.scan]] + name = 'nmap-nntp' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,nntp-ntlm-info" -oN "{scandir}/{protocol}_{port}_nntp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nntp_nmap.xml" {address}' + +[oracle] + +service-names = [ + '^oracle' +] + + [[oracle.scan]] + name = 'nmap-oracle' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(oracle* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_oracle_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_nmap.xml" {address}' + + [[oracle.scan]] + name = 'oracle-tnscmd-ping' + command = 'tnscmd10g ping -h {address} -p {port} 2>&1 | tee "{scandir}/{protocol}_{port}_oracle_tnscmd_ping.txt"' + + [[oracle.scan]] + name = 'oracle-tnscmd-version' + command = 'tnscmd10g version -h {address} -p {port} 2>&1 | tee "{scandir}/{protocol}_{port}_oracle_tnscmd_version.txt"' + + [[oracle.scan]] + name = 'oracle-tnscmd-version' + command = 'tnscmd10g version -h {address} -p {port} 2>&1 | tee "{scandir}/{protocol}_{port}_oracle_tnscmd_version.txt"' + + [[oracle.scan]] + name = 'oracle-scanner' + command = 'oscanner -v -s {address} -P {port} 2>&1 | tee "{scandir}/{protocol}_{port}_oracle_scanner.txt"' + + [[oracle.manual]] + description = 'Brute-force SIDs using Nmap' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,oracle-sid-brute" -oN "{scandir}/{protocol}_{port}_oracle_sid-brute_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_sid-brute_nmap.xml" {address}' + + [[oracle.manual]] + description = 'Install ODAT (https://github.com/quentinhardy/odat) and run the following commands:' + commands = [ + 'python odat.py tnscmd -s {address} -p {port} --ping', + 'python odat.py tnscmd -s {address} -p {port} --version', + 'python odat.py tnscmd -s {address} -p {port} --status', + 'python odat.py sidguesser -s {address} -p {port}', + 'python odat.py passwordguesser -s {address} -p {port} -d --accounts-file accounts/accounts_multiple.txt', + 'python odat.py tnspoison -s {address} -p {port} -d --test-module' + ] + + [[oracle.manual]] + description = 'Install Oracle Instant Client (https://github.com/rapid7/metasploit-framework/wiki/How-to-get-Oracle-Support-working-with-Kali-Linux) and then bruteforce with patator:' + commands = [ + 'patator oracle_login host={address} port={port} user=COMBO00 password=COMBO01 0=/usr/share/seclists/Passwords/Default-Credentials/oracle-betterdefaultpasslist.txt -x ignore:code=ORA-01017 -x ignore:code=ORA-28000' + ] + +[pop3] + +service-names = [ + '^pop3' +] + + [[pop3.scan]] + name = 'nmap-pop3' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(pop3* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_pop3_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_pop3_nmap.xml" {address}' + +[rdp] + +service-names = [ + '^rdp', + '^ms\-wbt\-server', + '^ms\-term\-serv' +] + + [[rdp.scan]] + name = 'nmap-rdp' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(rdp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_rdp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rdp_nmap.xml" {address}' + + [[rdp.manual]] + description = 'Bruteforce logins:' + commands = [ + 'hydra -L "{username_wordlist}" -P "{password_wordlist}" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{address}', + 'medusa -U "{username_wordlist}" -P "{password_wordlist}" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {address}' + ] + +[rmi] + +service-names = [ + '^java\-rmi', + '^rmiregistry' +] + + [[rmi.scan]] + name = 'nmap-rmi' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,rmi-vuln-classloader,rmi-dumpregistry" -oN "{scandir}/{protocol}_{port}_rmi_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rmi_nmap.xml" {address}' + +[rpc] + +service-names = [ + '^msrpc', + '^rpcbind', + '^erpc' +] + + [[rpc.scan]] + name = 'nmap-msrpc' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,msrpc-enum,rpc-grind,rpcinfo" -oN "{scandir}/{protocol}_{port}_rpc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rpc_nmap.xml" {address}' + + [[rpc.manual]] + description = 'RPC Client:' + commands = [ + 'rpcclient -p {port} -U "" {address}' + ] + +[sip] + +service-names = [ + '^asterisk' +] + + [[sip.scan]] + name = 'nmap-sip' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,sip-enum-users,sip-methods" -oN "{scandir}/{protocol}_{port}_sip_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_sip_nmap.xml" {address}' + + [[sip.scan]] + name = 'svwar' + command = 'svwar -D -m INVITE -p {port} {address}' + +[ssh] + +service-names = [ + '^ssh' +] + + [[ssh.scan]] + name = 'nmap-ssh' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,ssh2-enum-algos,ssh-hostkey,ssh-auth-methods" -oN "{scandir}/{protocol}_{port}_ssh_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ssh_nmap.xml" {address}' + + [[ssh.manual]] + description = 'Bruteforce logins:' + commands = [ + 'hydra -L "{username_wordlist}" -P "{password_wordlist}" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{address}', + 'medusa -U "{username_wordlist}" -P "{password_wordlist}" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {address}' + ] +[smb] + +service-names = [ + '^smb', + '^microsoft\-ds', + '^netbios' +] + + [[smb.scan]] + name = 'nmap-smb' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(nbstat or smb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_nmap.xml" {address}' + + [[smb.scan]] + name = 'enum4linux' + command = 'enum4linux -a -M -l -d {address} 2>&1 | tee "{scandir}/enum4linux.txt"' + run_once = true + ports.tcp = [139, 389, 445] + ports.udp = [137] + + [[smb.scan]] + name = 'nbtscan' + command = 'nbtscan -rvh {address} 2>&1 | tee "{scandir}/nbtscan.txt"' + run_once = true + ports.udp = [137] + + [[smb.scan]] + name = 'smbclient' + command = 'smbclient -L\\ -N -I {address} 2>&1 | tee "{scandir}/smbclient.txt"' + run_once = true + ports.tcp = [139, 445] + + [[smb.scan]] + name = 'smbmap-share-permissions' + command = 'smbmap -H {address} -P {port} 2>&1 | tee -a "{scandir}/smbmap-share-permissions.txt"; smbmap -u null -p "" -H {address} -P {port} 2>&1 | tee -a "{scandir}/smbmap-share-permissions.txt"' + + [[smb.scan]] + name = 'smbmap-list-contents' + command = 'smbmap -H {address} -P {port} -R 2>&1 | tee -a "{scandir}/smbmap-list-contents.txt"; smbmap -u null -p "" -H {address} -P {port} -R 2>&1 | tee -a "{scandir}/smbmap-list-contents.txt"' + + [[smb.scan]] + name = 'smbmap-execute-command' + command = 'smbmap -H {address} -P {port} -x "ipconfig /all" 2>&1 | tee -a "{scandir}/smbmap-execute-command.txt"; smbmap -u null -p "" -H {address} -P {port} -x "ipconfig /all" 2>&1 | tee -a "{scandir}/smbmap-execute-command.txt"' + + [[smb.manual]] + description = 'Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:' + commands = [ + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}', + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}', + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}' + ] + +[smtp] + +service-names = [ + '^smtp' +] + + [[smtp.scan]] + name = 'nmap-smtp' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(smtp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smtp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smtp_nmap.xml" {address}' + + [[smtp.scan]] + name = 'smtp-user-enum' + command = 'smtp-user-enum -M VRFY -U "{username_wordlist}" -t {address} -p {port} 2>&1 | tee "{scandir}/{protocol}_{port}_smtp_user-enum.txt"' + +[snmp] + +service-names = [ + '^snmp' +] + + [[snmp.scan]] + name = 'nmap-snmp' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(snmp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_snmp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_snmp_nmap.xml" {address}' + + [[snmp.scan]] + name = 'onesixtyone' + command = 'onesixtyone -c /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt -dd {address} 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_onesixtyone.txt"' + run_once = true + ports.udp = [161] + + [[snmp.scan]] + name = 'snmpwalk' + command = 'snmpwalk -c public -v 1 {address} 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk.txt"' + run_once = true + ports.udp = [161] + + [[snmp.scan]] + name = 'snmpwalk-system-processes' + command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.1.6.0 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_system_processes.txt"' + run_once = true + ports.udp = [161] + + [[snmp.scan]] + name = 'snmpwalk-running-processes' + command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.2 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_running_processes.txt"' + run_once = true + ports.udp = [161] + + [[snmp.scan]] + name = 'snmpwalk-process-paths' + command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.4 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_process_paths.txt"' + run_once = true + ports.udp = [161] + + [[snmp.scan]] + name = 'snmpwalk-storage-units' + command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_storage_units.txt"' + run_once = true + ports.udp = [161] + + [[snmp.scan]] + name = 'snmpwalk-software-names' + command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.6.3.1.2 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_software_names.txt"' + run_once = true + ports.udp = [161] + + [[snmp.scan]] + name = 'snmpwalk-user-accounts' + command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.4.1.77.1.2.25 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_user_accounts.txt"' + run_once = true + ports.udp = [161] + + [[snmp.scan]] + name = 'snmpwalk-tcp-ports' + command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.6.13.1.3 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_tcp_ports.txt"' + run_once = true + ports.udp = [161] + +[telnet] + +service-names = [ + '^telnet' +] + + [[telnet.scan]] + name = 'nmap-telnet' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,telnet-encryption,telnet-ntlm-info" -oN "{scandir}/{protocol}_{port}_telnet-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_telnet_nmap.xml" {address}' + +[tftp] + +service-names = [ + '^tftp' +] + + [[tftp.scan]] + name = 'nmap-tftp' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,tftp-enum" -oN "{scandir}/{protocol}_{port}_tftp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_tftp_nmap.xml" {address}' + +[vnc] + +service-names = [ + '^vnc' +] + + [[vnc.scan]] + name = 'nmap-vnc' + command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(vnc* or realvnc* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_vnc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_vnc_nmap.xml" {address}' diff --git a/portscan.py b/portscan.py new file mode 100644 index 0000000..5d1ae51 --- /dev/null +++ b/portscan.py @@ -0,0 +1,101 @@ +#!/usr/bin/python + +import socket +import sys +import re +from importlib import util + +threading_spec = util.find_spec("threading") +queue_spec = util.find_spec("queue") + +if threading_spec is not None: + import threading + import queue + NUM_THREADS = 10 + THREADING_ENABLED = True + QUEUE = queue.Queue() +else: + THREADING_ENABLED = False + +if len(sys.argv) < 2: + print("Usage: %s [ports] [num_threads]") + exit(1) + +host = sys.argv[1] +ports = range(1,1001) + +if len(sys.argv) >= 3: + ports_param = sys.argv[2] + pattern = re.compile("^(\\d)+(-(\\d+)?)?$") + m = pattern.match(ports_param) + if m is None: + print("Invalid port range") + exit(1) + + start_port = int(m.group(1)) + end_port = start_port + if m.group(2) is not None: + if m.group(3) is None: + end_port = 65535 + else: + end_port = int(m.group(3)) + + if start_port < 1 or start_port > 65535: + print("Invalid start port") + exit(1) + elif end_port < 1 or end_port > 65535: + print("Invalid end port") + exit(1) + elif start_port > end_port: + print("Invalid port range") + exit(1) + + ports = range(start_port, end_port+1) + +if len(sys.argv) >= 4: + if not THREADING_ENABLED: + print("Threading is not supported by this system, you need the libraries: threading, queue") + exit(1) + else: + NUM_THREADS = int(sys.argv[3]) + if NUM_THREADS < 1: + print("Invalid thread count:", NUM_THREADS) + exit(1) + +def tryConnect(host, port): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(3.0) + sock.connect((host,port)) + sock.close() + return True + except Exception as e: + return False + +def doWork(q, host): + while not q.empty(): + p = q.get() + if tryConnect(host, p): + print("[+] Port %d is open" % p) + +if not THREADING_ENABLED: + print("Scanning ports: %d-%d..." % (ports[0], ports[len(ports)-1])) + open_ports = [] + for p in ports: + if tryConnect(host, p): + print("[+] Port %d is open" % p) + print("Done") +else: + print("Scanning ports: %d-%d with %d threads..." % (ports[0], ports[len(ports)-1], NUM_THREADS)) + for i in ports: + QUEUE.put(i) + + threads = [] + for i in range(NUM_THREADS): + t = threading.Thread(target=doWork, args=(QUEUE, host)) + t.start() + threads.append(t) + + for t in threads: + t.join() + print("Done")