├── .gitignore ├── README.md ├── outwriter.py ├── pyshareanalyzer.py └── pysharecrawler.py /.gitignore: -------------------------------------------------------------------------------- 1 | TODO 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | * Python 2.7 4 | * Impacket Python library 5 | * sqlite3 Python library 6 | 7 | # sharecrawler 8 | Python crawler for remote Windows shares 9 | 10 | ``` 11 | usage: pysharecrawler.py [-h] (--rhosts RHOSTS | --file FILE) 12 | [--hashes HASHES] [--verbose] [--maxdepth MAXDEPTH] 13 | [--out OUT] 14 | LOGIN 15 | 16 | Complete Windows Samba share crawler. 17 | 18 | positional arguments: 19 | LOGIN Can be standalone username for local account or 20 | domain/username 21 | 22 | optional arguments: 23 | -h, --help show this help message and exit 24 | --rhosts RHOSTS IP Adress or IP/CIDR 25 | --file FILE Read IP adresses from input file. One adress per line 26 | --hashes HASHES NTLM hashes, format is LMHASH:NTHASH 27 | --verbose Show debug messages 28 | --maxdepth MAXDEPTH Maximum depth to crawl in shares (default=1) 29 | --out OUT Output type: (print, csv:, sqlite:) 30 | ``` 31 | 32 | # shareanalyzer 33 | Search patterns in files and directories in Windows samba shares 34 | 35 | ``` 36 | usage: pyshareanalyzer.py [-h] INPUT OUTPUT FILTERS 37 | 38 | Windows Samba share analyzer. 39 | 40 | positional arguments: 41 | INPUT Input type: (csv:, sqlite:) 42 | OUTPUT Output type: (print, csv:, sqlite:, 43 | html:) 44 | FILTERS Path of file containing filtering regexes, one per line 45 | 46 | optional arguments: 47 | -h, --help show this help message and exit 48 | ``` 49 | -------------------------------------------------------------------------------- /outwriter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Michael Molho 3 | 2015 4 | michael.molho@gmail.com 5 | """ 6 | 7 | import os 8 | import os.path 9 | import sqlite3 10 | import time 11 | import datetime 12 | 13 | 14 | def sizeof_fmt(num, suffix='B'): 15 | for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: 16 | if abs(num) < 1024.0: 17 | return "%3.1f%s%s" % (num, unit, suffix) 18 | num /= 1024.0 19 | return "%.1f%s%s" % (num, 'Yi', suffix) 20 | 21 | 22 | class CsvOutWriter(): 23 | def __init__(self, filepath): 24 | self.filepath = filepath 25 | 26 | def write(self, host, nbtname, share, fileattrs, filepath): 27 | pass 28 | 29 | def commit(self): 30 | pass 31 | 32 | 33 | 34 | class SqliteOutWriter(): 35 | def __init__(self, filepath): 36 | self.filepath = filepath 37 | if os.path.isfile(filepath): 38 | keepfile = raw_input("\nWARNING: The file " + filepath + " already exits. Would you like to keep the content and add new entries ? Otherwise the file will be erased. (y/N) ") 39 | if keepfile == "N": 40 | os.remove(filepath) 41 | self.initdb(False) 42 | elif keepfile == "y": 43 | self.initdb(True) 44 | else: 45 | self.initdb(False) 46 | 47 | def initdb(self, keepexisting): 48 | self.dbconn = sqlite3.connect(self.filepath) 49 | self.cur = self.dbconn.cursor() 50 | if not keepexisting: 51 | self.cur.execute("CREATE TABLE Entries(Host TEXT, \ 52 | NbtName TEXT, \ 53 | Attributes INTEGER, \ 54 | Smbmtime INTEGER, \ 55 | Size INTEGER, \ 56 | Share TEXT, \ 57 | Filepath TEXT)") 58 | 59 | def write(self, host, nbtname, share, fileattrs, filepath): 60 | query = "INSERT INTO Entries VALUES(?, ?, ?, ?, ?, ?, ?)" 61 | data = ( host, 62 | nbtname, 63 | int(fileattrs.get_attributes()), 64 | int(fileattrs.get_mtime()), #careful it's smb time, not timestamp 65 | int(fileattrs.get_filesize()), 66 | share, 67 | filepath ) 68 | self.cur.execute( query, data ) 69 | 70 | def commit(self): 71 | self.dbconn.commit() 72 | 73 | 74 | 75 | class StandardOutWriter(): 76 | def commit(self): 77 | pass 78 | 79 | def write(self, host, nbtname, share, fileattrs, filepath): 80 | is_ro = 'R' if fileattrs.is_readonly() else '-' 81 | is_system = 'S' if fileattrs.is_system() else '-' 82 | is_hidden = 'H' if fileattrs.is_hidden() else '-' 83 | is_directory = 'D' if fileattrs.is_directory() else '-' 84 | attrs = "-".join([is_ro, is_system, is_hidden, is_directory]) 85 | mtime = datetime.datetime.fromtimestamp(int(fileattrs.get_mtime_epoch())).strftime('%Y-%m-%d') 86 | size = str(sizeof_fmt(fileattrs.get_filesize())).ljust(10) 87 | print u" [*] ".encode('utf-8') + attrs.ljust(4) + ' ' + mtime + ' ' + size + ' ' + share.encode('utf-8') + filepath 88 | -------------------------------------------------------------------------------- /pyshareanalyzer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sqlite3 4 | import argparse 5 | import sys 6 | import sqlite3 7 | import time 8 | from impacket import smb 9 | from outwriter import CsvOutWriter, SqliteOutWriter, StandardOutWriter 10 | 11 | class SmbAnalyzer(): 12 | def __init__(self, input, filters, out="print"): 13 | self.input = input 14 | self.out = out 15 | self.outwriter = StandardOutWriter() 16 | 17 | if len(input) > 7 and input[0:7] == "sqlite:": 18 | self.filepath = input.split(':')[1] 19 | self.func_getvalue = self.sqlite_getvalue 20 | 21 | self.filters = [line.strip() for line in open(filters, 'r').readlines() if line[0] != '#'] 22 | 23 | 24 | def analyze(self): 25 | for val in self.func_getvalue(): 26 | host = val[0] 27 | nbtname = val[1] 28 | attributes = val[2] 29 | mtime = val[3] 30 | size = val[4] 31 | share = val[5] 32 | filepath = val[6] 33 | filename = filepath.replace('\\', '/').split('/')[-1] 34 | smbfile = smb.SharedFile(int(time.time()), int(time.time()), int(mtime), int(size), int(size), int(attributes), filename, filename) 35 | for filter in self.filters: 36 | if filter.lower() in filepath.lower(): 37 | self.outwriter.write(host, nbtname, share, smbfile, filepath) 38 | 39 | 40 | def sqlite_getvalue(self): 41 | self.dbconn = sqlite3.connect(self.filepath) 42 | self.cur = self.dbconn.cursor() 43 | self.cur.execute("SELECT * FROM Entries") 44 | for row in self.cur.fetchall(): 45 | yield row 46 | 47 | if __name__ == "__main__": 48 | parser = argparse.ArgumentParser(description='Windows Samba share analyzer.') 49 | parser.add_argument('INPUT', action="store", help="Input type: (csv:, sqlite:)") 50 | parser.add_argument('OUTPUT', action="store", help="Output type: (print, csv:, sqlite:, html:)") 51 | parser.add_argument('FILTERS', action="store", help="Path of file containing filtering regexes, one per line") 52 | 53 | if len(sys.argv) == 1: 54 | parser.print_help() 55 | sys.exit(1) 56 | 57 | result = parser.parse_args() 58 | cmdargs = dict(result._get_kwargs()) 59 | 60 | analyzer = SmbAnalyzer(cmdargs['INPUT'], cmdargs['FILTERS'], cmdargs['OUTPUT']) 61 | analyzer.analyze() 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /pysharecrawler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Michael Molho 5 | 2015 6 | michael.molho@gmail.com 7 | """ 8 | 9 | from impacket.dcerpc import srvsvc 10 | 11 | import sys 12 | import string 13 | import time 14 | import logging 15 | from impacket import smb, version, smb3, nt_errors 16 | from impacket.dcerpc.v5 import samr, transport, srvs 17 | from impacket.dcerpc.v5.dtypes import NULL 18 | from impacket.smbconnection import * 19 | from impacket.nmb import NetBIOS 20 | from getpass import getpass 21 | import argparse 22 | import ntpath 23 | import netaddr 24 | from outwriter import CsvOutWriter, SqliteOutWriter, StandardOutWriter 25 | 26 | 27 | class SmbCrawler(): 28 | def __init__(self, verbose=False, out="print"): 29 | self.host = '' 30 | self.nbtname = '' 31 | self.smb = None 32 | self.maxdepth = 999 33 | self.password = None 34 | self.lmhash = None 35 | self.nthash = None 36 | self.username = '' 37 | self.domain = '' 38 | self.verbose = verbose 39 | 40 | if len(out) > 4 and out[0:4] == "csv:": 41 | filepath = out.split(':')[1] 42 | self.outwriter = CsvOutWriter(filepath) 43 | elif len(out) > 7 and out[0:7] == "sqlite:": 44 | filepath = out.split(':')[1] 45 | self.outwriter = SqliteOutWriter(filepath) 46 | else: 47 | self.outwriter = StandardOutWriter() 48 | 49 | def resolveNbtName(self): 50 | nbt = NetBIOS() 51 | try: 52 | name = nbt.getnetbiosname(self.host) 53 | return name 54 | except: 55 | return '' 56 | 57 | def open(self, host, port): 58 | self.host = host 59 | self.nbtname = self.resolveNbtName() 60 | if port == 139: 61 | self.smb = SMBConnection('*SMBSERVER', host, sess_port=port) 62 | else: 63 | self.smb = SMBConnection(host, host, sess_port=port) 64 | 65 | dialect = self.smb.getDialect() 66 | if dialect == SMB_DIALECT: 67 | logging.info("SMBv1 dialect used") 68 | elif dialect == SMB2_DIALECT_002: 69 | logging.info("SMBv2.0 dialect used") 70 | elif dialect == SMB2_DIALECT_21: 71 | logging.info("SMBv2.1 dialect used") 72 | else: 73 | logging.info("SMBv3.0 dialect used") 74 | 75 | def login(self, domain, username, password): 76 | if self.smb is None: 77 | logging.error("No connection open") 78 | return 79 | 80 | try: 81 | self.smb.login(username, password, domain=domain) 82 | except Exception as e: 83 | print ("Authentication failed : " + str(e)) 84 | raise 85 | self.username = username 86 | self.domain = domain 87 | 88 | if self.smb.isGuestSession() > 0: 89 | logging.info("GUEST Session Granted") 90 | else: 91 | logging.info("USER Session Granted") 92 | 93 | def login_hash(self, domain, username, hashes): 94 | if self.smb is None: 95 | logging.error("No connection open") 96 | return 97 | 98 | try: 99 | lmhash, nthash = hashes.split(':') 100 | self.smb.login(username, '', domain,lmhash=lmhash, nthash=nthash) 101 | except Exception as e: 102 | print ("Authentication failed : " + str(e)) 103 | raise 104 | self.username = username 105 | self.domain = domain 106 | 107 | if self.smb.isGuestSession() > 0: 108 | logging.info("GUEST Session Granted") 109 | else: 110 | logging.info("USER Session Granted") 111 | 112 | def shares(self): 113 | shares = [] 114 | rpctransport = transport.SMBTransport(self.smb.getRemoteHost(), self.smb.getRemoteHost(), filename = r'\srvsvc', smb_connection = self.smb) 115 | dce = rpctransport.get_dce_rpc() 116 | dce.connect() 117 | dce.bind(srvs.MSRPC_UUID_SRVS) 118 | res = srvs.hNetrShareEnum(dce,1) 119 | resp = res['InfoStruct']['ShareInfo']['Level1']['Buffer'] 120 | for i in range(len(resp)): 121 | shares += [resp[i]['shi1_netname'][:-1]] 122 | return shares 123 | 124 | def ls(self, share, pwd): 125 | files = [] 126 | f_pwd = '' 127 | f_pwd = ntpath.join(pwd, '*') 128 | f_pwd = string.replace(f_pwd,'/','\\') 129 | f_pwd = ntpath.normpath(f_pwd) 130 | for f in self.smb.listPath(share, f_pwd): 131 | if f.get_longname() not in ['.', '..']: 132 | files += [f] 133 | return files 134 | 135 | def use(self,share): 136 | tid = self.smb.connectTree(share) 137 | self.ls(share, '\\') 138 | return tid 139 | 140 | def spider(self, share, root, maxdepth): 141 | if maxdepth < 0: 142 | return [] 143 | try: 144 | files = self.ls(share, root) 145 | except Exception as e: 146 | if self.verbose: 147 | print ("Error in ls("+share+","+root+","+str(maxdepth)+") : " + str(e)) 148 | return [] 149 | for f in files: 150 | new_root = ntpath.join(root, f.get_longname()) 151 | new_root = ntpath.normpath(new_root) 152 | self.outwriter.write(self.host, self.nbtname, share, f, new_root) 153 | if f.is_directory(): 154 | self.spider(share, root + f.get_longname() + '\\', maxdepth - 1) 155 | 156 | def crawl(self, maxdepth, thread = 1): 157 | self.maxdepth = maxdepth 158 | shares = self.shares() 159 | for share in shares: 160 | print ('[+] Spidering ' + share) 161 | try: 162 | tid = self.use(share) 163 | except Exception as e: 164 | if self.verbose: 165 | print ("Error in use("+share+") : " + str(e)) 166 | self.spider(share, '\\', maxdepth) 167 | self.outwriter.commit() 168 | 169 | if __name__ == "__main__": 170 | rhosts = [] 171 | domain = '' 172 | username = '' 173 | 174 | parser = argparse.ArgumentParser(description='Complete Windows Samba share crawler.') 175 | parser.add_argument('LOGIN', action="store", help="Can be standalone username for local account or domain/username") 176 | usergroup = parser.add_mutually_exclusive_group(required=True) 177 | usergroup.add_argument('--rhosts', action="store", default=None, help="IP Adress or IP/CIDR") 178 | usergroup.add_argument('--file', action="store", default=None, help="Read IP adresses from input file. One adress per line") 179 | parser.add_argument('--hashes', action="store", default=None, help="NTLM hashes, format is LMHASH:NTHASH") 180 | parser.add_argument('--verbose', action="store_true", default=False, help="Show debug messages") 181 | parser.add_argument('--maxdepth', action="store", type=int, default=1, help="Maximum depth to crawl in shares (default=1)") 182 | parser.add_argument('--out', action="store", default="print", help="Output type: (print, csv:, sqlite:)") 183 | 184 | if len(sys.argv) == 1: 185 | parser.print_help() 186 | sys.exit(1) 187 | 188 | result = parser.parse_args() 189 | cmdargs = dict(result._get_kwargs()) 190 | 191 | if cmdargs['file'] != None: 192 | rhosts += [line.strip() for line in open(cmdargs['file'], 'r')] 193 | else: 194 | rhosts += [ip.__str__() for ip in list(netaddr.IPNetwork(cmdargs['rhosts']))] 195 | if '/' in cmdargs['LOGIN']: 196 | domain, username = tuple(cmdargs['LOGIN'].split('/')) 197 | else: 198 | domain, username = '', cmdargs['LOGIN'] 199 | 200 | if cmdargs['hashes'] == None: 201 | password = getpass("Password:") 202 | 203 | crawler = SmbCrawler( verbose=cmdargs['verbose'], out=cmdargs['out'] ) 204 | 205 | for rhost in rhosts: 206 | print ('\n -- ' + rhost + ' -- \n') 207 | try: 208 | crawler.open(rhost,445) 209 | print ('[+] Resolve netbios name : ' + crawler.nbtname + '\n') 210 | if cmdargs['hashes'] != None: 211 | crawler.login_hash(domain, username, cmdargs['hashes']) 212 | else: 213 | crawler.login(domain, username, password) 214 | crawler.crawl(maxdepth = cmdargs['maxdepth']) 215 | except Exception as e: 216 | if crawler.verbose: 217 | print ("Error : " + str(e)) 218 | --------------------------------------------------------------------------------