449 lines
16 KiB
Python
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)
|
|
|
|
|