├── README ├── ffpassdecrypt.py └── firefox_passwd.py /README: -------------------------------------------------------------------------------- 1 | ffpassdecrypt - Decode the passwords stored using Firefox browser. The script currently works only on Linux. 2 | 3 | Author : Pradeep Nayak (pradeep1288@gmail.com) 4 | usage: ./ffpassdecrypt.py [paths_to_location_of_files] 5 | 6 | Run it with no parameters to extract the standard passwords from all profiles of the current logged in user, 7 | or with an optional '-P' argument (before any path) to query the master password for decryption. 8 | 9 | Required files: 10 | + key3.db 11 | + signons.sqlite 12 | + cert8.db 13 | are used and needed to collect the passwords. 14 | -------------------------------------------------------------------------------- /ffpassdecrypt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | ffpassdecrypt - Decode the passwords stored using Firefox browser. The script currently works only on Linux. 4 | 5 | Author : Pradeep Nayak (pradeep1288@gmail.com) 6 | usage: ./ffpassdecrypt.py [paths_to_location_of_files] 7 | 8 | Run it with no parameters to extract the standard passwords from all profiles of the current logged in user, 9 | or with an optional '-P' argument (before any path) to query the master password for decryption. 10 | 11 | Required files: 12 | + key3.db 13 | + signons.sqlite 14 | + cert8.db 15 | are used and needed to collect the passwords. 16 | 17 | """ 18 | 19 | import sys 20 | import os 21 | 22 | try: 23 | # try to use the python-nss lib from mozilla 24 | # import nss 25 | #except ImportError: 26 | # fall back to dlopen of libnss3.so 27 | from ctypes import ( 28 | CDLL, Structure, 29 | c_void_p, c_int, c_uint, c_ubyte, c_char_p, 30 | byref, cast, string_at, 31 | ) 32 | 33 | #### libnss definitions 34 | class SECItem(Structure): 35 | _fields_ = [('type',c_uint),('data',c_void_p),('len',c_uint)] 36 | 37 | class secuPWData(Structure): 38 | _fields_ = [('source',c_ubyte),('data',c_char_p)] 39 | 40 | (PW_NONE, PW_FROMFILE, PW_PLAINTEXT, PW_EXTERNAL) = (0, 1, 2, 3) 41 | # SECStatus 42 | (SECWouldBlock, SECFailure, SECSuccess) = (-2, -1, 0) 43 | #### end of libnss definitions 44 | 45 | #except ImportError as e: 46 | # print 'Failed to find either nss or ctypes library.' 47 | # raise 48 | except ImportError: pass 49 | 50 | try: 51 | from sqlite3 import dbapi2 as sqlite 52 | except ImportError: 53 | from pysqlite2 import dbapi2 as sqlite 54 | 55 | import base64 56 | import struct 57 | import glob 58 | import re 59 | import time 60 | 61 | import getopt 62 | from getpass import getpass 63 | 64 | 65 | def findpath_userdirs(profiledir='~/.mozilla/firefox'): 66 | usersdir = os.path.expanduser(profiledir) 67 | userdir = os.listdir(usersdir) 68 | res=[] 69 | for user in userdir: 70 | if os.path.isdir(usersdir + os.sep + user): 71 | res.append(usersdir + os.sep + user) 72 | return res 73 | 74 | def errorlog(row, path, libnss): 75 | print "----[-]Error while Decoding! writting error.log:" 76 | print libnss.PORT_GetError() 77 | try: 78 | f=open('error.log','a') 79 | f.write("-------------------\n") 80 | f.write("#ERROR in: %s at %s\n" %(path,time.ctime())) 81 | f.write("Site: %s\n"%row[1]) 82 | f.write("Username: %s\n"%row[6]) 83 | f.write("Password: %s\n"%row[7]) 84 | f.write("-------------------\n") 85 | f.close() 86 | except IOError: 87 | print "Error while writing logfile - No log created!" 88 | 89 | 90 | 91 | # reads the signons.sqlite which is a sqlite3 Database (>Firefox 3) 92 | def readsignonDB(directory, dbname, use_pass, libnss): 93 | profile = os.path.split(directory)[-1] 94 | 95 | if libnss.NSS_Init(directory) != 0: 96 | print 'Could not initialize NSS for "%s"' % profile 97 | 98 | print "Profile directory: %s" % profile 99 | 100 | keySlot = libnss.PK11_GetInternalKeySlot() 101 | libnss.PK11_CheckUserPassword(keySlot, getpass() if use_pass else '') 102 | libnss.PK11_Authenticate(keySlot, True, 0) 103 | 104 | uname = SECItem() 105 | passwd = SECItem() 106 | dectext = SECItem() 107 | 108 | pwdata = secuPWData() 109 | pwdata.source = PW_NONE 110 | pwdata.data = 0 111 | 112 | signons_db = directory+os.sep+dbname 113 | conn = sqlite.connect(signons_db) 114 | c = conn.cursor() 115 | c.execute("SELECT * FROM moz_logins;") 116 | for row in c: 117 | print "--Site(%s):"%row[1] 118 | uname.data = cast(c_char_p(base64.b64decode(row[6])),c_void_p) 119 | uname.len = len(base64.b64decode(row[6])) 120 | passwd.data = cast(c_char_p(base64.b64decode(row[7])),c_void_p) 121 | passwd.len=len(base64.b64decode(row[7])) 122 | if libnss.PK11SDR_Decrypt(byref(uname),byref(dectext),byref(pwdata))==-1: 123 | errorlog(row, signons_db, libnss) 124 | print "----Username %s" % string_at(dectext.data,dectext.len) 125 | if libnss.PK11SDR_Decrypt(byref(passwd),byref(dectext),byref(pwdata))==-1: 126 | errorlog(row, signons_db, libnss) 127 | print "----Password %s" % string_at(dectext.data,dectext.len) 128 | c.close() 129 | conn.close() 130 | libnss.NSS_Shutdown() 131 | 132 | 133 | def main(): 134 | 135 | try: 136 | optlist, args = getopt.getopt(sys.argv[1:], 'P') 137 | except getopt.GetoptError as err: 138 | # print help information and exit: 139 | print str(err) # will print something like "option -a not recognized" 140 | usage() 141 | sys.exit(2) 142 | 143 | 144 | if len(args)==0: 145 | ordner = findpath_userdirs() 146 | else: 147 | ordner = args 148 | 149 | use_pass = False 150 | for o, a in optlist: 151 | if o == '-P': 152 | use_pass = True 153 | 154 | # dlopen libnss3 155 | libnss = CDLL("libnss3.so") 156 | 157 | # Set function profiles 158 | 159 | libnss.PK11_GetInternalKeySlot.restype = c_void_p 160 | libnss.PK11_CheckUserPassword.argtypes = [c_void_p, c_char_p] 161 | libnss.PK11_Authenticate.argtypes = [c_void_p, c_int, c_void_p] 162 | 163 | for user in ordner: 164 | signonfiles = glob.glob(user + os.sep + "signons*.*") 165 | for signonfile in signonfiles: 166 | (filepath,filename) = os.path.split(signonfile) 167 | filetype = re.findall('\.(.*)',filename)[0] 168 | if filetype.lower() == "sqlite": 169 | readsignonDB(filepath, filename, use_pass, libnss) 170 | else: 171 | print 'Unhandled signons file "%s", skipping' % filename 172 | 173 | if __name__ == '__main__': 174 | main() 175 | -------------------------------------------------------------------------------- /firefox_passwd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Recovers your Firefox or Thunderbird passwords 4 | 5 | Author : Tobias Mueller 6 | """ 7 | 8 | import sys 9 | import os 10 | 11 | try: 12 | # try to use the python-nss lib from mozilla 13 | # import nss 14 | #except ImportError: 15 | # fall back to dlopen of libnss3.so 16 | from ctypes import ( 17 | CDLL, Structure, 18 | c_void_p, c_uint, c_ubyte, c_char_p, 19 | byref, cast, string_at, 20 | ) 21 | 22 | #### libnss definitions 23 | class SECItem(Structure): 24 | _fields_ = [('type',c_uint),('data',c_void_p),('len',c_uint)] 25 | 26 | class secuPWData(Structure): 27 | _fields_ = [('source',c_ubyte),('data',c_char_p)] 28 | 29 | (PW_NONE, PW_FROMFILE, PW_PLAINTEXT, PW_EXTERNAL) = (0, 1, 2, 3) 30 | # SECStatus 31 | (SECWouldBlock, SECFailure, SECSuccess) = (-2, -1, 0) 32 | #### end of libnss definitions 33 | 34 | #except ImportError as e: 35 | # print 'Failed to find either nss or ctypes library.' 36 | # raise 37 | except ImportError: pass 38 | 39 | try: 40 | from sqlite3 import dbapi2 as sqlite 41 | except ImportError: 42 | from pysqlite2 import dbapi2 as sqlite 43 | 44 | import base64 45 | from getpass import getpass 46 | import logging 47 | from optparse import OptionParser 48 | from collections import namedtuple 49 | from ConfigParser import RawConfigParser, NoOptionError 50 | 51 | from subprocess import Popen, CalledProcessError, PIPE 52 | 53 | 54 | LOGLEVEL_DEFAULT = 'warn' 55 | 56 | log = logging.getLogger() 57 | #PWDECRYPT = 'pwdecrypt' 58 | PWDECRYPT = '/usr/bin/pwdecrypt' # from libnss3-tools 59 | 60 | SITEFIELDS = ['id', 'hostname', 'httpRealm', 'formSubmitURL', 'usernameField', 'passwordField', 'encryptedUsername', 'encryptedPassword', 'guid', 'encType', 'plain_username', 'plain_password' ] 61 | Site = namedtuple('FirefoxSite', SITEFIELDS) 62 | '''The format of the SQLite database is (2011): 63 | (id INTEGER PRIMARY KEY, 64 | hostname TEXT NOT NULL, 65 | httpRealm TEXT, 66 | formSubmitURL TEXT, 67 | usernameField TEXT NOT NULL, 68 | passwordField TEXT NOT NULL, 69 | encryptedUsername TEXT NOT NULL, 70 | encryptedPassword TEXT NOT NULL, 71 | guid TEXT, 72 | encType INTEGER); 73 | ''' 74 | 75 | 76 | def get_default_firefox_profile_directory(profiledir='~/.mozilla/firefox'): 77 | """Returns the directory name of the default profile 78 | 79 | If you changed the default dir to something like ~/.thunderbird, 80 | you would get the Thunderbird default profile directory. 81 | """ 82 | 83 | profiles_dir = os.path.expanduser(profiledir) 84 | profile_path = None 85 | 86 | cp = RawConfigParser() 87 | cp.read(os.path.join(profiles_dir, "profiles.ini")) 88 | for section in cp.sections(): 89 | if not cp.has_option(section, "Path"): 90 | continue 91 | 92 | if (not profile_path or 93 | (cp.has_option(section, "Default") and cp.get(section, "Default").strip() == "1")): 94 | profile_path = os.path.join(profiles_dir, cp.get(section, "Path").strip()) 95 | 96 | if not profile_path: 97 | raise RuntimeError("Cannot find default Firefox profile") 98 | 99 | return profile_path 100 | 101 | 102 | def get_encrypted_sites(firefox_profile_dir=None): 103 | """Opens signons.sqlite and yields encryped password data""" 104 | 105 | if firefox_profile_dir is None: 106 | firefox_profile_dir = get_default_firefox_profile_directory() 107 | signons_db = os.path.join(firefox_profile_dir, "signons.sqlite") 108 | query = '''SELECT id, hostname, httpRealm, formSubmitURL, 109 | usernameField, passwordField, encryptedUsername, 110 | encryptedPassword, guid, encType, 'noplainuser', 'noplainpasswd' FROM moz_logins;''' 111 | 112 | # We don't want to type out all the column from the DB as we have 113 | ## stored them in the SITEFIELDS already. However, we have two 114 | ## components extra, the plain usename and password. So we remove 115 | ## that from the list, because the table doesn't have that column. 116 | ## And we add two literal SQL strings to make our "Site" data 117 | ## structure happy 118 | #queryfields = SITEFIELDS[:-2] + ["'noplainuser'", "'noplainpassword'"] 119 | #query = '''SELECT %s 120 | # FROM moz_logins;''' % ', '.join(queryfields) 121 | 122 | conn = sqlite.connect(signons_db) 123 | try: 124 | cursor = conn.cursor() 125 | cursor.execute(query) 126 | 127 | for site in map(Site._make, cursor.fetchall()): 128 | yield site 129 | finally: 130 | conn.close() 131 | 132 | def decrypt(encrypted_string, firefox_profile_directory, password = None): 133 | """Opens an external tool to decrypt strings 134 | 135 | This is mostly for historical reasons or if the API changes. It is 136 | very slow because it needs to call out a lot. It uses the 137 | "pwdecrypt" tool which you might have packaged. Otherwise, you 138 | need to build it yourself. 139 | """ 140 | 141 | log = logging.getLogger('firefoxpasswd.decrypt') 142 | execute = [PWDECRYPT, '-d', firefox_profile_directory] 143 | if password: 144 | execute.extend(['-p', password]) 145 | process = Popen(execute, stdin=PIPE, stdout=PIPE, stderr=PIPE) 146 | output, error = process.communicate(encrypted_string) 147 | 148 | log.debug('Sent: %s', encrypted_string) 149 | log.debug('Got: %s', output) 150 | 151 | NEEDLE = 'Decrypted: "' # This string is prepended to the decrypted password if found 152 | output = output.strip() 153 | if output == encrypted_string: 154 | log.error('Password was not correct. Please try again without a ' 155 | 'password or with the correct one') 156 | 157 | index = output.index(NEEDLE) + len(NEEDLE) 158 | password = output[index:-1] # And we strip the final quotation mark 159 | 160 | return password 161 | 162 | 163 | class NativeDecryptor(object): 164 | """Calls the NSS API to decrypt strings""" 165 | 166 | def __init__(self, directory, password = ''): 167 | """You need to give the profile directory and optionally a 168 | password. If you don't give a password but one is needed, you 169 | will be prompted by getpass to provide one. 170 | """ 171 | self.directory = directory 172 | self.log = logging.getLogger('NativeDecryptor') 173 | self.log.debug('Trying to work on %s', directory) 174 | 175 | self.libnss = CDLL('libnss3.so') 176 | if self.libnss.NSS_Init(directory) != 0: 177 | self.log.error('Could not initialize NSS') 178 | 179 | # Initialize to the empty string, not None, because the password 180 | # function expects rather an empty string 181 | self.password = password = password or '' 182 | 183 | 184 | slot = self.libnss.PK11_GetInternalKeySlot() 185 | 186 | pw_good = self.libnss.PK11_CheckUserPassword(slot, c_char_p(password)) 187 | while pw_good != SECSuccess: 188 | msg = 'Password is not good (%d)!' % pw_good 189 | print >>sys.stderr, msg 190 | password = getpass('Please enter password: ') 191 | pw_good = self.libnss.PK11_CheckUserPassword(slot, c_char_p(password)) 192 | #raise RuntimeError(msg) 193 | 194 | # That's it, we're done with passwords, but we leave the old 195 | # code below in, for nostalgic reasons. 196 | 197 | if password is None: 198 | pwdata = secuPWData() 199 | pwdata.source = PW_NONE 200 | pwdata.data = 0 201 | else: 202 | # It's not clear whether this actually works 203 | pwdata = secuPWData() 204 | pwdata.source = PW_PLAINTEXT 205 | pwdata.data = c_char_p (password) 206 | # It doesn't actually work :-( 207 | 208 | 209 | # Now follow some attempts that were not succesful! 210 | def setpwfunc(): 211 | # One attempt was to use PK11PassworFunc. Didn't work. 212 | def password_cb(slot, retry, arg): 213 | #s = self.libnss.PL_strdup(password) 214 | s = self.libnss.PL_strdup("foo") 215 | return s 216 | 217 | PK11PasswordFunc = CFUNCTYPE(c_void_p, PRBool, c_void_p) 218 | c_password_cb = PK11PasswordFunc(password_cb) 219 | #self.libnss.PK11_SetPasswordFunc(c_password_cb) 220 | 221 | 222 | # To be ignored 223 | def changepw(): 224 | # Another attempt was to use ChangePW. Again, no effect. 225 | #ret = self.libnss.PK11_ChangePW(slot, pwdata.data, 0); 226 | ret = self.libnss.PK11_ChangePW(slot, password, 0) 227 | if ret == SECFailure: 228 | raise RuntimeError('Setting password failed! %s' % ret) 229 | 230 | #self.pwdata = pwdata 231 | 232 | 233 | def __del__(self): 234 | self.libnss.NSS_Shutdown() 235 | 236 | 237 | def decrypt(self, string, *args): 238 | """Decrypts a given string""" 239 | 240 | libnss = self.libnss 241 | 242 | uname = SECItem() 243 | dectext = SECItem() 244 | #pwdata = self.pwdata 245 | 246 | cstring = SECItem() 247 | cstring.data = cast( c_char_p( base64.b64decode(string)), c_void_p) 248 | cstring.len = len(base64.b64decode(string)) 249 | #if libnss.PK11SDR_Decrypt (byref (cstring), byref (dectext), byref (pwdata)) == -1: 250 | self.log.debug('Trying to decrypt %s (error: %s)', string, libnss.PORT_GetError()) 251 | if libnss.PK11SDR_Decrypt (byref (cstring), byref (dectext)) == -1: 252 | error = libnss.PORT_GetError() 253 | libnss.PR_ErrorToString.restype = c_char_p 254 | error_str = libnss.PR_ErrorToString(error) 255 | raise Exception ("%d: %s" % (error, error_str)) 256 | 257 | decrypted_data = string_at(dectext.data, dectext.len) 258 | 259 | return decrypted_data 260 | 261 | 262 | def encrypted_sites(self): 263 | """Yields the encryped passwords from the profile""" 264 | sites = get_encrypted_sites(self.directory) 265 | 266 | return sites 267 | 268 | 269 | def decrypted_sites(self): 270 | """Decrypts the encrypted_sites and yields the results""" 271 | 272 | sites = self.encrypted_sites() 273 | 274 | for site in sites: 275 | plain_user = self.decrypt(site.encryptedUsername) 276 | plain_password = self.decrypt(site.encryptedPassword) 277 | site = site._replace(plain_username=plain_user, 278 | plain_password=plain_password) 279 | 280 | yield site 281 | 282 | 283 | def get_firefox_sites_with_decrypted_passwords(firefox_profile_directory = None, password = None): 284 | """decryption of passwords using the external pwdecrypt program""" 285 | if not firefox_profile_directory: 286 | firefox_profile_directory = get_default_firefox_profile_directory() 287 | #decrypt = NativeDecryptor(firefox_profile_directory).decrypt 288 | for site in get_encrypted_sites(firefox_profile_directory): 289 | plain_user = decrypt(site.encryptedUsername, firefox_profile_directory, password) 290 | plain_password = decrypt(site.encryptedPassword, firefox_profile_directory, password) 291 | site = site._replace(plain_username=plain_user, plain_password=plain_password) 292 | log.debug("Dealing with Site: %r", site) 293 | log.info("user: %s, passwd: %s", plain_user, plain_password) 294 | yield site 295 | 296 | def main_decryptor(firefox_profile_directory, password, thunderbird=False): 297 | """Main function to get Firefox and Thunderbird passwords""" 298 | if not firefox_profile_directory: 299 | if thunderbird: 300 | dir = '~/.thunderbird/' 301 | else: 302 | dir = '~/.mozilla/firefox' 303 | firefox_profile_directory = get_default_firefox_profile_directory(dir) 304 | 305 | decryptor = NativeDecryptor(firefox_profile_directory, password) 306 | 307 | for site in decryptor.decrypted_sites(): 308 | print site 309 | 310 | def main(): 311 | parser = OptionParser() 312 | parser.add_option("-d", "--directory", default=None, 313 | help="the Firefox profile directory to use") 314 | parser.add_option("-p", "--password", default=None, 315 | help="the master password for the Firefox profile") 316 | parser.add_option("-l", "--loglevel", default=LOGLEVEL_DEFAULT, 317 | help="the level of logging detail [debug, info, warn, critical, error]") 318 | parser.add_option("-t", "--thunderbird", default=False, action='store_true', 319 | help="by default we try to find the Firefox default profile." 320 | " But you can as well ask for Thunderbird's default profile." 321 | " For a more reliable way, give the directory with -d.") 322 | parser.add_option("-n", "--native", default=True, action='store_true', 323 | help="use the native decryptor, i.e. make Python use " 324 | "libnss directly instead of invoking the helper program" 325 | "DEFUNCT! this option will not be checked.") 326 | parser.add_option("-e", "--external", default=False, action='store_true', 327 | help="use an external program `pwdecrypt' to actually " 328 | "decrypt the passwords. This calls out a lot and is dead " 329 | "slow. " 330 | "You need to use this method if you have a password " 331 | "protected database though.") 332 | options, args = parser.parse_args() 333 | 334 | loglevel = {'debug': logging.DEBUG, 'info': logging.INFO, 335 | 'warn': logging.WARN, 'critical':logging.CRITICAL, 336 | 'error': logging.ERROR}.get(options.loglevel, LOGLEVEL_DEFAULT) 337 | logging.basicConfig(level=loglevel) 338 | log = logging.getLogger() 339 | 340 | password = options.password 341 | 342 | if not options.external: 343 | sys.exit (main_decryptor(options.directory, password, thunderbird=options.thunderbird)) 344 | else: 345 | for site in get_firefox_sites_with_decrypted_passwords(options.directory, password): 346 | print site 347 | 348 | if __name__ == '__main__': 349 | main() 350 | --------------------------------------------------------------------------------