diff --git a/.todos b/.todos new file mode 100644 index 0000000..69307ef --- /dev/null +++ b/.todos @@ -0,0 +1,7 @@ +- Add concurrency *COMPLETED 091024* + - sniffer + - specify threads + - concurrent futures + + +- Add extra error handling and logging *COMPLETED 091024* diff --git a/CHANGELOG.md b/CHANGELOG.md index 552eb97..16a4c48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # sharesniffer Change Log +## [0.1-b.9] = 2024-09-05 +### changed +- New Argument (--nmapdatadir): Users can specify their own NSE script directory using this argument. If they don't, the script will look for it in default directories. +- Directory Check: If the user provides a directory that doesn’t exist, the script will notify them and exit. +- Dynamic NSE Script Path Detection: If no custom path is given, the script will try to locate it using common paths (/usr/local/share/nmap/scripts, /usr/share/nmap/scripts). +- Error Handling for Missing Directories: If the script cannot find the NSE script directory, it will exit with an error message. +- Created .todos file + ## [0.1-b.8] = 2018-07-02 ### fixed - bug fix for traceback TypeError in get_nfs_shares function (pyneda pr d0f5888) diff --git a/README.md b/README.md index ba3a90b..d1111ec 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,10 @@ python sharesniffer.py -l 4 --hosts 192.168.56.0/24 -e 192.168.56.1,192.168.56.2 ### Download ```shell -$ git clone https://github.com/shirosaidev/sharesniffer.git +$ git clone https://github.com/FashyGainz/sharesniffer.git $ cd sharesniffer ``` -[Download latest version](https://github.com/shirosaidev/sharesniffer/releases/latest) +[Download latest version](https://github.com/FashyGainz/sharesniffer/releases/latest) ### CLI Options diff --git a/sharesniffer.py b/sharesniffer.py index 6ed8e6c..ecea7f6 100644 --- a/sharesniffer.py +++ b/sharesniffer.py @@ -30,15 +30,19 @@ import subprocess import os import sys +import ipaddress from random import randint +from concurrent.futures import ThreadPoolExecutor, as_completed SHARESNIFFER_VERSION = '0.1-b.8' __version__ = SHARESNIFFER_VERSION +import ipaddress # Add this import to handle CIDR blocks + class sniffer: - def __init__(self, hosts=None, excludehosts=None, nfs=False, smb=False, smbuser='guest', smbpass=''): + def __init__(self, hosts=None, excludehosts=None, nfs=False, smb=False, smbuser='guest', smbpass='', max_workers=10, speedlevel=4): self.hosts = hosts self.nfs = nfs self.smb = smb @@ -46,75 +50,134 @@ def __init__(self, hosts=None, excludehosts=None, nfs=False, smb=False, smbuser= self.smbpass = smbpass self.excludehosts = excludehosts self.nm = nmap.PortScanner() - if args.speedlevel == 5: - t = "5" - min_p = "200" - max_p = "512" - max_ret = "1" - min_r = "200" - max_r = "512" - host_t = "1" - elif args.speedlevel == 4: - t = "4" - min_p = "100" - max_p = "256" - max_ret = "1" - min_r = "100" - max_r = "256" - host_t = "2" - elif args.speedlevel == 3: - t = "3" - min_p = "50" - max_p = "128" - max_ret = "2" - min_r = "50" - max_r = "128" - host_t = "3" - else: - t = "3" - min_p = "50" - max_p = "128" - max_ret = "2" - min_r = "50" - max_r = "128" - host_t = "3" - - if self.excludehosts: - self.nmapargs = '--exclude ' + self.excludehosts + \ - ' -n -T'+t+' -Pn -PS111,445 --open --min-parallelism '+min_p+' --max-parallelism '+max_p+' ' \ - '--max-retries '+max_ret+' --min-rate '+min_r+' --max-rate '+max_r+' --host-timeout '+host_t - else: - self.nmapargs = '-n -T'+t+' -Pn -PS111,445 --open --min-parallelism '+min_p+' --max-parallelism '+max_p+' ' \ - '--max-retries '+max_ret+' --min-rate '+min_r+' --max-rate '+max_r+' --host-timeout '+host_t + self.max_workers = max_workers + self.speedlevel = speedlevel + self.nmapargs = f'-T{self.speedlevel}' # Initialize nmapargs based on speed level - def get_nfs_shares(self, hostlist): - nfsshares = [] - for host in hostlist: - shares = {'host': host, 'openshares': [], 'closedshares': []} - output = self.nm.scan(host, '111', - arguments='%s --datadir %s --script %s/nfs-showmount.nse,%s/nfs-ls.nse' - % (self.nmapargs, nmapdatadir, nmapdatadir, nmapdatadir)) - logger.debug('nm scan output: ' + str(output)) + def get_host_ranges(self): + """ Retrieves the network ranges for scanning. """ + cidr = [] + for ifacename in netifaces.interfaces(): try: - nfsshowmount = output['scan'][host]['tcp'][111]['script']['nfs-showmount'].strip().split('\n') - nfsls = output['scan'][host]['tcp'][111]['script']['nfs-ls'].strip().split('\n') + addrs = netifaces.ifaddresses(ifacename) + addr = addrs[netifaces.AF_INET] + ip = addr[0]['addr'] except KeyError: - print('%s PORT 111/tcp OPEN (rpcbind) but no results from nse script' % host) continue - openshares = [] - closedshares = [] - sharedict = {'sharename': nfsshowmount[0].strip().split(' ')[0]} - if re.search(r'ERROR: Mount failed: Permission denied', nfsls[4]): - closedshares.append(sharedict) + if ip == '127.0.0.1' or ip == 'fe80::1%lo0': + continue + try: + netmask = addr[0]['netmask'].split('.') + except KeyError: + cidr.append(ip + '/' + '32') continue + ipaddr = ip.split('.') + net_start = [str(int(ipaddr[x]) & int(netmask[x])) + for x in range(0, 4)] + binary_str = '' + for octet in netmask: + binary_str += bin(int(octet))[2:].zfill(8) + net_size = str(len(binary_str.rstrip('0'))) + cidr.append('.'.join(net_start) + '/' + net_size) + hostlist = ' '.join(cidr) + return hostlist + + def expand_cidr(self, hosts): + """ Expands a CIDR range into individual IPs. """ + expanded_ips = [] + for host in hosts.split(): + if '/' in host: # If CIDR notation is present + try: + ip_network = ipaddress.ip_network(host, strict=False) + expanded_ips.extend([str(ip) for ip in ip_network.hosts()]) + except ValueError as e: + logger.error(f"Invalid CIDR notation: {host} - {e}") else: - openshares.append(sharedict) - for share in openshares: - shares['openshares'].append(share['sharename']) - for share in closedshares: - shares['closedshares'].append(share['sharename']) - nfsshares.append(shares) - return nfsshares + expanded_ips.append(host) + return expanded_ips + + def scan_host(self, host): + """ Scans a single host and returns NFS/SMB open ports. """ + open_ports = {'nfs': False, 'smb': False} + logger.info(f"Scanning host {host}...") + + # Perform the Nmap scan + self.nm.scan(host, '111,445', arguments=self.nmapargs) + + # Check if the host is in the scan results + if host not in self.nm.all_hosts(): + logger.warning(f"No scan results for host: {host}") + return host, open_ports + + # Proceed only if the host exists in the scan results + for proto in self.nm[host].all_protocols(): + lport = self.nm[host][proto].keys() + for port in lport: + if self.nm[host][proto][port]['state'] == 'open': + if port == 111: + open_ports['nfs'] = True + if port == 445: + open_ports['smb'] = True + return host, open_ports + + def sniff_hosts(self): + """ Sniffs for NFS/SMB shares on the network. """ + hostlist_nfs = [] + hostlist_smb = [] + if not self.hosts: + logger.info('No hosts specified, finding your network info') + hosts = self.get_host_ranges() + logger.info('Networks found: %s', hosts) + else: + hosts = self.hosts + + # Expand any CIDR ranges into individual IP addresses + expanded_hosts = self.expand_cidr(hosts) + + with ThreadPoolExecutor(max_workers=self.max_workers) as executor: + future_to_host = {executor.submit(self.scan_host, host): host for host in expanded_hosts} + for future in as_completed(future_to_host): + host, open_ports = future.result() + if open_ports['nfs']: + hostlist_nfs.append(host) + if open_ports['smb']: + hostlist_smb.append(host) + + return hostlist_nfs, hostlist_smb + +def get_nfs_shares(self, hostlist): + nfsshares = [] + for host in hostlist: + shares = {'host': host, 'openshares': [], 'closedshares': []} + output = self.nm.scan(host, '111', + arguments='%s --datadir %s --script %s/nfs-showmount.nse,%s/nfs-ls.nse' + % (self.nmapargs, nmapdatadir, nmapdatadir, nmapdatadir)) + logger.debug('nm scan output: ' + str(output)) + try: + nfsshowmount = output['scan'][host]['tcp'][111]['script']['nfs-showmount'].strip().split('\n') + nfsls = output['scan'][host]['tcp'][111]['script']['nfs-ls'].strip().split('\n') + except KeyError: + print('%s PORT 111/tcp OPEN (rpcbind) but no results from nse script' % host) + continue + + openshares = [] + closedshares = [] + sharedict = {'sharename': nfsshowmount[0].strip().split(' ')[0]} + + # Ensure the list has at least 5 elements before accessing nfsls[4] + if len(nfsls) > 4 and re.search(r'ERROR: Mount failed: Permission denied', nfsls[4]): + closedshares.append(sharedict) + continue + else: + openshares.append(sharedict) + + for share in openshares: + shares['openshares'].append(share['sharename']) + for share in closedshares: + shares['closedshares'].append(share['sharename']) + nfsshares.append(shares) + + return nfsshares def get_smb_shares(self, hostlist): smbshares = [] @@ -224,7 +287,7 @@ def sniff_hosts(self): class mounter: def __init__(self, shares, mountdir='./', nfsmntopt='ro,nodev,nosuid', smbmntopt='ro,nodev,nosuid', - smbtype='smbfs', smbuser='guest', smbpass=''): + smbtype='smbfs', smbuser='guest', smbpass='', max_workers=10): self.shares = shares self.mountdir = mountdir self.nfsmntopt = nfsmntopt @@ -232,79 +295,72 @@ def __init__(self, shares, mountdir='./', nfsmntopt='ro,nodev,nosuid', smbmntopt self.smbtype = smbtype self.smbuser = smbuser self.smbpass = smbpass + self.max_workers = max_workers + + def mount_nfs_share(self, host, share): + """ Mounts an NFS share and returns the result. """ + mountpoint = self.mountdir + '/'+args.mountprefix+'-nfs_' + host + '_' + share.replace('/', '_') + mkdir = ['mkdir', '-p', mountpoint] + subprocess.Popen(mkdir) + mount = ['mount', '-v', '-o', self.nfsmntopt, '-t', 'nfs', host + ':' + share, mountpoint] + logger.debug('mount cmd: %s', mount) + process = subprocess.Popen(mount, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output = process.communicate() + if process.returncode > 0: + logger.debug('mount cmd exit code: %s', process.returncode) + mounted = False + try: + if os.path.exists(mountpoint): + os.rmdir(mountpoint) + except OSError: + raise OSError('error removing mountpoint directory') + else: + mounted = True + return {'host': host, 'sharetype': 'nfs', 'sharename': share, 'mountpoint': mountpoint, + 'output': output, 'exitcode': process.returncode, 'mounted': mounted} + + def mount_smb_share(self, host, share): + """ Mounts an SMB share and returns the result. """ + mountpoint = self.mountdir + '/'+args.mountprefix+'-smb_' + host + '_' + share.replace(' ', '_') + mkdir = ['mkdir', '-p', mountpoint] + subprocess.Popen(mkdir) + mount = ['mount', '-v', '-o', self.smbmntopt, '-t', self.smbtype, + '//' + self.smbuser + ':' + self.smbpass + '@' + host + '/' + share.replace(' ', '%20'), + mountpoint] + logger.debug('mount cmd: %s', mount) + process = subprocess.Popen(mount, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output = process.communicate() + if process.returncode > 0: + logger.debug('mount cmd exit code: %s', process.returncode) + mounted = False + try: + if os.path.exists(mountpoint): + os.rmdir(mountpoint) + except OSError: + raise OSError('error removing mountpoint directory') + else: + mounted = True + return {'host': host, 'sharetype': 'smb', 'sharename': share, 'mountpoint': mountpoint, + 'output': output, 'exitcode': process.returncode, 'mounted': mounted} def mount_shares(self): mount_status = [] - for hostdict in self.shares['nfsshares']: - hostname = hostdict['host'] - for share in hostdict['openshares']: - mountpoint = self.mountdir + '/'+args.mountprefix+'-nfs_' + hostname + '_' + share.replace('/', '_') - mkdir = ['mkdir', '-p', mountpoint] - subprocess.Popen(mkdir) - mount = ['mount', '-v', '-o', self.nfsmntopt, '-t', 'nfs', hostname + ':' + share, mountpoint] - logger.debug('mount cmd: %s', mount) - process = subprocess.Popen(mount, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output = process.communicate() - if process.returncode > 0: - logger.debug('mount cmd exit code: %s', process.returncode) - mounted = False - try: - if os.path.exists(mountpoint): - os.rmdir(mountpoint) - except OSError: - raise OSError('error removing mountpoint directory') - else: - mounted = True - mount_status.append( - {'host': hostname, 'sharetype': 'nfs', 'sharename': share, 'mountpoint': mountpoint, - 'output': output, 'exitcode': process.returncode, 'mounted': mounted}) - for hostdict in self.shares['smbshares']: - hostname = hostdict['host'] - for share in hostdict['openshares']: - mountpoint = self.mountdir + '/'+args.mountprefix+'-smb_' + hostname + '_' + share.replace(' ', '_') - mkdir = ['mkdir', '-p', mountpoint] - subprocess.Popen(mkdir) - mount = ['mount', '-v', '-o', self.smbmntopt, '-t', self.smbtype, - '//' + self.smbuser + ':' + self.smbpass + '@' + hostname + '/' + share.replace(' ', '%20'), - mountpoint] - logger.debug('mount cmd: %s', mount) - process = subprocess.Popen(mount, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output = process.communicate() - if process.returncode > 0: - logger.debug('mount cmd exit code: %s', process.returncode) - mounted = False - try: - if os.path.exists(mountpoint): - os.rmdir(mountpoint) - except OSError: - raise OSError('error removing mountpoint directory') - else: - mounted = True - mount_status.append( - {'host': hostname, 'sharetype': 'smb', 'sharename': share, 'mountpoint': mountpoint, - 'output': output, 'exitcode': process.returncode, 'mounted': mounted}) - return mount_status - - def umount_shares(self): - mount_status = [] - for hostdict in self.shares['nfsshares']: - hostname = hostdict['host'] - for share in hostdict['openshares']: - mountpoint = self.mountdir + '/'+args.mountprefix+'-nfs_' + hostname + '_' + share.replace('/', '_') - if os.path.ismount(mountpoint): - umount = ['umount', mountpoint] - subprocess.call(umount) - if os.path.exists(mountpoint): - os.rmdir(mountpoint) - for hostdict in self.shares['smbshares']: - hostname = hostdict['host'] - for share in hostdict['openshares']: - mountpoint = self.mountdir + '/'+args.mountprefix+'-smb_' + hostname + '_' + share.replace(' ', '_') - if os.path.ismount(mountpoint): - umount = ['umount', mountpoint] - subprocess.call(umount) - if os.path.exists(mountpoint): - os.rmdir(mountpoint) + + # Use ThreadPoolExecutor with max_workers set by user + with ThreadPoolExecutor(max_workers=self.max_workers) as executor: + future_to_mount = [] + + for hostdict in self.shares['nfsshares']: + for share in hostdict['openshares']: + future_to_mount.append(executor.submit(self.mount_nfs_share, hostdict['host'], share)) + + for hostdict in self.shares['smbshares']: + for share in hostdict['openshares']: + future_to_mount.append(executor.submit(self.mount_smb_share, hostdict['host'], share)) + + for future in as_completed(future_to_mount): + mount_status.append(future.result()) + return mount_status @@ -327,7 +383,8 @@ def sniff_network(): sniff = sniffer(hosts=args.hosts, excludehosts=args.excludehosts, nfs=args.nfs, smb=args.smb, smbuser=args.smbuser, - smbpass=args.smbpass) + smbpass=args.smbpass, max_workers=args.maxworkers) + shares = sniff.sniff_hosts() hostlist_nfs, hostlist_smb = sniff.sniff_hosts() shares = {'nfsshares': [], 'smbshares': []} if len(hostlist_nfs) > 0 or len(hostlist_smb) > 0: @@ -406,13 +463,14 @@ def auto_mounter(shares): if __name__ == "__main__": - # default mount options (optimized for crawling) mntopt_nfs = "ro,nosuid,nodev,noexec,udp,proto=udp,noatime,nodiratime,rsize=1024,dsize=1024,vers=3,rdirplus" mntopt_smb = "ro,nosuid,nodev,noexec,udp,proto=udp,noatime,nodiratime,rsize=1024,dsize=1024" # parse cli args parser = argparse.ArgumentParser() + parser.add_argument("--maxworkers", type=int, default=10, + help="Maximum number of concurrent threads for scanning and mounting (default: 10)") parser.add_argument("--hosts", metavar="HOSTS", help="Hosts to scan, example: 10.10.56.0/22 or 10.10.56.2 (default: scan all hosts)") parser.add_argument("-e", "--excludehosts", metavar="EXCLUDEHOSTS", @@ -426,7 +484,7 @@ def auto_mounter(shares): parser.add_argument("-s", "--smb", action="store_true", help="Scan network for smb shares") parser.add_argument("--smbmntopt", metavar="SMBMNTOPT", default=mntopt_smb, - help = "smb mount options (default: "+mntopt_smb+")") + help="smb mount options (default: "+mntopt_smb+")") parser.add_argument("--smbtype", metavar='SMBTYPE', default="smbfs", help="Can be smbfs (default) or cifs") parser.add_argument("--smbuser", metavar='SMBUSER', default="guest", @@ -440,11 +498,13 @@ def auto_mounter(shares): parser.add_argument("-p", "--mountprefix", metavar="MOUNTPREFIX", default="sharesniffer", help="Prefix for mountpoint directory name (default: sharesniffer)") parser.add_argument("-v", "--verbose", action="store_true", - help="Increase output verbosity") + help="Increase output verbosity") parser.add_argument("--debug", action="store_true", help="Debug message output") parser.add_argument("-q", "--quiet", action="store_true", help="Run quiet and just print out any possible mount points for crawling") + parser.add_argument("--nmapdatadir", metavar="NMAPDATADIR", default=None, + help="Path to the Nmap NSE script directory (optional)") parser.add_argument("-V", "--version", action="version", version="sharesniffer v%s" % SHARESNIFFER_VERSION, help="Prints version and exits") @@ -499,24 +559,36 @@ def auto_mounter(shares): print(banner + '\n') # check for Nmap nse scripts directory - nmapdatadir = None - nmap_script_dirs = ['/usr/local/share/nmap/scripts', '/usr/share/nmap/scripts'] - for path in nmap_script_dirs: - if os.path.isdir(path): - nmapdatadir = path - break - if not nmapdatadir: - print("Unable to locate nmap nse scripts directory") - sys.exit(1) + if args.nmapdatadir: + if os.path.isdir(args.nmapdatadir): + nmapdatadir = args.nmapdatadir + else: + print("Provided Nmap NSE script directory does not exist: %s" % args.nmapdatadir) + sys.exit(1) + else: + nmapdatadir = None + nmap_script_dirs = ['/usr/local/share/nmap/scripts', '/usr/share/nmap/scripts'] + for path in nmap_script_dirs: + if os.path.isdir(path): + nmapdatadir = path + break + if not nmapdatadir: + print("Unable to locate nmap nse scripts directory") + sys.exit(1) + logger.debug('Nmap datadir: ' + nmapdatadir) # get shares and mountpoints shares = sniff_network() if args.automount: - mountstocrawl = auto_mounter(shares) + mmounts = mounter(shares, mountdir=args.mountpoint, nfsmntopt=args.nfsmntopt, + smbmntopt=args.smbmntopt, smbtype=args.smbtype, + smbuser=args.smbuser, smbpass=args.smbpass, max_workers=args.maxworkers) + mountstocrawl = mounts.mount_shares() if args.quiet: for m in mountstocrawl: print(m) else: logger.info('Skipping auto-mount, exiting') + sys.exit(0)