"""solidScan.py An automated scanner to use in environments where you can be as loud as you want and a baseball bat is as good as a lockpick. Copyright (C) 2021 J.C. Boysha This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, Version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ # Necssary Imports import nmap3 import masscan import json import sys import argparse import os import subprocess def automatedScans(scanConf): """ Run all of the automated scan declarations in a configuration or scanfile. Keyword Argument: scanConf: An array of scan objects, or a json file with scan configurations present. """ # TODO write code for the automated scans. def scanMass(scope, ports, rate): """ Perform a masscan of the network scope provided to determine what hosts are listening on what ports. Keyword Arguments: scope - The scope of the network to be scanned in CIDR format ports - The range of ports to be scanned in nmap format rate - The rate of the scan in Kpps """ if not verbose: subprocess.call(["masscan","-oJ","masscan.json","--rate" ,str(rate),"-p", str(ports), str(scope) ,"--wait","0"],stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) print("[#] Masscan complete!") elif verbose: subprocess.call(["masscan","-oJ","masscan.json","--rate" ,str(rate),"-p", str(ports), str(scope) ,"--wait","0"]) hosts = [] try: with open("masscan.json", "r") as scanResult: results = json.load(scanResult) for result in results: # Check to see if host already exists and append there. for host in hosts: if result['ip'] == host[0]: for port in result['ports']: if port['proto'] == "tcp": host[1].append(str(port['port'])) # If the host doesn't exist, move on else: newHost = [] newHost.append(result['ip']) newHost.append([]) for port in result['ports']: if port['proto'] == "tcp": newHost[1].append(str(port['port'])) hosts.append(newHost) if len(hosts) <= 0: newHost = [] newHost.append(result['ip']) newHost.append([]) for port in result['ports']: if port['proto'] == "tcp": newHost[1].append(str(port['port'])) hosts.append(newHost) if not debug: os.remove("masscan.json") hostsDict = {} for host, ports in hosts: hostsDict.setdefault(host, ','.join(ports)) for host in hostsDict: hostsDict[host] = ','.join(set(hostsDict[host].split(','))) return hostsDict except: print("An error occured with the masscan, were there results?") exit(-42) def getCpes(): """ Output the Cpes for each host, with software as well. Optional Keyword Argument: results: a well-formed python3-nmap JSON output """ cpeResults = {} try: with open('cpesDump.json', 'r') as cpesJson: scanResults = json.load(cpesJson) except: print("[!] Unable to load cpeDump.json. Failing") exit(-217) try: with open(cpe_dictionary, 'r') as cpeDictionary: cpeDict = json.load(cpeDictionary) except: print("[!] Unable to load cpe Dictionary. Failing") exit(-218) for host in scanResults: hostPorts = {} if str(host) != 'runtime' and str(host) != 'stats': print("[#] Host : " + str(host)) try: print("[-] Host OS: " + str(scanResults[host]['osmatch'])) except: print("[!]OS not Identified") for port in scanResults[host]['ports']: # try: # try: cpe = str(port['cpe'][0]['cpe']) print("[-] Open port at: "+ str(port['portid'])) print("[>] Protocol: " + str(port['protocol'])) print("[>] Port Fingerprint (cpe2.2): " + cpe) vulns = [] for cpeConv in cpeDict: if cpe in str(cpeConv): cpe23 = cpeDict[cpeConv]['cpe23Uri'] vulns.extend(cpeDict[cpeConv]['vulns']) try: vulns = list(dict.fromkeys(vulns)) except: vulns = vulns if cpe23 is None: cpe23 = "" print("\t[>] " + str(len(vulns)) + " vulnerabilities" + " found") hostPorts.update({str(port['portid']): {'num':str(port['portid']), 'protocol':str(port['protocol']), 'cpe22': cpe, 'cpe23' : cpe23, 'vulns' : vulns}}) """except: print("[-] Open port at: "+ str(port['portid'])) print("[>] Protocol: " + str(port['protocol'])) print("[!] Port could not be fingerprinted.") hostPorts.update({str(port['portid']): {'num':str(port['portid']), 'protocol':str(port['protocol']), 'cpe22': "Not Found", "vulns": []}})""" """except Exception as e: print(e) continue""" cpeResults.update({str(host): {'Ports': hostPorts}}) if not debug: os.remove('cpesDump.json') return cpeResults def nmapScan(sockets): """ Perform an nmap scan against a list of IPs. Keyword Argument: sockets: An array of tuples as follows: (Ip Address of Host, nmap formatted string of ports to scan). """ scanResults = {} nm=nmap3.Nmap() for sock in sockets: print("[#] Host found: " + sock[0]) print("[-] Ports open: " + sock[1]) for sock in sockets: print("[-] Scanning " + sock[0] + " on ports: " + sock[1]) results = nm.nmap_version_detection(sock[0], args="-p " + sock[1]) if verbose is not None and verbose: print(json.dumps(results, sort_keys=True, indent=4)) scanResults.update(results) if not fingerprint: print("[!] Writing results to " + outpath + outfile + ".") try: with open(outpath + outfile, 'a') as output: json.dump(scanResults, output, sort_keys=True, indent=4) except: with open(outpath + outfile, 'w') as output: json.dump(scanResults, output, sort_keys=True, indent=4) else: with open('cpesDump.json', 'w') as output: json.dump(scanResults, output, sort_keys=True, indent=4) def getConfiguration (filename = "data/conf.json"): """ Import and initialize variables based on the configuration file. If a scanConfiguration is provided, run the automated scan(s). Keyword Argument: filename - The name of the configuration file. (Default = data/conf.json) """ global scanConf global rate global ports global scope global outfile global outpath global cpe_dictionary global fingerprint try: with open(filename) as config: configuration = json.load(config) try: scanConf = configuration['scans'] if isinstance(scanConf, list): automatedScans(scanConf) except: scanConf = None except: print("[!] Running without Configuration file.") if args.ports is not None: try: ports = args.ports print("[#] Ports: " + ports) except: print("Ports must be provided in nmap readable format. I.E.\ 0-1024; 1,22,80-443; etc. ") exit(-3) else: try: ports = configuration['ports'] print("[#] Ports: " + ports) except: print("Error with Ports in Configuration file, please correct") exit(-2) if args.rate is not None: try: rate = int(args.rate) print("[#] Rate: " + str(rate)) if not isinstance(rate, int): error = -5 raise Exception("Rate must be an integer number.") if (rate > 150000 and args.ignore is None): error = -7 raise Exception("Overlimit failure. " + str(rate) + " is \ very large. Please use --ignore or -i to \ continue.") elif (rate < 1000 and args.ignore is None): error = -9 raise Exception("Underlimit failre. " + str(rate) + " is \ very low, scan may take an extreme \ amount of time. Please use --ignore or \ -i to continue") except Exception as e: print(e) exit(error) else: try: rate = configuration['rate'] print("[#] Rate: " + str(rate)) if not isinstance(rate, int): error = -4 raise Exception("Rate must be an integer number.") if (rate > 150000 and args.ignore is None): error = -6 raise Exception("Overlimit failure. " + rate + " is very \ large. Please use --ignore or -i to \ continue.") elif (rate < 1000 and args.ignore is None): error = -8 raise Exception("Underlimit failre. " + rate + " is very \ low, scan may take an extreme amount of \ time. Please use --ignore or -i to \ continue") except Exception as e: print("Error in configuration file \n" + e) exit(error) if args.scope is not None: try: scope = args.scope print("[#] Scope: " + scope) if scope[-3] != '/' and scope[-2] != '/': raise Exception("Scope Error") except: print("Scope must be provided in CIDR format. I.E. " + "127.0.0.0/24 to indicate a 256 host subnet " + "of 127.0.0.0.") exit(-11) else: try: scope = configuration['scope'] print("[#] Scope: " + scope) if scope[-3] != '/' and scope[-2] != '/': raise Exception("Scope Error") except: print("Scope must be provided in CIDR format. I.E. " + "127.0.0.0/24 to indicate a 256 host subnet of " + "127.0.0.0. Please check configuration file.") exit(-10) if args.outfile is not None: try: outfile = args.outfile print("[#] Outfile: " + outfile) except: print("Outfile must be a string name of a file") exit(-13) else: try: outfile = configuration['outfile'] print("[#] Outfile: " + outfile) except: print("Outfile must be a string name of a file. Please check" + " configuration file.") exit(-12) if args.outpath is not None: try: outpath = args.outpath if not os.path.exists(outpath): raise Exception("Scope Error") except: print("Outpath must be set to a valid path. Please verify the " + "path exists and try again.") exit(-15) else: try: outpath = configuration['outpath'] if not os.path.exists(outpath): raise Exception("Scope Error") except: print("Outpath must be set to a valid path. Please verify the" + " path exists and try again. Double check the " + " configuration file.") exit(-14) try: cpe_dictionary = configuration['cpe-dictionary-file'] try: open(cpe_dictionary, 'r') except: raise Exception except: print("Must have a valid CPE Dictionary file. Please correct \ config file") if __name__ == "__main__": global verbose global debug parser = argparse.ArgumentParser() parser.add_argument("--rate","-r", help="The rate masscan can use to scan \ the network in Kpps. (Default is 50000)") parser.add_argument("--scope","-s", help="The scope of the network to be \ scanned in CIDR format(Default is 127.0.0.0/24)") parser.add_argument("--ports","-p", help="The ports, in nmap format, to \ be scanned on each host in the scope. (Default is \ 0-1024)") parser.add_argument("--scanConf","-S", help="Path to a preconfigured \ scan file in json format. (Default is not used)") parser.add_argument("--conf","-c", help="Path to the configuration file \ (Default is ./conf.json)") parser.add_argument("--ignore", "-i", help="Suppress all warnings and \ alerts") parser.add_argument("--outpath", "-op", help="Path where the output file \ will be saved.") parser.add_argument("--outfile", "-of", help="Name of the output file. \ The file will be in JSON format.") parser.add_argument("--debug", "-d", help="Don't delete working files." , action="store_true") parser.add_argument("--verbose", "-v", help="Increase verbosity of script" , action="store_true") parser.add_argument("--fingerprint", "-f", help="Get an output of the CPEs \ for all scanned systems.", action="store_true") args = parser.parse_args() try: if scanConf is not None: automatedScans(scanConf) except: print("[!] scanConf not set.") if args.debug: debug = True else: debug = False if args.verbose: verbose = True else: verbose = False if args.fingerprint: fingerprint = True else: fingerprint = False if args.conf is not None: try: getConfiguration(args.conf) except: print("Configuration file failed to load. \n Please provide path \ to configuration file (relative or absolute)") exit(-1) else: getConfiguration() msRes = scanMass(scope, ports, rate) targets = [] for res in msRes: targets.append((res, msRes[res])) if fingerprint is not None and fingerprint: nmapScan(targets) cpeResults = getCpes() with open(outfile, 'w') as out: json.dump(cpeResults, out, indent=4, sort_keys=True) else: nmapScan(targets)