From ca77242c8661f5b5a083b28ead31bf8f4908478b Mon Sep 17 00:00:00 2001 From: MrMatch246 Date: Mon, 4 Aug 2025 17:14:44 +0200 Subject: [PATCH 01/17] added continue functionality for port and service plugins --- autorecon/main.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/autorecon/main.py b/autorecon/main.py index bbc0ddd..c5abcb4 100644 --- a/autorecon/main.py +++ b/autorecon/main.py @@ -329,6 +329,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 +454,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 +487,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') @@ -539,6 +542,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 @@ -627,6 +635,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 +665,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']: From 8bfedde7b319866e4ea9ce4ce8c3f2a9f47736a7 Mon Sep 17 00:00:00 2001 From: MrMatch246 Date: Wed, 6 Aug 2025 11:43:19 +0200 Subject: [PATCH 02/17] coherent usage of sets --- autorecon/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/autorecon/main.py b/autorecon/main.py index c5abcb4..5de2cdf 100644 --- a/autorecon/main.py +++ b/autorecon/main.py @@ -509,7 +509,7 @@ async def scan_target(target): target.reportdir = reportdir - pending = [] + pending = set() heartbeat = asyncio.create_task(start_heartbeat(target, period=config['heartbeat'])) @@ -532,7 +532,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() @@ -568,7 +568,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) @@ -1549,10 +1549,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 From ec2283ba45853804ae9d0172ee51a659ace063bf Mon Sep 17 00:00:00 2001 From: MrMatch246 Date: Wed, 6 Aug 2025 11:43:47 +0200 Subject: [PATCH 03/17] use of raw strings for regex --- autorecon/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autorecon/main.py b/autorecon/main.py index 5de2cdf..64c767e 100644 --- a/autorecon/main.py +++ b/autorecon/main.py @@ -518,7 +518,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': @@ -1266,7 +1266,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)) From 220cb06ae0997efef70db7aa6bac18eb657c2fa4 Mon Sep 17 00:00:00 2001 From: MrMatch246 Date: Tue, 12 Aug 2025 17:18:59 +0200 Subject: [PATCH 04/17] added nuclei --- README.md | 2 +- autorecon/default-plugins/nuclei.py | 36 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 autorecon/default-plugins/nuclei.py 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/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 From 3d82bb70e53701632393a48387197b0a4c13e364 Mon Sep 17 00:00:00 2001 From: MrMatch246 Date: Wed, 13 Aug 2025 16:46:05 +0200 Subject: [PATCH 05/17] fixed reporting-markdown.py --- .../default-plugins/reporting-markdown.py | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) 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```') From 823aa059dfdf3f738aade9b295690773b4d38514 Mon Sep 17 00:00:00 2001 From: MrMatch246 Date: Thu, 4 Sep 2025 15:53:23 +0200 Subject: [PATCH 06/17] Added option for plugin renewals --- autorecon/main.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/autorecon/main.py b/autorecon/main.py index 64c767e..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)): @@ -499,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) From 8af0595cd62e48191690386d00beef6f34027648 Mon Sep 17 00:00:00 2001 From: MrMatch246 <50702646+MrMatch246@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:49:16 +0200 Subject: [PATCH 07/17] Fixed ' that breaks md highlighting in obsidian --- autorecon/default-plugins/bruteforce-http.py | 2 +- autorecon/default-plugins/smtp-user-enum.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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 ' ]) From 54673f92a39a882bd586256c5732bf0dfa89aa3f Mon Sep 17 00:00:00 2001 From: MrMatch246 <50702646+MrMatch246@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:27:56 +0200 Subject: [PATCH 08/17] Removed extra dirs --- autorecon/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/autorecon/main.py b/autorecon/main.py index 24ceba7..abbd9eb 100644 --- a/autorecon/main.py +++ b/autorecon/main.py @@ -491,8 +491,8 @@ async def scan_target(target): 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) + #exploitdir = os.path.join(basedir, 'exploit') + #os.makedirs(exploitdir, exist_ok=True) lootdir = os.path.join(basedir, 'loot') os.makedirs(lootdir, exist_ok=True) @@ -503,8 +503,8 @@ async def scan_target(target): #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) + #screenshotdir = os.path.join(reportdir, 'screenshots') + #os.makedirs(screenshotdir, exist_ok=True) else: reportdir = scandir From e151ab6d891ce9bb5baef8f77b61a9f3c7e5fb20 Mon Sep 17 00:00:00 2001 From: MrMatch246 <50702646+MrMatch246@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:22:29 +0200 Subject: [PATCH 09/17] removed number of dirbuster threads --- autorecon/default-plugins/dirbuster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autorecon/default-plugins/dirbuster.py b/autorecon/default-plugins/dirbuster.py index bd48ab0..a6848e6 100644 --- a/autorecon/default-plugins/dirbuster.py +++ b/autorecon/default-plugins/dirbuster.py @@ -15,7 +15,7 @@ class DirBuster(ServiceScan): def configure(self): self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') self.add_list_option('wordlist', default=[os.path.join(config['data_dir'], 'wordlists', 'dirbuster.txt')], help='The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: %(default)s') - self.add_option('threads', default=10, help='The number of threads to use when directory busting. Default: %(default)s') + self.add_option('threads', default=4, help='The number of threads to use when directory busting. Default: %(default)s') self.add_option('ext', default='txt,html,php,asp,aspx,jsp', help='The extensions you wish to fuzz (no dot, comma separated). Default: %(default)s') self.add_true_option('recursive', help='Enables recursive searching (where available). Warning: This may cause significant increases to scan times. Default: %(default)s') self.add_option('extras', default='', help='Any extra options you wish to pass to the tool when it runs. e.g. --dirbuster.extras=\'-s 200,301 --discover-backup\'') From 28637a93964228a3a4a2321e633b23bc87e3e744 Mon Sep 17 00:00:00 2001 From: MrMatch246 <50702646+MrMatch246@users.noreply.github.com> Date: Tue, 23 Sep 2025 20:58:23 +0200 Subject: [PATCH 10/17] disabled dirbuster for now --- autorecon/default-plugins/dirbuster.py | 1 + 1 file changed, 1 insertion(+) diff --git a/autorecon/default-plugins/dirbuster.py b/autorecon/default-plugins/dirbuster.py index a6848e6..a4d4c0a 100644 --- a/autorecon/default-plugins/dirbuster.py +++ b/autorecon/default-plugins/dirbuster.py @@ -41,6 +41,7 @@ class DirBuster(ServiceScan): return False async def run(self, service): + return dot_extensions = ','.join(['.' + x for x in self.get_option('ext').split(',')]) for wordlist in self.get_option('wordlist'): name = os.path.splitext(os.path.basename(wordlist))[0] From 5d1e0180a1d120809f33a80ccf9a25e27dd860af Mon Sep 17 00:00:00 2001 From: MrMatch246 <50702646+MrMatch246@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:15:31 +0200 Subject: [PATCH 11/17] modified dirbuster.py --- autorecon/default-plugins/dirbuster.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/autorecon/default-plugins/dirbuster.py b/autorecon/default-plugins/dirbuster.py index a4d4c0a..a1507e3 100644 --- a/autorecon/default-plugins/dirbuster.py +++ b/autorecon/default-plugins/dirbuster.py @@ -15,7 +15,7 @@ class DirBuster(ServiceScan): def configure(self): self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') self.add_list_option('wordlist', default=[os.path.join(config['data_dir'], 'wordlists', 'dirbuster.txt')], help='The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: %(default)s') - self.add_option('threads', default=4, help='The number of threads to use when directory busting. Default: %(default)s') + self.add_option('threads', default=10, help='The number of threads to use when directory busting. Default: %(default)s') self.add_option('ext', default='txt,html,php,asp,aspx,jsp', help='The extensions you wish to fuzz (no dot, comma separated). Default: %(default)s') self.add_true_option('recursive', help='Enables recursive searching (where available). Warning: This may cause significant increases to scan times. Default: %(default)s') self.add_option('extras', default='', help='Any extra options you wish to pass to the tool when it runs. e.g. --dirbuster.extras=\'-s 200,301 --discover-backup\'') @@ -41,12 +41,11 @@ class DirBuster(ServiceScan): return False async def run(self, service): - return dot_extensions = ','.join(['.' + x for x in self.get_option('ext').split(',')]) for wordlist in self.get_option('wordlist'): name = os.path.splitext(os.path.basename(wordlist))[0] if self.get_option('tool') == 'feroxbuster': - await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "' + self.get_option('ext') + '" -v -k ' + ('' if self.get_option('recursive') else '-n ') + '-q -e -r -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) + await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "' + self.get_option('ext') + '" -v -k ' + ('' if self.get_option('recursive') else '-n --dont-extract-links') + '-q -r -o --auto-bail --auto-tune --status-codes 200,204,301,302,307,308,401,403,405 "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) elif self.get_option('tool') == 'gobuster': await service.execute('gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -x "' + self.get_option('ext') + '" -z -r -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) From a58d757a91931b71194cd2a3532c1083be2372f5 Mon Sep 17 00:00:00 2001 From: MrMatch246 <50702646+MrMatch246@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:30:39 +0200 Subject: [PATCH 12/17] copy plugin checks added --- autorecon/main.py | 48 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/autorecon/main.py b/autorecon/main.py index abbd9eb..5c2049a 100644 --- a/autorecon/main.py +++ b/autorecon/main.py @@ -19,6 +19,26 @@ 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) + +# ----------------------- 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) @@ -33,23 +53,35 @@ else: 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(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')) + shutil.copytree(plugins_src, plugins_dst) + shutil.copytree(wordlists_src, wordlists_dst) else: - 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)): + 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 From 38618196996db48863da2ddfbab6e43c36881428 Mon Sep 17 00:00:00 2001 From: MrMatch246 <50702646+MrMatch246@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:33:29 +0200 Subject: [PATCH 13/17] fixed missing space --- autorecon/default-plugins/dirbuster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autorecon/default-plugins/dirbuster.py b/autorecon/default-plugins/dirbuster.py index a1507e3..2553054 100644 --- a/autorecon/default-plugins/dirbuster.py +++ b/autorecon/default-plugins/dirbuster.py @@ -45,7 +45,7 @@ class DirBuster(ServiceScan): for wordlist in self.get_option('wordlist'): name = os.path.splitext(os.path.basename(wordlist))[0] if self.get_option('tool') == 'feroxbuster': - await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "' + self.get_option('ext') + '" -v -k ' + ('' if self.get_option('recursive') else '-n --dont-extract-links') + '-q -r -o --auto-bail --auto-tune --status-codes 200,204,301,302,307,308,401,403,405 "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) + await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "' + self.get_option('ext') + '" -v -k ' + ('' if self.get_option('recursive') else '-n --dont-extract-links') + ' -q -r -o --auto-bail --auto-tune --status-codes 200,204,301,302,307,308,401,403,405 "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) elif self.get_option('tool') == 'gobuster': await service.execute('gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -x "' + self.get_option('ext') + '" -z -r -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) From a9b9f13790cb3e9791a74fb3c6a5461cd77891ac Mon Sep 17 00:00:00 2001 From: MrMatch246 <50702646+MrMatch246@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:36:49 +0200 Subject: [PATCH 14/17] fixed wrong order --- autorecon/default-plugins/dirbuster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autorecon/default-plugins/dirbuster.py b/autorecon/default-plugins/dirbuster.py index 2553054..3b5aaeb 100644 --- a/autorecon/default-plugins/dirbuster.py +++ b/autorecon/default-plugins/dirbuster.py @@ -45,7 +45,7 @@ class DirBuster(ServiceScan): for wordlist in self.get_option('wordlist'): name = os.path.splitext(os.path.basename(wordlist))[0] if self.get_option('tool') == 'feroxbuster': - await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "' + self.get_option('ext') + '" -v -k ' + ('' if self.get_option('recursive') else '-n --dont-extract-links') + ' -q -r -o --auto-bail --auto-tune --status-codes 200,204,301,302,307,308,401,403,405 "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) + await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "' + self.get_option('ext') + '" -v -k ' + ('' if self.get_option('recursive') else '-n --dont-extract-links') + ' -q -r --auto-bail --auto-tune --status-codes 200,204,301,302,307,308,401,403,405 -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) elif self.get_option('tool') == 'gobuster': await service.execute('gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -x "' + self.get_option('ext') + '" -z -r -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) From 0d1c09d2aeeb5ab84988820800c6e4c9b2768fbb Mon Sep 17 00:00:00 2001 From: MrMatch246 <50702646+MrMatch246@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:39:19 +0200 Subject: [PATCH 15/17] removed auto-tune --- autorecon/default-plugins/dirbuster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autorecon/default-plugins/dirbuster.py b/autorecon/default-plugins/dirbuster.py index 3b5aaeb..501b891 100644 --- a/autorecon/default-plugins/dirbuster.py +++ b/autorecon/default-plugins/dirbuster.py @@ -45,7 +45,7 @@ class DirBuster(ServiceScan): for wordlist in self.get_option('wordlist'): name = os.path.splitext(os.path.basename(wordlist))[0] if self.get_option('tool') == 'feroxbuster': - await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "' + self.get_option('ext') + '" -v -k ' + ('' if self.get_option('recursive') else '-n --dont-extract-links') + ' -q -r --auto-bail --auto-tune --status-codes 200,204,301,302,307,308,401,403,405 -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) + await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "' + self.get_option('ext') + '" -v -k ' + ('' if self.get_option('recursive') else '-n --dont-extract-links') + ' -q -r --auto-bail --status-codes 200,204,301,302,307,308,401,403,405 -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) elif self.get_option('tool') == 'gobuster': await service.execute('gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -x "' + self.get_option('ext') + '" -z -r -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"' + (' ' + self.get_option('extras') if self.get_option('extras') else '')) From 8191d026fa0bdaec1fc29bb98e5bf52f89c21706 Mon Sep 17 00:00:00 2001 From: MrMatch246 <50702646+MrMatch246@users.noreply.github.com> Date: Tue, 21 Oct 2025 17:42:46 +0200 Subject: [PATCH 16/17] added target address to nmap filename --- autorecon/default-plugins/portscan-all-tcp-ports.py | 4 ++-- autorecon/default-plugins/portscan-guess-tcp-ports.py | 4 ++-- autorecon/default-plugins/portscan-top-100-udp-ports.py | 4 ++-- autorecon/default-plugins/portscan-top-tcp-ports.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/autorecon/default-plugins/portscan-all-tcp-ports.py b/autorecon/default-plugins/portscan-all-tcp-ports.py index c6dc70e..b671b17 100644 --- a/autorecon/default-plugins/portscan-all-tcp-ports.py +++ b/autorecon/default-plugins/portscan-all-tcp-ports.py @@ -20,11 +20,11 @@ class AllTCPPortScan(PortScan): if target.ports: if target.ports['tcp']: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p ' + target.ports['tcp'] + ' -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p ' + target.ports['tcp'] + ' -oN "{scandir}/' + str(target.address) + '_full_tcp_nmap.txt" -oX "{scandir}/xml/' + str(target.address) + '_full_tcp_nmap.xml" {address}', blocking=False) else: return [] else: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p- -oN "{scandir}/' + str(target.address) + '_full_tcp_nmap.txt" -oX "{scandir}/xml/' + str(target.address) + '_full_tcp_nmap.xml" {address}', blocking=False) services = [] while True: line = await stdout.readline() diff --git a/autorecon/default-plugins/portscan-guess-tcp-ports.py b/autorecon/default-plugins/portscan-guess-tcp-ports.py index 1919fff..ca87e99 100644 --- a/autorecon/default-plugins/portscan-guess-tcp-ports.py +++ b/autorecon/default-plugins/portscan-guess-tcp-ports.py @@ -15,11 +15,11 @@ class GuessPortScan(PortScan): async def run(self, target): if target.ports: if target.ports['tcp']: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p ' + target.ports['tcp'] + ' -oN "{scandir}/_custom_ports_tcp_nmap.txt" -oX "{scandir}/xml/_custom_ports_tcp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p ' + target.ports['tcp'] + ' -oN "{scandir}/' + str(target.address) + '_custom_ports_tcp_nmap.txt" -oX "{scandir}/xml/' + str(target.address) + '_custom_ports_tcp_nmap.xml" {address}', blocking=False) else: return [] else: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/' + str(target.address) + '_quick_tcp_nmap.txt" -oX "{scandir}/xml/' + str(target.address) + '_quick_tcp_nmap.xml" {address}', blocking=False) insecure_ports = { '20':'ftp', '21':'ftp', '22':'ssh', '23':'telnet', '25':'smtp', '53':'domain', '69':'tftp', '79':'finger', '80':'http', '88':'kerberos', '109':'pop3', '110':'pop3', '111':'rpcbind', '119':'nntp', '135':'msrpc', '139':'netbios-ssn', '143':'imap', '161':'snmp', '220':'imap', '389':'ldap', '433':'nntp', '445':'smb', '587':'smtp', '631':'ipp', '873':'rsync', '1098':'java-rmi', '1099':'java-rmi', '1433':'mssql', '1521':'oracle', '2049':'nfs', '2483':'oracle', '3020':'smb', '3306':'mysql', '3389':'rdp', '3632':'distccd', '5060':'asterisk', '5500':'vnc', '5900':'vnc', '5985':'wsman', '6379':'redis', '8080':'http-proxy', '27017':'mongod', '27018':'mongod', '27019':'mongod' diff --git a/autorecon/default-plugins/portscan-top-100-udp-ports.py b/autorecon/default-plugins/portscan-top-100-udp-ports.py index 7f28de3..3cd724c 100644 --- a/autorecon/default-plugins/portscan-top-100-udp-ports.py +++ b/autorecon/default-plugins/portscan-top-100-udp-ports.py @@ -17,11 +17,11 @@ class Top100UDPPortScan(PortScan): if os.getuid() == 0 or config['disable_sanity_checks']: if target.ports: if target.ports['udp']: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --osscan-guess -p ' + target.ports['udp'] + ' -oN "{scandir}/_custom_ports_udp_nmap.txt" -oX "{scandir}/xml/_custom_ports_udp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --osscan-guess -p ' + target.ports['udp'] + ' -oN "{scandir}/' + str(target.address) + '_custom_ports_udp_nmap.txt" -oX "{scandir}/xml/' + str(target.address) + '_custom_ports_udp_nmap.xml" {address}', blocking=False) else: return [] else: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --top-ports 100 -oN "{scandir}/_top_100_udp_nmap.txt" -oX "{scandir}/xml/_top_100_udp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --top-ports 100 -oN "{scandir}/' + str(target.address) + '_top_100_udp_nmap.txt" -oX "{scandir}/xml/' + str(target.address) + '_top_100_udp_nmap.xml" {address}', blocking=False) services = [] while True: line = await stdout.readline() diff --git a/autorecon/default-plugins/portscan-top-tcp-ports.py b/autorecon/default-plugins/portscan-top-tcp-ports.py index d332812..71faa73 100644 --- a/autorecon/default-plugins/portscan-top-tcp-ports.py +++ b/autorecon/default-plugins/portscan-top-tcp-ports.py @@ -21,7 +21,7 @@ class QuickTCPPortScan(PortScan): else: traceroute_os = ' -A --osscan-guess' - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -oN "{scandir}/' + str(target.address) + '_quick_tcp_nmap.txt" -oX "{scandir}/xml/' + str(target.address) + '_quick_tcp_nmap.xml" {address}', blocking=False) services = await target.extract_services(stdout) for service in services: From 7c32153e0a36c376800c1f791a8d63c8a66d6b56 Mon Sep 17 00:00:00 2001 From: MrMatch246 <50702646+MrMatch246@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:50:25 +0200 Subject: [PATCH 17/17] add --import-nmap flag to skip port scanning from existing XML Adds --import-nmap XML_FILE which parses an existing nmap XML scan result and feeds the discovered hosts/services directly into the service-scan plugin pipeline, skipping all port scan plugins. - parse_nmap_xml() extracts open ports and service names from the XML - When no targets are specified, all hosts in the XML become targets - When explicit targets are given, they are looked up in the XML; any target not found falls back to normal port scanning with a warning - Semaphore setup, port_scan_plugin_count, and main loop task counter are updated to behave like --force-services (service-scans only) Co-Authored-By: Claude Sonnet 4.6 --- autorecon/config.py | 3 +- autorecon/main.py | 131 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 128 insertions(+), 6 deletions(-) diff --git a/autorecon/config.py b/autorecon/config.py index f75a091..78814d8 100644 --- a/autorecon/config.py +++ b/autorecon/config.py @@ -77,5 +77,6 @@ config = { 'max_plugin_target_instances': None, 'max_plugin_global_instances': None, 'accessible': False, - 'verbose': 0 + 'verbose': 0, + 'imported_nmap_services': None } diff --git a/autorecon/main.py b/autorecon/main.py index 5c2049a..702f20d 100644 --- a/autorecon/main.py +++ b/autorecon/main.py @@ -1,6 +1,7 @@ #!/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: @@ -38,6 +39,67 @@ def needs_update(src, 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) @@ -493,7 +555,7 @@ async def service_scan(plugin, service): async def generate_report(plugin, targets): semaphore = autorecon.service_scan_semaphore - if not config['force_services']: + if not config['force_services'] and config.get('imported_nmap_services') is None: semaphore = await get_semaphore(autorecon) async with semaphore: @@ -571,6 +633,49 @@ async def scan_target(target): 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': @@ -621,7 +726,7 @@ async def scan_target(target): timed_out = True break - if not config['force_services']: + if not config['force_services'] and config.get('imported_nmap_services') is None: # Extract Services services = [] @@ -950,6 +1055,7 @@ async def run(): 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') @@ -1385,7 +1491,7 @@ async def run(): errors = True if not errors: - if config['force_services']: + 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']) @@ -1443,6 +1549,21 @@ async def run(): 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: @@ -1542,7 +1663,7 @@ async def run(): 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']: + 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']: @@ -1621,7 +1742,7 @@ async def run(): 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 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: