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