diff --git a/README.md b/README.md index 74ade16..c88e90f 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ whatweb On Kali Linux, you can ensure these are all installed using the following commands: ```bash -sudo apt install seclists curl dnsrecon enum4linux feroxbuster gobuster impacket-scripts nbtscan nikto nmap onesixtyone oscanner redis-tools smbclient smbmap snmp sslscan sipvicious tnscmd10g whatweb +sudo apt install seclists curl dnsrecon enum4linux feroxbuster gobuster impacket-scripts nbtscan nuclei nikto nmap onesixtyone oscanner redis-tools smbclient smbmap snmp sslscan sipvicious tnscmd10g whatweb ``` ### Installation Method #1: pipx (Recommended) diff --git a/autorecon/default-plugins/bruteforce-http.py b/autorecon/default-plugins/bruteforce-http.py index d735ac4..1b7ff7f 100644 --- a/autorecon/default-plugins/bruteforce-http.py +++ b/autorecon/default-plugins/bruteforce-http.py @@ -12,7 +12,7 @@ class BruteforceHTTP(ServiceScan): self.match_service_name('^nacn_http$', negative_match=True) def manual(self, service, plugin_was_run): - service.add_manual_commands('Credential bruteforcing commands (don\'t run these without modifying them):', [ + service.add_manual_commands('Credential bruteforcing commands (dont run these without modifying them):', [ 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_auth_hydra.txt" {http_scheme}-get://{addressv6}/path/to/auth/area', 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_auth_medusa.txt" -M http -h {addressv6} -m DIR:/path/to/auth/area', 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_form_hydra.txt" {http_scheme}-post-form://{addressv6}/path/to/login.php:"username=^USER^&password=^PASS^":"invalid-login-message"', diff --git a/autorecon/default-plugins/nuclei.py b/autorecon/default-plugins/nuclei.py new file mode 100644 index 0000000..7d99198 --- /dev/null +++ b/autorecon/default-plugins/nuclei.py @@ -0,0 +1,36 @@ +from autorecon.plugins import ServiceScan +from shutil import which + + +class Nuclei(ServiceScan): + def __init__(self): + super().__init__() + self.name = "nuclei" + self.tags = ["default", "safe", "long"] + + self.cmd = 'nuclei -disable-update-check -no-color -target {address}:{port} -scan-all-ips -o "{scandir}/{protocol}_{port}_nuclei.txt"' + + def configure(self): + self.match_all_service_names(True) + self.add_pattern( + r"(.*\[(critical|high)\].*)", + description="Nuclei {match2} finding: {match1}", + ) + + def check(self): + if which("nuclei") is None: + self.error( + "The program nuclei could not be found. Make sure it is installed. (On Kali, run: sudo apt install nuclei)" + ) + return False + + async def run(self, service): + if service.target.ipversion == "IPv4": + await service.execute(self.cmd) + + def manual(self, service, plugin_was_run): + if service.target.ipversion == "IPv4" and not plugin_was_run: + service.add_manual_command( + f"({self.name}) Fast and customizable vulnerability scanner based on simple YAML based DSL:", + self.cmd, + ) \ No newline at end of file diff --git a/autorecon/default-plugins/reporting-markdown.py b/autorecon/default-plugins/reporting-markdown.py index bba0330..86f8f19 100644 --- a/autorecon/default-plugins/reporting-markdown.py +++ b/autorecon/default-plugins/reporting-markdown.py @@ -10,24 +10,31 @@ class Markdown(Report): async def run(self, targets): if len(targets) > 1: - report = os.path.join(config['output'], 'report.md') + report = os.path.join(config['output'], 'Full_Report.md') + single_target = False elif len(targets) == 1: - report = os.path.join(targets[0].reportdir, 'report.md') + report = targets[0].reportdir + single_target = True else: return + os.makedirs(report, exist_ok=True) for target in targets: - os.makedirs(os.path.join(report, target.address), exist_ok=True) + # Use target.address subdirectory only if multiple targets exist + target_root = report if single_target else os.path.join(report, target.address) + os.makedirs(target_root, exist_ok=True) files = [os.path.abspath(filename) for filename in glob.iglob(os.path.join(target.scandir, '**/*'), recursive=True) if os.path.isfile(filename) and filename.endswith(('.txt', '.html'))] + # --- Port Scans --- if target.scans['ports']: - os.makedirs(os.path.join(report, target.address, 'Port Scans'), exist_ok=True) + ports_dir = os.path.join(target_root, 'Port Scans') + os.makedirs(ports_dir, exist_ok=True) for scan in target.scans['ports'].keys(): if len(target.scans['ports'][scan]['commands']) > 0: - with open(os.path.join(report, target.address, 'Port Scans', 'PortScan - ' + target.scans['ports'][scan]['plugin'].name + '.md'), 'w') as output: + with open(os.path.join(ports_dir, 'PortScan - ' + target.scans['ports'][scan]['plugin'].name + '.md'), 'w') as output: for command in target.scans['ports'][scan]['commands']: output.writelines('```bash\n' + command[0] + '\n```') for filename in files: @@ -35,13 +42,17 @@ class Markdown(Report): output.writelines('\n\n[' + filename + '](file://' + filename + '):\n\n') with open(filename, 'r') as file: output.writelines('```\n' + file.read() + '\n```\n') + + # --- Services --- if target.scans['services']: - os.makedirs(os.path.join(report, target.address, 'Services'), exist_ok=True) + services_dir = os.path.join(target_root, 'Services') + os.makedirs(services_dir, exist_ok=True) for service in target.scans['services'].keys(): - os.makedirs(os.path.join(report, target.address, 'Services', 'Service - ' + service.tag().replace('/', '-')), exist_ok=True) + service_dir = os.path.join(services_dir, 'Service - ' + service.tag().replace('/', '-')) + os.makedirs(service_dir, exist_ok=True) for plugin in target.scans['services'][service].keys(): if len(target.scans['services'][service][plugin]['commands']) > 0: - with open(os.path.join(report, target.address, 'Services', 'Service - ' + service.tag().replace('/', '-'), target.scans['services'][service][plugin]['plugin'].name + '.md'), 'w') as output: + with open(os.path.join(service_dir, target.scans['services'][service][plugin]['plugin'].name + '.md'), 'w') as output: for command in target.scans['services'][service][plugin]['commands']: output.writelines('```bash\n' + command[0] + '\n```') for filename in files: @@ -50,26 +61,30 @@ class Markdown(Report): with open(filename, 'r') as file: output.writelines('```\n' + file.read() + '\n```\n') + # --- Manual Commands --- manual_commands = os.path.join(target.scandir, '_manual_commands.txt') if os.path.isfile(manual_commands): - with open(os.path.join(report, target.address, 'Manual Commands' + '.md'), 'w') as output: + with open(os.path.join(target_root, 'Manual Commands.md'), 'w') as output: with open(manual_commands, 'r') as file: output.writelines('```bash\n' + file.read() + '\n```') + # --- Patterns --- patterns = os.path.join(target.scandir, '_patterns.log') if os.path.isfile(patterns): - with open(os.path.join(report, target.address, 'Patterns' + '.md'), 'w') as output: + with open(os.path.join(target_root, 'Patterns.md'), 'w') as output: with open(patterns, 'r') as file: output.writelines(file.read()) + # --- Commands --- commands = os.path.join(target.scandir, '_commands.log') if os.path.isfile(commands): - with open(os.path.join(report, target.address, 'Commands' + '.md'), 'w') as output: + with open(os.path.join(target_root, 'Commands.md'), 'w') as output: with open(commands, 'r') as file: output.writelines('```bash\n' + file.read() + '\n```') + # --- Errors --- errors = os.path.join(target.scandir, '_errors.log') if os.path.isfile(errors): - with open(os.path.join(report, target.address, 'Errors' + '.md'), 'w') as output: + with open(os.path.join(target_root, 'Errors.md'), 'w') as output: with open(errors, 'r') as file: output.writelines('```\n' + file.read() + '\n```') diff --git a/autorecon/default-plugins/smtp-user-enum.py b/autorecon/default-plugins/smtp-user-enum.py index 5a995b2..18a8c02 100644 --- a/autorecon/default-plugins/smtp-user-enum.py +++ b/autorecon/default-plugins/smtp-user-enum.py @@ -15,6 +15,6 @@ class SMTPUserEnum(ServiceScan): await service.execute('hydra smtp-enum://{addressv6}:{port}/expn -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_expn.txt') def manual(self, service, plugin_was_run): - service.add_manual_command('Try User Enumeration using "RCPT TO". Replace with the target\'s domain name:', [ + service.add_manual_command('Try User Enumeration using "RCPT TO". Replace with the target domain name:', [ 'hydra smtp-enum://{addressv6}:{port}/rcpt -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -o "{scandir}/{protocol}_{port}_smtp_user-enum_hydra_rcpt.txt" -p ' ]) diff --git a/autorecon/main.py b/autorecon/main.py index bbc0ddd..24ceba7 100644 --- a/autorecon/main.py +++ b/autorecon/main.py @@ -41,8 +41,9 @@ if not os.path.exists(config['data_dir']): 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')) + develop =False + if not os.path.exists(os.path.join(config['data_dir'], 'plugins')) or develop: + shutil.copytree(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default-plugins'), os.path.join(config['data_dir'], 'plugins'), dirs_exist_ok=True) 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)): @@ -329,6 +330,7 @@ async def port_scan(plugin, target): 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): @@ -453,6 +455,7 @@ async def service_scan(plugin, service): 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): @@ -485,6 +488,7 @@ async def scan_target(target): 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') @@ -496,8 +500,8 @@ async def scan_target(target): 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() + #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) @@ -506,7 +510,7 @@ async def scan_target(target): target.reportdir = reportdir - pending = [] + pending = set() heartbeat = asyncio.create_task(start_heartbeat(target, period=config['heartbeat'])) @@ -515,7 +519,7 @@ async def scan_target(target): forced_services = [x.strip().lower() for x in config['force_services']] for forced_service in forced_services: - match = re.search('(?P(tcp|udp))\/(?P\d+)\/(?P[\w\-]+)(\/(?Psecure|insecure))?', forced_service) + match = re.search(r'(?P(tcp|udp))/(?P\d+)/(?P[\w\-]+)(/(?Psecure|insecure))?', forced_service) if match: protocol = match.group('protocol') if config['proxychains'] and protocol == 'udp': @@ -529,7 +533,7 @@ async def scan_target(target): services.append(service) if services: - pending.append(asyncio.create_task(asyncio.sleep(0))) + pending.add(asyncio.create_task(asyncio.sleep(0))) else: error('No services were defined. Please check your service syntax: [tcp|udp]///[secure|insecure]') heartbeat.cancel() @@ -539,6 +543,11 @@ async def scan_target(target): 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 @@ -560,7 +569,7 @@ async def scan_target(target): if matching_tags and not excluded_tags: target.scans['ports'][plugin.slug] = {'plugin':plugin, 'commands':[]} - pending.append(asyncio.create_task(port_scan(plugin, target))) + pending.add(asyncio.create_task(port_scan(plugin, target))) async with autorecon.lock: autorecon.scanning_targets.append(target) @@ -627,6 +636,7 @@ async def scan_target(target): 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' @@ -656,6 +666,13 @@ async def scan_target(target): 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']: @@ -1250,7 +1267,7 @@ async def run(): mode = 'udp' port = port.split('U:')[1] - match = re.search('^([0-9]+)\-([0-9]+)$', port) + match = re.search(r'^([0-9]+)-([0-9]+)$', port) if match: num1 = int(match.group(1)) num2 = int(match.group(2)) @@ -1533,10 +1550,10 @@ async def run(): if not config['disable_keyboard_control']: terminal_settings = termios.tcgetattr(sys.stdin.fileno()) - pending = [] + pending = set() i = 0 while autorecon.pending_targets: - pending.append(asyncio.create_task(scan_target(autorecon.pending_targets.pop(0)))) + pending.add(asyncio.create_task(scan_target(autorecon.pending_targets.pop(0)))) i+=1 if i >= num_initial_targets: break