SolidScan/solidScan.py
2024-05-13 21:24:52 -06:00

449 lines
16 KiB
Python

"""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 <https://www.gnu.org/licenses/>.
"""
# 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)