diff --git a/CHANGELOG b/CHANGELOG index c434922f..3119874c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +LaZagne 0.6 (26/05/2015) +- For Windows / Linux + * Firefox / Thunderbird: No more dependency with nss library (many thanks to Laurent for his technic: https://github.com/lclevy/firepwd) + * Fix opera bug + +- Only Windows + * WinSCP false positive removed (when SSH key is used) + LaZagne 0.5 (21/05/2015) - For Windows * Fix chrome bug diff --git a/Linux/src/LaZagne.py b/Linux/src/LaZagne.py index f6479006..2040bb0f 100644 --- a/Linux/src/LaZagne.py +++ b/Linux/src/LaZagne.py @@ -13,13 +13,13 @@ import logging from softwares.browsers.mozilla import Mozilla -# configuration +# Configuration from config.header import Header from config.write_output import write_header, write_footer, print_footer from config.constant import * from config.manageModules import get_categories, get_modules -# print the title +# Print the title Header().first_title() category = get_categories() @@ -44,7 +44,7 @@ def output(): del args['write'] def verbosity(): - # write on the console + debug file + # Write on the console + debug file if args['verbose']==0: level=logging.CRITICAL elif args['verbose'] == 1: level=logging.INFO elif args['verbose']>=2: level=logging.DEBUG @@ -55,7 +55,7 @@ def verbosity(): stream.setFormatter(formatter) root = logging.getLogger() root.setLevel(level) - # if other logging are set + # If other logging are set for r in root.handlers: r.setLevel(logging.CRITICAL) root.addHandler(stream) @@ -63,13 +63,13 @@ def verbosity(): def launch_module(b): ok = False - # launch only a specific module + # Launch only a specific module for i in args: if args[i] and i in b: b[i].run() ok = True - # launch all modules + # Launch all modules if not ok: for i in b: b[i].run() @@ -109,7 +109,7 @@ def runAllModules(): constant.mozilla_software = 'Thunderbird' launch_module(modules[categoryName]) -# prompt help if an error occurs +# Prompt help if an error occurs class MyParser(argparse.ArgumentParser): def error(self, message): sys.stderr.write('error: %s\n\n' % message) @@ -136,13 +136,13 @@ def error(self, message): category[c]['parser'] = argparse.ArgumentParser(add_help=False,formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=constant.MAX_HELP_POSITION)) category[c]['parser']._optionals.title = category[c]['help'] - # manage options + # Manage options category[c]['subparser'] = [] for module in modules[c]: m = modules[c][module] category[c]['parser'].add_argument(m.options['command'], action=m.options['action'], dest=m.options['dest'], help=m.options['help']) - # manage all suboptions by modules + # Manage all suboptions by modules if m.suboptions and m.name != 'thunderbird': tmp = [] for sub in m.suboptions: @@ -168,7 +168,7 @@ def error(self, message): dic_tmp = {c: {'parents': parser_tab, 'help':'Run %s module' % c, 'func': runModule}} dic = dict(dic.items() + dic_tmp.items()) -#2- main commands +# 2- main commands subparsers = parser.add_subparsers(help='Choose a main command') for d in dic: subparsers.add_parser(d,parents=dic[d]['parents'],help=dic[d]['help']).set_defaults(func=dic[d]['func'],auditType=d) @@ -181,7 +181,7 @@ def error(self, message): verbosity() arguments.func() -# print the number of passwords found +# Print the number of passwords found if constant.output == 'txt': write_footer() print_footer() diff --git a/Linux/src/config/constant.py b/Linux/src/config/constant.py index 051646c5..1640213c 100644 --- a/Linux/src/config/constant.py +++ b/Linux/src/config/constant.py @@ -2,7 +2,7 @@ class constant(): folder_name = 'results' MAX_HELP_POSITION = 27 - CURRENT_VERSION = 0.5 + CURRENT_VERSION = 0.6 output = None file_logger = None verbose = False diff --git a/Linux/src/softwares/browsers/mozilla.py b/Linux/src/softwares/browsers/mozilla.py index baa45697..32e54bff 100644 --- a/Linux/src/softwares/browsers/mozilla.py +++ b/Linux/src/softwares/browsers/mozilla.py @@ -11,15 +11,19 @@ import shutil from config.dico import get_dico import itertools +#https://pypi.python.org/pypi/pyasn1/ +from pyasn1.codec.der import decoder +from struct import unpack +from binascii import hexlify, unhexlify +from hashlib import sha1 +import hmac +from Crypto.Util.number import long_to_bytes +from Crypto.Cipher import DES3 from config.header import Header from config.constant import * from config.write_output import print_debug, print_output from config.moduleInfo import ModuleInfo -# Password structures -class SECItem(Structure): - _fields_ = [('type', c_uint),('data', c_void_p),('len', c_uint)] - # Database classes database_find = False class Credentials(object): @@ -82,19 +86,15 @@ class Mozilla(ModuleInfo): def __init__(self, isThunderbird = False): - self.credentials_categorie = None - self.libnss = None - self.slot = None - - self.username = SECItem() - self.passwd = SECItem() - self.dectext = SECItem() - + self.credentials_categorie = None + self.toCheck = [] self.manually_pass = None self.dictionnary_path = None self.number_toStop = None + self.key3 = '' + # Manage options suboptions = [ {'command': '-m', 'action': 'store', 'dest': 'manually', 'help': 'enter the master password manually', 'title': 'Advanced Mozilla master password options'}, @@ -110,13 +110,6 @@ def __init__(self, isThunderbird = False): else: options = {'command': '-t', 'action': 'store_true', 'dest': 'thunderbird', 'help': 'thunderbird'} ModuleInfo.__init__(self, 'thunderbird', 'browsers', options, suboptions) - - def __del__(self): - self.libnss = None - - self.username = None - self.passwd = None - self.dectext = None def get_path(self, software_name): path = "" @@ -147,44 +140,145 @@ def manage_advanced_options(self): self.toCheck = ['b', 'd'] self.number_toStop = 3 - def initialize_libnss(self, list_libnss, profile): - for lib in list_libnss: - try: - self.libnss = CDLL(lib) - if self.libnss.NSS_Init(profile) == 0: - return True - except Exception,e: - print_debug('ERROR', '{0}'.format(e)) - return False - - def found_libnss(self): - directory = '/usr/lib/' - list_libnss = [] - for root, dirs, files in os.walk(directory): - for file in files: - if file == 'libnss3.so': - list_libnss.append(root + os.sep + file) - return list_libnss - - def decrypt(self, software_name, credentials): - pwdFound = [] - for host, user, passw in credentials: - values = {} - values["Website"] = format(host.encode("utf-8")) - self.username.data = cast(c_char_p(b64decode(user)), c_void_p) - self.username.len = len(b64decode(user)) - self.passwd.data = cast(c_char_p(b64decode(passw)), c_void_p) - self.passwd.len = len(b64decode(passw)) - - if self.libnss.PK11SDR_Decrypt(byref(self.username), byref(self.dectext), None) != -1: - values["Username"] = string_at(self.dectext.data, self.dectext.len) - - if self.libnss.PK11SDR_Decrypt(byref(self.passwd), byref(self.dectext), None) != -1: - values["Password"] = string_at(self.dectext.data, self.dectext.len) + # -------------------------------------------- + + def getShortLE(self, d, a): + return unpack('L',(d)[a:a+4])[0] + + def printASN1(self, d, l, rl): + type = ord(d[0]) + length = ord(d[1]) + if length&0x80 > 0: #http://luca.ntop.org/Teaching/Appunti/asn1.html, + nByteLength = length&0x7f + length = ord(d[2]) + #Long form. Two to 127 octets. Bit 8 of first octet has value "1" and bits 7-1 give the number of additional length octets. + skip=1 + else: + skip=0 + + if type==0x30: + seqLen = length + readLen = 0 + while seqLen>0: + len2 = self.printASN1(d[2+skip+readLen:], seqLen, rl+1) + seqLen = seqLen - len2 + readLen = readLen + len2 + return length+2 + elif type==6: #OID + return length+2 + elif type==4: #OCTETSTRING + return length+2 + elif type==5: #NULL + # print 0 + return length+2 + elif type==2: #INTEGER + return length+2 + else: + if length==l-2: + self.printASN1( d[2:], length, rl+1) + return length + + #extract records from a BSD DB 1.85, hash mode + def readBsddb(self, name): + f = open(name,'rb') + + #http://download.oracle.com/berkeley-db/db.1.85.tar.gz + header = f.read(4*15) + magic = self.getLongBE(header,0) + if magic != 0x61561: + print_debug('WARNING', 'Bad magic number') + return False + version = self.getLongBE(header,4) + if version !=2: + print_debug('WARNING', 'Bad version !=2 (1.85)') + return False + pagesize = self.getLongBE(header,12) + nkeys = self.getLongBE(header,0x38) + + readkeys = 0 + page = 1 + nval = 0 + val = 1 + db1 = [] + while (readkeys < nkeys): + f.seek(pagesize*page) + offsets = f.read((nkeys+1)* 4 +2) + offsetVals = [] + i=0 + nval = 0 + val = 1 + keys = 0 + while nval != val : + keys +=1 + key = self.getShortLE(offsets,2+i) + val = self.getShortLE(offsets,4+i) + nval = self.getShortLE(offsets,8+i) + offsetVals.append(key+ pagesize*page) + offsetVals.append(val+ pagesize*page) + readkeys += 1 + i += 4 + offsetVals.append(pagesize*(page+1)) + valKey = sorted(offsetVals) + for i in range( keys*2 ): + f.seek(valKey[i]) + data = f.read(valKey[i+1] - valKey[i]) + db1.append(data) + page += 1 + f.close() + db = {} + + for i in range( 0, len(db1), 2): + db[ db1[i+1] ] = db1[ i ] + + return db + + def decrypt3DES(self, globalSalt, masterPassword, entrySalt, encryptedData ): + #see http://www.drh-consultancy.demon.co.uk/key3.html + hp = sha1( globalSalt+masterPassword ).digest() + pes = entrySalt + '\x00'*(20-len(entrySalt)) + chp = sha1( hp+entrySalt ).digest() + k1 = hmac.new(chp, pes+entrySalt, sha1).digest() + tk = hmac.new(chp, pes, sha1).digest() + k2 = hmac.new(chp, tk+entrySalt, sha1).digest() + k = k1+k2 + iv = k[-8:] + key = k[:24] + + return DES3.new( key, DES3.MODE_CBC, iv).decrypt(encryptedData) + + def extractSecretKey(self, globalSalt, masterPassword, entrySalt): + + (globalSalt, masterPassword, entrySalt) = self.is_masterpassword_correct(masterPassword) + + if unhexlify('f8000000000000000000000000000001') not in self.key3: + return None + privKeyEntry = self.key3[ unhexlify('f8000000000000000000000000000001') ] + saltLen = ord( privKeyEntry[1] ) + nameLen = ord( privKeyEntry[2] ) + privKeyEntryASN1 = decoder.decode( privKeyEntry[3+saltLen+nameLen:] ) + data = privKeyEntry[3+saltLen+nameLen:] + self.printASN1(data, len(data), 0) - if len(values): - pwdFound.append(values) - return pwdFound + #see https://github.com/philsmd/pswRecovery4Moz/blob/master/pswRecovery4Moz.txt + entrySalt = privKeyEntryASN1[0][0][1][0].asOctets() + privKeyData = privKeyEntryASN1[0][1].asOctets() + privKey = self.decrypt3DES( globalSalt, masterPassword, entrySalt, privKeyData ) + self.printASN1(privKey, len(privKey), 0) + + privKeyASN1 = decoder.decode( privKey ) + prKey= privKeyASN1[0][2].asOctets() + self.printASN1(prKey, len(prKey), 0) + prKeyASN1 = decoder.decode( prKey ) + id = prKeyASN1[0][1] + key = long_to_bytes( prKeyASN1[0][3] ) + + print_debug('DEBUG', 'key: %s' % repr(key)) + return key + + # -------------------------------------------- # Get the path list of the firefox profiles def get_firefox_profiles(self, directory): @@ -236,25 +330,18 @@ def save_db(self, userpath): # ------------------------------ Master Password Functions ------------------------------ - # check if a masterpassword is set - def is_masterpasswd_set(self): - password = '' - self.slot = self.libnss.PK11_GetInternalKeySlot() - self.libnss.PK11_Authenticate(self.slot, True, 0) - pw_good = self.libnss.PK11_CheckUserPassword(self.slot, c_char_p(password)) - self.libnss.PK11_FreeSlot(self.slot) - - # Not masterpassword set - if pw_good == 0: - return False - else: - return True - - def is_masterpassword_correct(self, pwd): - pw_good = self.libnss.PK11_CheckUserPassword(self.slot, c_char_p(pwd)) - if pw_good == 0: - return True - return False + def is_masterpassword_correct(self, masterPassword=''): + #see http://www.drh-consultancy.demon.co.uk/key3.html + pwdCheck = self.key3['password-check'] + entrySaltLen = ord(pwdCheck[1]) + entrySalt = pwdCheck[3: 3+entrySaltLen] + encryptedPasswd = pwdCheck[-16:] + globalSalt = self.key3['global-salt'] + cleartextData = self.decrypt3DES( globalSalt, masterPassword, entrySalt, encryptedPasswd ) + if cleartextData != 'password-check\x02\x02': + return ('', '', '') + + return (globalSalt, masterPassword, entrySalt) # Retrieve masterpassword def found_masterpassword(self): @@ -262,9 +349,9 @@ def found_masterpassword(self): # master password entered manually if 'm' in self.toCheck: print_debug('ATTACK', 'Check the password entered manually !') - if self.is_masterpassword_correct(self.manually_pass): + if self.is_masterpassword_correct(self.manually_pass)[0]: print_debug('FIND', 'Master password found: %s' % self.manually_pass) - return True + return self.manually_pass else: print_debug('WARNING', 'The Master password entered is not correct') @@ -275,16 +362,16 @@ def found_masterpassword(self): num_lines = sum(1 for line in pass_file) except: print_debug('ERROR', 'Unable to open passwords file: %s' % str(self.dictionnary_path)) - return 1 + return False pass_file.close() print_debug('ATTACK', 'Dictionnary Attack !!! (%s words)' % str(num_lines)) try: with open(self.dictionnary_path) as f: for p in f: - if self.is_masterpassword_correct(p.strip()): + if self.is_masterpassword_correct(p.strip())[0]: print_debug('FIND', 'Master password found: %s' % p.strip()) - return True + return p.strip() except (KeyboardInterrupt, SystemExit): print 'INTERRUPTED!' @@ -301,9 +388,9 @@ def found_masterpassword(self): print_debug('ATTACK', '%d most used passwords !!! ' % num_lines) for word in wordlist: - if self.is_masterpassword_correct(word): + if self.is_masterpassword_correct(word)[0]: print_debug('FIND', 'Master password found: %s' % word.strip()) - return True + return word print_debug('WARNING', 'No password has been found using the default list') @@ -319,9 +406,9 @@ def found_masterpassword(self): while current <= self.number_toStop and pass_found == False: for i in itertools.product(tab, repeat=current): word = ''.join(map(str,i)) - if self.is_masterpassword_correct(word): + if self.is_masterpassword_correct(word)[0]: print_debug('FIND', 'Master password found: %s' % word.strip()) - return True + return word.strip() current+= 1 except (KeyboardInterrupt, SystemExit): print 'INTERRUPTED!' @@ -330,6 +417,7 @@ def found_masterpassword(self): print_debug('DEBUG', '{0}'.format(e)) print_debug('WARNING', 'No password has been found using the brute force attack') + return False # ------------------------------ End of Master Password Functions ------------------------------ @@ -352,16 +440,10 @@ def run(self): print_debug('WARNING', 'Installation path not found') return - list_libnss = self.found_libnss() - - # Check if the libnss could be initialized well - if not list_libnss: - print_debug('WARNING', 'The libnss have not been initialized because the libnss3.so has not been found') - #Check if mozilla folder has been found elif not os.path.exists(path): print_debug('INFO', software_name + ' not installed.') - + return else: if specific_path: if os.path.exists(specific_path): @@ -375,42 +457,65 @@ def run(self): pwdFound = [] for profile in profile_list: print_debug('INFO', 'Profile path found: %s' % profile) + if not os.path.exists(profile + os.sep + 'key3.db'): + print_debug('WARNING', 'key3 file not found: %s' % self.key3) + return - if self.initialize_libnss(list_libnss, profile): - masterPwd = self.is_masterpasswd_set() - - # check if passwors are stored on the Json format + self.key3 = self.readBsddb(profile + os.sep + 'key3.db') + if not self.key3: + return + + # check if passwords are stored on the Json format + try: + credentials = JsonDatabase(profile) + except: + database_find = False + + if not database_find: + # check if passwords are stored on the sqlite format try: - credentials = JsonDatabase(profile) + credentials = SqliteDatabase(profile) except: database_find = False - - if not database_find: - # check if passwors are stored on the sqlite format - try: - credentials = SqliteDatabase(profile) - except: - database_find = False + + if database_find: + masterPassword = '' + (globalSalt, masterPassword, entrySalt) = self.is_masterpassword_correct(masterPassword) - if database_find: - if masterPwd: - print_debug('WARNING', 'A masterpassword is used !!') - masterPwdFound = self.found_masterpassword() - - if not masterPwd or masterPwdFound: - try: - # decrypt passwords on the db - pwdFound+=self.decrypt(software_name, credentials) - except Exception,e: - print_debug('ERROR', '{0}'.format(e)) - - # if a master password is set (but not found), we save the db to bruteforce offline - elif masterPwd and not masterPwdFound and constant.output == 'txt': - self.save_db(profile) + # find masterpassword if set + if not globalSalt: + print_debug('WARNING', 'Master Password is used !') + masterPassword = self.found_masterpassword() + if not masterPassword: + return + + # get user secret key + key = self.extractSecretKey(globalSalt, masterPassword, entrySalt) + if not key: + return + + # everything is ready to decrypt password + for host, user, passw in credentials: + values = {} + values["Website"] = host + + # Login + loginASN1 = decoder.decode(b64decode(user)) + iv = loginASN1[0][1][1].asOctets() + ciphertext = loginASN1[0][2].asOctets() + login = re.sub( r'[^\\x\w]', '', DES3.new( key, DES3.MODE_CBC, iv).decrypt(ciphertext) ) + values["Username"] = login - self.libnss.NSS_Shutdown() - else: - print_debug('ERROR', 'Could not initialize the NSS library') - + # Password + passwdASN1 = decoder.decode(b64decode(passw)) + iv = passwdASN1[0][1][1].asOctets() + ciphertext = passwdASN1[0][2].asOctets() + password = re.sub( r'[^\\x\w]', '', DES3.new( key, DES3.MODE_CBC, iv).decrypt(ciphertext) ) + values["Password"] = password + + if len(values): + pwdFound.append(values) + # print the results - print_output(software_name, pwdFound) \ No newline at end of file + print_output(software_name, pwdFound) + diff --git a/Linux/src/softwares/browsers/opera.py b/Linux/src/softwares/browsers/opera.py index 780922b9..cf87739c 100644 --- a/Linux/src/softwares/browsers/opera.py +++ b/Linux/src/softwares/browsers/opera.py @@ -58,7 +58,7 @@ def decipher_old_version(self, path): # retrieve wand.dat file if not os.path.exists(path + os.sep + 'wand.dat'): - constant.file_logger.warn('wand.dat file has not been found.\n') + print_debug('WARNING', 'wand.dat file has not been found.') return # read wand.dat diff --git a/Linux/src/softwares/sysadmin/env_variable.py b/Linux/src/softwares/sysadmin/env_variable.py index 806d5a01..898127aa 100644 --- a/Linux/src/softwares/sysadmin/env_variable.py +++ b/Linux/src/softwares/sysadmin/env_variable.py @@ -7,14 +7,14 @@ class Env_variable(ModuleInfo): def __init__(self): options = {'command': '-e', 'action': 'store_true', 'dest': 'env', 'help': 'environment variables'} - ModuleInfo.__init__(self, 'Environnement variables', 'sysadmin', options) + ModuleInfo.__init__(self, 'Environment variables', 'sysadmin', options) def run(self): values = {} pwdFound = [] # print the title - Header().title_info('Environnement variables') + Header().title_info('Environment variables') # --------- http_proxy -------- tmp = '' diff --git a/Linux/standalone/32bits/LaZagne-32bits.REMOVED.git-id b/Linux/standalone/32bits/LaZagne-32bits.REMOVED.git-id index 22d8b454..3fd6aa30 100644 --- a/Linux/standalone/32bits/LaZagne-32bits.REMOVED.git-id +++ b/Linux/standalone/32bits/LaZagne-32bits.REMOVED.git-id @@ -1 +1 @@ -3d2330db4e0f58d8d940c92baef27353ad3c64cf \ No newline at end of file +21e7d30bf539ef20c4ba4145747c89c99fb9be7d \ No newline at end of file diff --git a/Linux/standalone/64bits/LaZagne-64bits.REMOVED.git-id b/Linux/standalone/64bits/LaZagne-64bits.REMOVED.git-id index b44f8c79..951761bc 100644 --- a/Linux/standalone/64bits/LaZagne-64bits.REMOVED.git-id +++ b/Linux/standalone/64bits/LaZagne-64bits.REMOVED.git-id @@ -1 +1 @@ -2e176144f750de5c9d67ddcc8c1a70f966226664 \ No newline at end of file +c7346f7b8f22075132dfac8727b44a264e5d8ddc \ No newline at end of file diff --git a/README.md b/README.md index 176988df..b495135c 100644 --- a/README.md +++ b/README.md @@ -73,28 +73,20 @@ Requirements To compile the source code, some external libraries are required. * For Windows - * colorama (for the Console colors) - * https://pypi.python.org/pypi/colorama - - * Python for Windows Extensions - * http://sourceforge.net/projects/pywin32/ - - * pycrypto - * pip install pycrypto - - * Impacket (for Windows hashes + LSA Secrets) - * https://github.com/CoreSecurity/impacket - -* For Linux - * For Ubuntu 14.04 - * python-kde4 (Kwallet) - - * Other distributions - * Python 2.7 - * argparse - * Crypto - * dbus (Pidgin) - * python-kde4 (Kwallet) + * Python 2.7 + * Colorama (for the Console colors): https://pypi.python.org/pypi/colorama + * Python for Windows Extensions: http://sourceforge.net/projects/pywin32/ + * PyCrypto: pip install pycrypto + * Impacket (for Windows hashes + LSA Secrets): https://github.com/CoreSecurity/impacket + * Pyasn1 (for ASN1 decoding): https://pypi.python.org/pypi/pyasn1/ + +* For Linux + * Python 2.7 + * Argparse + * PyCrypto: https://www.dlitz.net/software/pycrypto/ + * Dbus (Pidgin) + * Python-kde4 (Kwallet) + * Pyasn1 (for ASN1 decoding): https://pypi.python.org/pypi/pyasn1/ ---- | __Alessandro ZANNI__ | diff --git a/Windows/src/LaZagne/config/constant.py b/Windows/src/LaZagne/config/constant.py index c1914dd4..0573644f 100644 --- a/Windows/src/LaZagne/config/constant.py +++ b/Windows/src/LaZagne/config/constant.py @@ -2,7 +2,7 @@ class constant(): folder_name = 'results' MAX_HELP_POSITION = 27 - CURRENT_VERSION = 0.5 + CURRENT_VERSION = 0.6 output = None file_logger = None diff --git a/Windows/src/LaZagne/laZagne.py b/Windows/src/LaZagne/laZagne.py index c46ceade..fa01b530 100644 --- a/Windows/src/LaZagne/laZagne.py +++ b/Windows/src/LaZagne/laZagne.py @@ -27,7 +27,7 @@ # Define a dictionary for all modules modules = {} -for categoryName in category.keys(): +for categoryName in category: modules[categoryName] = {} # Add all modules to the dictionary @@ -64,15 +64,14 @@ def verbosity(): def launch_module(b): ok = False # Launch only a specific module - for i in args.keys(): - if args[i]: - if i in b.keys(): - b[i].run() - ok = True + for i in args: + if args[i] and i in b: + b[i].run() + ok = True # Launch all modules if not ok: - for i in b.keys(): + for i in b: b[i].run() def manage_advanced_options(): @@ -103,7 +102,7 @@ def runModule(): # Run all def runAllModules(): manage_advanced_options() - for categoryName in category.keys(): + for categoryName in category: if categoryName == 'browsers': constant.mozilla_software = 'Firefox' elif categoryName == 'mails': @@ -133,7 +132,7 @@ def error(self, message): # ------------------------------------------- Add options and suboptions to all modules ------------------------------------------- all_subparser = [] -for c in category.keys(): +for c in category: category[c]['parser'] = argparse.ArgumentParser(add_help=False,formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=constant.MAX_HELP_POSITION)) category[c]['parser']._optionals.title = category[c]['help'] @@ -160,7 +159,7 @@ def error(self, message): # ------------------------------------------- Print all ------------------------------------------- parents = [PPoptional] + all_subparser + [PWrite] dic = {'all':{'parents':parents, 'help':'Run all modules', 'func': runAllModules}} -for c in category.keys(): +for c in category: parser_tab = [PPoptional, category[c]['parser']] if 'subparser' in category[c]: if category[c]['subparser']: @@ -171,7 +170,7 @@ def error(self, message): #2- Main commands subparsers = parser.add_subparsers(help='Choose a main command') -for d in dic.keys(): +for d in dic: subparsers.add_parser(d,parents=dic[d]['parents'],help=dic[d]['help']).set_defaults(func=dic[d]['func'],auditType=d) # ------------------------------------------- Parse arguments ------------------------------------------- diff --git a/Windows/src/LaZagne/softwares/browsers/mozilla.py b/Windows/src/LaZagne/softwares/browsers/mozilla.py index e8495aeb..869bfe30 100644 --- a/Windows/src/LaZagne/softwares/browsers/mozilla.py +++ b/Windows/src/LaZagne/softwares/browsers/mozilla.py @@ -11,15 +11,19 @@ import shutil from config.dico import get_dico import itertools +#https://pypi.python.org/pypi/pyasn1/ +from pyasn1.codec.der import decoder +from struct import unpack +from binascii import hexlify, unhexlify +from hashlib import sha1 +import hmac +from Crypto.Util.number import long_to_bytes +from Crypto.Cipher import DES3 from config.header import Header from config.constant import * from config.write_output import print_debug, print_output from config.moduleInfo import ModuleInfo -# Password structures -class SECItem(Structure): - _fields_ = [('type', c_uint),('data', c_void_p),('len', c_uint)] - # Database classes database_find = False class Credentials(object): @@ -33,9 +37,10 @@ def __init__(self, db): if tmp: database_find = True f.close() - + def __iter__(self): pass + def done(self): pass @@ -81,18 +86,15 @@ class Mozilla(ModuleInfo): def __init__(self, isThunderbird = False): - self.credentials_categorie = None - self.slot = None - - self.username = SECItem() - self.passwd = SECItem() - self.dectext = SECItem() - + self.credentials_categorie = None + self.toCheck = [] self.manually_pass = None self.dictionnary_path = None self.number_toStop = None - + + self.key3 = '' + # Manage options suboptions = [ {'command': '-m', 'action': 'store', 'dest': 'manually', 'help': 'enter the master password manually', 'title': 'Advanced Mozilla master password options'}, @@ -108,14 +110,6 @@ def __init__(self, isThunderbird = False): else: options = {'command': '-t', 'action': 'store_true', 'dest': 'thunderbird', 'help': 'thunderbird'} ModuleInfo.__init__(self, 'thunderbird', 'browsers', options, suboptions) - - - - def __del__(self): - self.libnss = None - self.username = None - self.passwd = None - self.dectext = None def get_path(self, software_name): if 'APPDATA' in os.environ: @@ -150,54 +144,145 @@ def manage_advanced_options(self): self.toCheck = ['b', 'd'] self.number_toStop = 3 - def initialize_libnss(self, list_libnss, profile): - for lib in list_libnss: - try: - self.libnss = CDLL(lib) - if self.libnss.NSS_Init(profile) == 0: - return True - except Exception,e: - print_debug('DEBUG', '{0}'.format(e)) - return False - - def found_libnss(self): - list_libnss = [] - if 'ProgramFiles' in os.environ: - if os.path.exists(os.environ['ProgramFiles'] + '\Mozilla Firefox'): - path = os.environ['ProgramFiles'] + '\Mozilla Firefox' - if os.path.exists(os.path.join(path, 'nss3.dll')): - os.environ['PATH'] = ';'.join([path, os.environ['PATH']]) - list_libnss.append(os.path.join(path, 'nss3.dll')) - - if 'ProgramFiles(x86)' in os.environ: - if os.path.exists(os.environ['ProgramFiles(x86)'] + '\Mozilla Firefox'): - path = os.environ['ProgramFiles(x86)'] + '\Mozilla Firefox' - if os.path.exists(os.path.join(path, 'nss3.dll')): - os.environ['PATH'] = ';'.join([path, os.environ['PATH']]) - list_libnss.append(os.path.join(path, 'nss3.dll')) - - return list_libnss - - - def decrypt(self, software_name, credentials): - pwdFound = [] - for host, user, passw in credentials: - values = {} - values["Website"] = format(host.encode("utf-8")) - self.username.data = cast(c_char_p(b64decode(user)), c_void_p) - self.username.len = len(b64decode(user)) - self.passwd.data = cast(c_char_p(b64decode(passw)), c_void_p) - self.passwd.len = len(b64decode(passw)) - - if self.libnss.PK11SDR_Decrypt(byref(self.username), byref(self.dectext), None) != -1: - values["Username"] = string_at(self.dectext.data, self.dectext.len) - - if self.libnss.PK11SDR_Decrypt(byref(self.passwd), byref(self.dectext), None) != -1: - values["Password"] = string_at(self.dectext.data, self.dectext.len) - - if len(values): - pwdFound.append(values) - return pwdFound + # -------------------------------------------- + + def getShortLE(self, d, a): + return unpack('L',(d)[a:a+4])[0] + + def printASN1(self, d, l, rl): + type = ord(d[0]) + length = ord(d[1]) + if length&0x80 > 0: #http://luca.ntop.org/Teaching/Appunti/asn1.html, + nByteLength = length&0x7f + length = ord(d[2]) + #Long form. Two to 127 octets. Bit 8 of first octet has value "1" and bits 7-1 give the number of additional length octets. + skip=1 + else: + skip=0 + + if type==0x30: + seqLen = length + readLen = 0 + while seqLen>0: + len2 = self.printASN1(d[2+skip+readLen:], seqLen, rl+1) + seqLen = seqLen - len2 + readLen = readLen + len2 + return length+2 + elif type==6: #OID + return length+2 + elif type==4: #OCTETSTRING + return length+2 + elif type==5: #NULL + # print 0 + return length+2 + elif type==2: #INTEGER + return length+2 + else: + if length==l-2: + self.printASN1( d[2:], length, rl+1) + return length + + #extract records from a BSD DB 1.85, hash mode + def readBsddb(self, name): + f = open(name,'rb') + + #http://download.oracle.com/berkeley-db/db.1.85.tar.gz + header = f.read(4*15) + magic = self.getLongBE(header,0) + if magic != 0x61561: + print_debug('WARNING', 'Bad magic number') + return False + version = self.getLongBE(header,4) + if version !=2: + print_debug('WARNING', 'Bad version !=2 (1.85)') + return False + pagesize = self.getLongBE(header,12) + nkeys = self.getLongBE(header,0x38) + + readkeys = 0 + page = 1 + nval = 0 + val = 1 + db1 = [] + while (readkeys < nkeys): + f.seek(pagesize*page) + offsets = f.read((nkeys+1)* 4 +2) + offsetVals = [] + i=0 + nval = 0 + val = 1 + keys = 0 + while nval != val : + keys +=1 + key = self.getShortLE(offsets,2+i) + val = self.getShortLE(offsets,4+i) + nval = self.getShortLE(offsets,8+i) + offsetVals.append(key+ pagesize*page) + offsetVals.append(val+ pagesize*page) + readkeys += 1 + i += 4 + offsetVals.append(pagesize*(page+1)) + valKey = sorted(offsetVals) + for i in range( keys*2 ): + f.seek(valKey[i]) + data = f.read(valKey[i+1] - valKey[i]) + db1.append(data) + page += 1 + f.close() + db = {} + + for i in range( 0, len(db1), 2): + db[ db1[i+1] ] = db1[ i ] + + return db + + def decrypt3DES(self, globalSalt, masterPassword, entrySalt, encryptedData ): + #see http://www.drh-consultancy.demon.co.uk/key3.html + hp = sha1( globalSalt+masterPassword ).digest() + pes = entrySalt + '\x00'*(20-len(entrySalt)) + chp = sha1( hp+entrySalt ).digest() + k1 = hmac.new(chp, pes+entrySalt, sha1).digest() + tk = hmac.new(chp, pes, sha1).digest() + k2 = hmac.new(chp, tk+entrySalt, sha1).digest() + k = k1+k2 + iv = k[-8:] + key = k[:24] + + return DES3.new( key, DES3.MODE_CBC, iv).decrypt(encryptedData) + + def extractSecretKey(self, globalSalt, masterPassword, entrySalt): + + (globalSalt, masterPassword, entrySalt) = self.is_masterpassword_correct(masterPassword) + + if unhexlify('f8000000000000000000000000000001') not in self.key3: + return None + privKeyEntry = self.key3[ unhexlify('f8000000000000000000000000000001') ] + saltLen = ord( privKeyEntry[1] ) + nameLen = ord( privKeyEntry[2] ) + privKeyEntryASN1 = decoder.decode( privKeyEntry[3+saltLen+nameLen:] ) + data = privKeyEntry[3+saltLen+nameLen:] + self.printASN1(data, len(data), 0) + + #see https://github.com/philsmd/pswRecovery4Moz/blob/master/pswRecovery4Moz.txt + entrySalt = privKeyEntryASN1[0][0][1][0].asOctets() + privKeyData = privKeyEntryASN1[0][1].asOctets() + privKey = self.decrypt3DES( globalSalt, masterPassword, entrySalt, privKeyData ) + self.printASN1(privKey, len(privKey), 0) + + privKeyASN1 = decoder.decode( privKey ) + prKey= privKeyASN1[0][2].asOctets() + self.printASN1(prKey, len(prKey), 0) + prKeyASN1 = decoder.decode( prKey ) + id = prKeyASN1[0][1] + key = long_to_bytes( prKeyASN1[0][3] ) + + print_debug('DEBUG', 'key: %s' % repr(key)) + return key + + # -------------------------------------------- # Get the path list of the firefox profiles def get_firefox_profiles(self, directory): @@ -211,6 +296,7 @@ def get_firefox_profiles(self, directory): return profile_list def save_db(self, userpath): + # create the folder to save it by profile relative_path = constant.folder_name + os.sep + 'firefox' if not os.path.exists(relative_path): @@ -248,25 +334,18 @@ def save_db(self, userpath): # ------------------------------ Master Password Functions ------------------------------ - # check if a masterpassword is set - def is_masterpasswd_set(self): - password = '' - self.slot = self.libnss.PK11_GetInternalKeySlot() - self.libnss.PK11_Authenticate(self.slot, True, 0) - pw_good = self.libnss.PK11_CheckUserPassword(self.slot, c_char_p(password)) - self.libnss.PK11_FreeSlot(self.slot) - - # Not masterpassword set - if pw_good == 0: - return False - else: - return True - - def is_masterpassword_correct(self, pwd): - pw_good = self.libnss.PK11_CheckUserPassword(self.slot, c_char_p(pwd)) - if pw_good == 0: - return True - return False + def is_masterpassword_correct(self, masterPassword=''): + #see http://www.drh-consultancy.demon.co.uk/key3.html + pwdCheck = self.key3['password-check'] + entrySaltLen = ord(pwdCheck[1]) + entrySalt = pwdCheck[3: 3+entrySaltLen] + encryptedPasswd = pwdCheck[-16:] + globalSalt = self.key3['global-salt'] + cleartextData = self.decrypt3DES( globalSalt, masterPassword, entrySalt, encryptedPasswd ) + if cleartextData != 'password-check\x02\x02': + return ('', '', '') + + return (globalSalt, masterPassword, entrySalt) # Retrieve masterpassword def found_masterpassword(self): @@ -274,9 +353,9 @@ def found_masterpassword(self): # master password entered manually if 'm' in self.toCheck: print_debug('ATTACK', 'Check the password entered manually !') - if self.is_masterpassword_correct(self.manually_pass): - print_debug('FIND', 'Master password found: %s\n' % self.manually_pass) - return True + if self.is_masterpassword_correct(self.manually_pass)[0]: + print_debug('FIND', 'Master password found: %s' % self.manually_pass) + return self.manually_pass else: print_debug('WARNING', 'The Master password entered is not correct') @@ -285,26 +364,25 @@ def found_masterpassword(self): try: pass_file = open(self.dictionnary_path, 'r') num_lines = sum(1 for line in pass_file) - except Exception,e: - print_debug('DEBUG', '{0}'.format(e)) + except: print_debug('ERROR', 'Unable to open passwords file: %s' % str(self.dictionnary_path)) - return 1 + return False pass_file.close() print_debug('ATTACK', 'Dictionnary Attack !!! (%s words)' % str(num_lines)) try: with open(self.dictionnary_path) as f: for p in f: - if self.is_masterpassword_correct(p.strip()): - print_debug('FIND', 'Master password found: %s\n' % p.strip()) - return True + if self.is_masterpassword_correct(p.strip())[0]: + print_debug('FIND', 'Master password found: %s' % p.strip()) + return p.strip() except (KeyboardInterrupt, SystemExit): print 'INTERRUPTED!' print_debug('DEBUG', 'Dictionnary attack interrupted') except Exception,e: print_debug('DEBUG', '{0}'.format(e)) - pass + print_debug('WARNING', 'The Master password has not been found using the dictionnary attack') # 500 most used passwords @@ -314,9 +392,9 @@ def found_masterpassword(self): print_debug('ATTACK', '%d most used passwords !!! ' % num_lines) for word in wordlist: - if self.is_masterpassword_correct(word): - print_debug('FIND', 'Master password found: %s\n' % word.strip()) - return True + if self.is_masterpassword_correct(word)[0]: + print_debug('FIND', 'Master password found: %s' % word.strip()) + return word print_debug('WARNING', 'No password has been found using the default list') @@ -332,48 +410,44 @@ def found_masterpassword(self): while current <= self.number_toStop and pass_found == False: for i in itertools.product(tab, repeat=current): word = ''.join(map(str,i)) - if self.is_masterpassword_correct(word): - print_debug('FIND', 'Master password found: %s\n' % word.strip()) - return True + if self.is_masterpassword_correct(word)[0]: + print_debug('FIND', 'Master password found: %s' % word.strip()) + return word.strip() current+= 1 except (KeyboardInterrupt, SystemExit): print 'INTERRUPTED!' print_debug('INFO', 'Dictionnary attack interrupted') except Exception,e: print_debug('DEBUG', '{0}'.format(e)) + print_debug('WARNING', 'No password has been found using the brute force attack') - + return False + # ------------------------------ End of Master Password Functions ------------------------------ # main function def run(self): global database_find database_find = False - + self.manage_advanced_options() software_name = constant.mozilla_software specific_path = constant.specific_path + # print the title + Header().title_info(software_name) + # get the installation path path = self.get_path(software_name) if not path: print_debug('WARNING', 'Installation path not found') return - # print the title - Header().title_info(software_name) - - list_libnss = self.found_libnss() - - # Check if the libnss could be initialized well - if not list_libnss: - print_debug('WARNING', 'The libnss have not been initialized because the nss3.dll has not been found') - #Check if mozilla folder has been found elif not os.path.exists(path): print_debug('INFO', software_name + ' not installed.') - + return else: if specific_path: if os.path.exists(specific_path): @@ -387,44 +461,65 @@ def run(self): pwdFound = [] for profile in profile_list: print_debug('INFO', 'Profile path found: %s' % profile) - - if self.initialize_libnss(list_libnss, profile): - masterPwd = self.is_masterpasswd_set() - - # check if passwors are stored on the Json format + if not os.path.exists(profile + os.sep + 'key3.db'): + print_debug('WARNING', 'key3 file not found: %s' % self.key3) + return + + self.key3 = self.readBsddb(profile + os.sep + 'key3.db') + if not self.key3: + return + + # check if passwords are stored on the Json format + try: + credentials = JsonDatabase(profile) + except: + database_find = False + + if not database_find: + # check if passwords are stored on the sqlite format try: - credentials = JsonDatabase(profile) - except Exception,e: - print_debug('DEBUG', '{0}'.format(e)) + credentials = SqliteDatabase(profile) + except: database_find = False - if not database_find: - # check if passwors are stored on the sqlite format - try: - credentials = SqliteDatabase(profile) - except Exception,e: - print_debug('DEBUG', '{0}'.format(e)) - database_find = False + + if database_find: + masterPassword = '' + (globalSalt, masterPassword, entrySalt) = self.is_masterpassword_correct(masterPassword) + + # find masterpassword if set + if not globalSalt: + print_debug('WARNING', 'Master Password is used !') + masterPassword = self.found_masterpassword() + if not masterPassword: + return - if database_find: - print - if masterPwd: - print_debug('WARNING', 'A masterpassword is used !!') - masterPwdFound = self.found_masterpassword() + # get user secret key + key = self.extractSecretKey(globalSalt, masterPassword, entrySalt) + if not key: + return + + # everything is ready to decrypt password + for host, user, passw in credentials: + values = {} + values["Website"] = host + + # Login + loginASN1 = decoder.decode(b64decode(user)) + iv = loginASN1[0][1][1].asOctets() + ciphertext = loginASN1[0][2].asOctets() + login = re.sub( r'[^\\x\w]', '', DES3.new( key, DES3.MODE_CBC, iv).decrypt(ciphertext) ) + values["Username"] = login - if not masterPwd or masterPwdFound: - try: - # decrypt passwords on the db - pwdFound+=self.decrypt(software_name, credentials) - except Exception,e: - print_debug('DEBUG', '{0}'.format(e)) - - # if a master password is set (but not found), we save the db to bruteforce offline - elif masterPwd and not masterPwdFound and constant.output == 'txt': - self.save_db(profile) - - self.libnss.NSS_Shutdown() - else: - print_debug('ERROR', 'Could not initialize the NSS library\n') - + # Password + passwdASN1 = decoder.decode(b64decode(passw)) + iv = passwdASN1[0][1][1].asOctets() + ciphertext = passwdASN1[0][2].asOctets() + password = re.sub( r'[^\\x\w]', '', DES3.new( key, DES3.MODE_CBC, iv).decrypt(ciphertext) ) + values["Password"] = password + + if len(values): + pwdFound.append(values) + # print the results - print_output(software_name, pwdFound) \ No newline at end of file + print_output(software_name, pwdFound) + diff --git a/Windows/src/LaZagne/softwares/browsers/opera.py b/Windows/src/LaZagne/softwares/browsers/opera.py index ad3f218e..ce5db355 100644 --- a/Windows/src/LaZagne/softwares/browsers/opera.py +++ b/Windows/src/LaZagne/softwares/browsers/opera.py @@ -46,11 +46,10 @@ def run(self): if passwords: self.parse_results(passwords) else: - print_debug('The wand.dat seems to be empty') + print_debug('INFO', 'The wand.dat seems to be empty') # new versions else: passwords = self.decipher_new_version(path) - def get_path(self): global CIPHERED_FILE diff --git a/Windows/src/LaZagne/softwares/sysadmin/winscp.py b/Windows/src/LaZagne/softwares/sysadmin/winscp.py index 92b02759..81271a90 100644 --- a/Windows/src/LaZagne/softwares/sysadmin/winscp.py +++ b/Windows/src/LaZagne/softwares/sysadmin/winscp.py @@ -109,14 +109,14 @@ def get_logins_info(self): port = '22' try: password = self.decrypt_password() + values['Password'] = password except Exception,e: print_debug('DEBUG', '{0}'.format(e)) - password = 'N/A' values['Hostname'] = self.get_hostname() values['Port'] = port values['Username'] = self.get_username() - values['Password'] = password + pwdFound.append(values) # print the results