import sys import shlex import pickle import socket import inspect import pkgutil import subprocess from pathlib import Path from collections import defaultdict import cmd2 from cmd2.ansi import style import recon def get_scans(): """ Iterates over the recon package and its modules to find all of the *Scan classes. * A contract exists here that says any scans need to end with the word scan in order to be found by this function. Returns: dict() containing mapping of {modulename: classname} for all potential recon-pipeline commands """ scans = defaultdict(list) # recursively walk packages; import each module in each package for loader, module_name, is_pkg in pkgutil.walk_packages(recon.__path__, prefix="recon."): _module = loader.find_module(module_name).load_module(module_name) globals()[module_name] = _module # walk all modules, grabbing classes that we've written and add them to the classlist set for name, obj in inspect.getmembers(sys.modules[__name__]): if inspect.ismodule(obj) and not name.startswith("_"): for subname, subobj in inspect.getmembers(obj): if inspect.isclass(subobj) and subname.lower().endswith("scan"): scans[subname].append(name) return scans # tool definitions for the auto-installer tools = { "go": {"installed": False, "dependencies": None, "commands": ["apt-get install -y -q golang"]}, "luigi": { "installed": False, "dependencies": ["pipenv", "luigi-service"], "commands": ["pipenv install luigi"], }, "luigi-service": { "installed": False, "dependencies": None, "commands": [ f"cp {str(Path(__file__).parent / 'luigid.service')} /lib/systemd/system/luigid.service", "systemctl start luigid.service", "systemctl enable luigid.service", ], }, "pipenv": { "installed": False, "dependencies": None, "commands": ["apt-get install -y -q pipenv"], }, "masscan": { "installed": False, "dependencies": None, "commands": [ "git clone https://github.com/robertdavidgraham/masscan /tmp/masscan", "make -s -j -C /tmp/masscan", "mv /tmp/bin/masscan /usr/bin/masscan", "rm -rf /tmp/masscan", ], }, "amass": { "installed": False, "dependencies": None, "commands": ["apt-get install -y -q amass"], }, "aquatone": { "installed": False, "dependencies": None, "commands": [ "mkdir /tmp/aquatone", "wget -q https://github.com/michenriksen/aquatone/releases/download/v1.7.0/aquatone_linux_amd64_1.7.0.zip -O /tmp/aquatone/aquatone.zip", "unzip /tmp/aquatone/aquatone.zip -d /tmp/aquatone", "mv /tmp/aquatone/aquatone /usr/bin/aquatone", "rm -rf /tmp/aquatone", ], }, "corscanner": { "installed": False, "dependencies": None, "commands": [ "git clone https://github.com/chenjj/CORScanner.git /opt/CORScanner", "pip install -r /opt/CORScanner/requirements.txt", "pip install future", ], }, "gobuster": { "installed": False, "dependencies": ["go"], "commands": [ "go get github.com/OJ/gobuster", "(cd ~/go/src/github.com/OJ/gobuster && go build && go install)", ], "shell": True, }, "tko-subs": { "installed": False, "dependencies": ["go"], "commands": [ "go get github.com/anshumanbh/tko-subs", "(cd ~/go/src/github.com/anshumanbh/tko-subs && go build && go install)", ], "shell": True, }, "subjack": { "installed": False, "dependencies": ["go"], "commands": [ "go get github.com/haccer/subjack", "(cd ~/go/src/github.com/haccer/subjack && go build && go install)", ], "shell": True, }, "webanalyze": { "installed": False, "dependencies": ["go"], "commands": [ "go get github.com/rverton/webanalyze", "(cd ~/go/src/github.com/rverton/webanalyze && go build && go install)", ], "shell": True, }, "recursive-gobuster": { "installed": False, "dependencies": None, "commands": [ "git clone https://github.com/epi052/recursive-gobuster.git /opt/recursive-gobuster", "ln -s /opt/recursive-gobuster/recursive-gobuster.pyz /usr/local/bin", ], }, } # options for ReconShell's 'scan' command scan_parser = cmd2.Cmd2ArgumentParser() scan_parser.add_argument("scantype", choices_function=get_scans) scan_parser.add_argument( "--target-file", completer_method=cmd2.Cmd.path_complete, help="file created by the user that defines the target's scope; list of ips/domains", ) scan_parser.add_argument( "--exempt-list", completer_method=cmd2.Cmd.path_complete, help="list of blacklisted ips/domains" ) scan_parser.add_argument( "--wordlist", completer_method=cmd2.Cmd.path_complete, help="path to wordlist used by gobuster" ) scan_parser.add_argument( "--interface", choices_function=lambda: [x[1] for x in socket.if_nameindex()], help="which interface masscan should use", ) scan_parser.add_argument( "--recursive", action="store_true", help="whether or not to recursively gobust" ) scan_parser.add_argument("--rate", help="rate at which masscan should scan") scan_parser.add_argument( "--top-ports", help="ports to scan as specified by nmap's list of top-ports (only meaningful to around 5000)", ) scan_parser.add_argument( "--ports", help="port specification for masscan (all ports example: 1-65535,U:1-65535)" ) scan_parser.add_argument( "--threads", help="number of threads for all of the threaded applications to use" ) scan_parser.add_argument("--scan-timeout", help="scan timeout for aquatone") scan_parser.add_argument("--proxy", help="proxy for gobuster if desired (ex. 127.0.0.1:8080)") scan_parser.add_argument("--extensions", help="list of extensions for gobuster (ex. asp,html,aspx)") scan_parser.add_argument( "--local-scheduler", action="store_true", help="use the local scheduler instead of the central scheduler (luigid)", ) scan_parser.add_argument( "--verbose", action="store_true", help="shows debug messages from luigi, useful for troubleshooting", ) # options for ReconShell's 'install' command install_parser = cmd2.Cmd2ArgumentParser() install_parser.add_argument( "tool", help="which tool to install", choices=list(tools.keys()) + ["all"] ) class ReconShell(cmd2.Cmd): prompt = "recon-pipeline> " @cmd2.with_argparser(scan_parser) def do_scan(self, args): """ Scan something. Possible scans include AmassScan CORScannerScan GobusterScan SearchsploitScan ThreadedNmapScan WebanalyzeScan AquatoneScan FullScan MasscanScan SubjackScan TKOSubsScan """ scans = get_scans() command = ["luigi", "--module", scans.get(args.scantype)[0]] command.extend(args.__statement__.arg_list) sentry = False if args.verbose: command.pop( command.index("--verbose") ) # verbose is not a luigi option, need to remove it subprocess.run(command) else: # suppress luigi messages in favor of less verbose/cleaner output proc = subprocess.Popen(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE) while True: output = proc.stderr.readline() if not output and proc.poll() is not None: break if not output: continue output = output.decode() if "===== Luigi Execution Summary =====" in output: self.poutput() sentry = True # block below used for printing status messages if sentry: self.poutput(style(output.strip(), fg="bright_blue")) elif output.startswith("INFO") and output.split()[-1] == "PENDING": words = output.split() self.poutput(style(f"[-] {words[5].split('_')[0]} queued", fg="bright_white")) elif output.startswith("INFO") and "running" in output: words = output.split() # output looks similar to , pid=3938074) running MasscanScan( # want to grab the index of the luigi task running scantypeidx = words.index("running") + 1 scantype = words[scantypeidx].split("(", 1)[0] self.poutput(style(f"[*] {scantype} running...", fg="bright_yellow")) elif output.startswith("INFO") and output.split()[-1] == "DONE": words = output.split() self.poutput( style(f"[+] {words[5].split('_')[0]} complete!", fg="bright_green") ) self.async_alert( style( "If anything went wrong, rerun your command with --verbose to enable debug statements.", fg="cyan", dim=True, ) ) @cmd2.with_argparser(install_parser) def do_install(self, args): """ Install any/all of the libraries/tools necessary to make the recon-pipeline function. """ global tools cachedir = Path.home() / ".cache/" cachedir.mkdir(parents=True, exist_ok=True) persistent_tool_dict = cachedir / ".tool-dict.pkl" if args.tool == "all": for tool in tools.keys(): self.do_install(tool) return if persistent_tool_dict.exists(): tools = pickle.loads(persistent_tool_dict.read_bytes()) if tools.get(args.tool).get("dependencies"): for dependency in tools.get(args.tool).get("dependencies"): if tools.get(dependency).get("installed"): continue self.poutput( style( f"{args.tool} has an unmet dependency; installing {dependency}", fg="blue", bold=True, ) ) self.do_install(dependency) if tools.get(args.tool).get("installed"): return self.poutput(style(f"{args.tool} is already installed.", fg="yellow")) else: self.poutput(style(f"Installing {args.tool}...", fg="blue", bold=True)) for command in tools.get(args.tool).get("commands"): if tools.get(args.tool).get("shell") and command.startswith( "(" ): # go installs use subshells (...) subprocess.run(command, shell=True) else: subprocess.run(shlex.split(command)) tools[args.tool]["installed"] = True self.poutput(style(f"{args.tool} installed!", fg="green", bold=True)) pickle.dump(tools, persistent_tool_dict.open("wb")) if __name__ == "__main__": rs = ReconShell(persistent_history_file="~/.reconshell_history") sys.exit(rs.cmdloop())