1670 lines
68 KiB
Python
1670 lines
68 KiB
Python
#!/usr/bin/python3
|
|
|
|
import argparse, asyncio, importlib.util, inspect, ipaddress, math, os, re, select, shutil, signal, socket, sys, termios, time, traceback, tty
|
|
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"
|
|
|
|
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.')
|
|
|
|
|
|
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(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default-plugins'), os.path.join(config['data_dir'], 'plugins'))
|
|
shutil.copytree(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'wordlists'), os.path.join(config['data_dir'], 'wordlists'))
|
|
else:
|
|
if not os.path.exists(os.path.join(config['data_dir'], 'plugins')):
|
|
shutil.copytree(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default-plugins'), os.path.join(config['data_dir'], 'plugins'))
|
|
if not os.path.exists(os.path.join(config['data_dir'], 'wordlists')):
|
|
shutil.copytree(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'wordlists'), os.path.join(config['data_dir'], 'wordlists'))
|
|
if not os.path.exists(os.path.join(config['data_dir'], 'VERSION-' + VERSION)):
|
|
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']:
|
|
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
|
|
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']:
|
|
# 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('-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']:
|
|
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)
|
|
|
|
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']:
|
|
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']:
|
|
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()
|