The finale. A KAPE (Kroll Artifact Parser and Extractor) triage of "Vera's" abandoned Windows laptop (Room 214). The chain: the browser remembered a password (a Chrome saved login), which we recover offline via DPAPI (Data Protection API) - but that needs Vera's Windows password, cracked from the SAM (Security Account Manager). The recovered saved password opens a VeraCrypt volume (fittingly named "Vera"), and inside, a fake invoice's image carries the flag. No brute force on the container - "some hidden files just need a really good memory."
It was always her. It was never a bug; it was the business model.
Housekeeping found a guest's laptop left behind after an early checkout, Room 214, registered to a "Vera." IT pulled a full triage before wiping it for the next guest.
Hunt down the artifacts scattered across her machine and figure out how they fit together. Somewhere in that trail is a password she never meant to leave behind. Follow it, and it'll open a door to something she was keeping very quiet.
Today's itinerary: Take a closer look at what she left behind. Some things aren't as locked away as she thought. Find out what she was hiding, and claim the flag.
A KAPE triage archive. All commands below use two shell variables for the triage paths:
$ T=".../14-Forensics-ManagementWantsAWord/management-wants-a-word-forensics-hh-day-14"
$ C="$T/KAPE/C"
$ find "$C" -maxdepth 4 -iname 'backup' -o -iname 'Login Data' -o -iname 'Local State' | sed "s#$C#C#"
C/Users/vera/Documents/backup
C/Users/vera/AppData/Local/Google/Chrome For Testing/User Data/Default/Login Data
C/Users/vera/AppData/Local/Google/Chrome For Testing/User Data/Local State
None. This is an offline triage - registry hives, a Chrome "For Testing" profile, DPAPI keys, and one suspicious 100 MiB file:
$ file "$C/Users/vera/Documents/backup"
.../backup: data
$ wc -c < "$C/Users/vera/Documents/backup"
104857600
$ xxd "$C/Users/vera/Documents/backup" | head -1
00000000: f372 f7cc d607 4b17 a8aa 8865 12af abdf .r....K....e....
Conclusion: 100 MiB, no header, high entropy - a headerless encrypted container. With the "Vera" theme, a strong bet on a VeraCrypt volume. We need its password.
The room hint: "a browser will remember things you never told anyone else." Chrome's
Login Data (SQLite) holds saved logins; the password blob is encrypted, but the
metadata is not.
$ cp "$C/Users/vera/AppData/Local/Google/Chrome For Testing/User Data/Default/Login Data" /tmp/logins.db
$ sqlite3 /tmp/logins.db "SELECT origin_url, username_value, substr(hex(password_value),1,6) FROM logins;"
http://bytelotus.thm:8080/|VeraSecretVault|763130
Conclusion: one saved login for account VeraSecretVault. The blob starts
763130 = "v10" - Chrome AES-256-GCM, key wrapped by DPAPI. Decrypting it
needs Vera's Windows password.
DPAPI for a local account is unlocked by the user's password. Pull the NTLM (NT LAN Manager) hash from the SAM (bootkey from SYSTEM) with impacket, then crack it.
$ python3 01-ntlm.py "$C/Windows/System32/config"
bootkey: 0f6f73ce89c8cda52d06fcc5131e040f
Administrator:500:...:1241186a4aac4f34f4bf7ace71b396a8:::
vera:1000:...:1241186a4aac4f34f4bf7ace71b396a8:::
$ python3 02-crack.py 1241186a4aac4f34f4bf7ace71b396a8 ~/work/CTF-CaptureTheFlag/wordlists/rockyou.txt
password: minivera
Conclusion: Vera's password is minivera (and Administrator reuses it). NTLM =
MD4 of the UTF-16LE password; rockyou hits it in seconds.
With the password: decrypt Vera's DPAPI master key, use it to unwrap Chrome's AES key from
Local State, then AES-256-GCM the saved v10 blob.
$ PW=$(sqlite3 /tmp/logins.db "SELECT hex(password_value) FROM logins;")
$ python3 03-dpapi-chrome.py "$C/Users/vera" \
S-1-5-21-2529683458-431225740-1723070931-1000 minivera "$PW"
saved password: Wh4t1sV3raD0inG0nTh1sH0st
Conclusion: the vault password is Wh4t1sV3raD0inG0nTh1sH0st - no cracking of
the container itself, exactly as the hint promised.
VeraCrypt's macOS mount needs macFUSE (Filesystem in Userspace) and admin. Instead, decrypt the volume directly in Python: derive the header key with PBKDF2 (Password-Based Key Derivation Function 2; HMAC-SHA512, 500000 iterations), decrypt the header with AES-256-XTS (XTS = XEX-based tweaked codebook with ciphertext stealing), recover the master key, then XTS-decrypt every 512-byte sector to a raw image.
$ python3 04-veracrypt-decrypt.py "$C/Users/vera/Documents/backup" Wh4t1sV3raD0inG0nTh1sH0st /tmp/vol.img
decrypted -> /tmp/vol.img
$ file /tmp/vol.img
/tmp/vol.img: DOS/MBR boot sector, OEM-ID "MSDOS5.0", ... FAT (32 bit)
The decrypted image is a plain FAT32 partition, so it mounts with no password (it is already
cleartext) via macOS hdiutil:
$ hdiutil attach -imagekey diskimage-class=CRawDiskImage /tmp/vol.img
/dev/disk8 /Volumes/NO NAME
The decrypted image mounted as "NO NAME" - one folder, secret_financial_documents.
Conclusion: the container is open, filesystem browsable. (Header params confirmed: AES-256-XTS, HMAC-SHA512, 500000 iterations, encrypted area start 131072.)
Inside are a decoy CSV and a PDF "invoice". strings finds no flag - because it is
rendered as an image inside the PDF (the CSV even nudges at it: "Image asset correction").
Render the PDF to see it.
$ ls "/Volumes/NO NAME/secret_financial_documents/"
important_invoice_byte_lotus.pdf transactions_q3.csv
$ cp "/Volumes/NO NAME/secret_financial_documents/important_invoice_byte_lotus.pdf" /tmp/invoice.pdf
$ hdiutil detach "/Volumes/NO NAME"
$ pdftoppm -r 200 -png /tmp/invoice.pdf /tmp/invoice # or: qlmanage -t -o /tmp /tmp/invoice.pdf
The recovered documents and the rendered invoice - the flag is the line item's description.
FLAG = THM{1t_w4s_V3r4_A11_Al0ng?!}
Conclusion: "it was Vera all along?!" - the leet o is a zero
(Al0ng); in the rasterised font the zero reads almost like an uppercase O.
The four scripts (saved in the challenge directory), verbatim.
01-ntlm.py - local NTLM hashes from the SAM + SYSTEM hives (impacket):
#!/usr/bin/env python3
# Extract local NTLM hashes from a KAPE triage (SAM + SYSTEM), using impacket.
# usage: python3 01-ntlm.py <triage>/KAPE/C/Windows/System32/config
import sys
from impacket.examples.secretsdump import LocalOperations, SAMHashes
cfg = sys.argv[1]
boot = LocalOperations(cfg + "/SYSTEM").getBootKey()
print("bootkey:", boot.hex())
SAMHashes(cfg + "/SAM", boot, isRemote=False).dump()
02-crack.py - crack an NTLM hash against rockyou (NTLM = MD4 of the UTF-16LE password):
#!/usr/bin/env python3
# Crack an NTLM hash against rockyou (NTLM = MD4 of UTF-16LE password).
# usage: python3 02-crack.py <ntlm-hex> <wordlist>
import sys, hashlib
target, wl = sys.argv[1].lower(), sys.argv[2]
with open(wl, 'rb') as f:
for line in f:
p = line.rstrip(b'\r\n')
try: s = p.decode('utf-8')
except UnicodeDecodeError: continue
if hashlib.new('md4', s.encode('utf-16-le')).hexdigest() == target:
print("password:", s); break
else:
print("not found")
03-dpapi-chrome.py - offline Chrome "v10" saved-password decryption (DPAPI + AES-256-GCM):
#!/usr/bin/env python3
# Offline-decrypt a Chrome "v10" saved password from a triage, given the user's
# Windows password: DPAPI master key -> Local State AES key -> AES-256-GCM password.
# usage: python3 03-dpapi-chrome.py <triage-user-dir> <SID> <password> <pw_value_hex>
import sys, os, json, base64, glob
from impacket.dpapi import MasterKeyFile, MasterKey, DPAPI_BLOB, deriveKeysFromUser
from Crypto.Cipher import AES
user, sid, pw, pw_hex = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
mkfile = glob.glob(os.path.join(user, "AppData/Roaming/Microsoft/Protect", sid, "????????-????-????-????-????????????"))[0]
lsfile = os.path.join(user, "AppData/Local/Google/Chrome For Testing/User Data/Local State")
data = open(mkfile, 'rb').read()
mkf = MasterKeyFile(data); data = data[len(mkf):]
mk = MasterKey(data[:mkf['MasterKeyLen']])
dk = None
for k in deriveKeysFromUser(sid, pw):
dk = mk.decrypt(k)
if dk: break
assert dk, "master key decryption failed"
enc = base64.b64decode(json.load(open(lsfile))['os_crypt']['encrypted_key'])[5:] # strip 'DPAPI'
aeskey = DPAPI_BLOB(enc).decrypt(dk)
pwb = bytes.fromhex(pw_hex); assert pwb[:3] == b'v10'
nonce, ct, tag = pwb[3:15], pwb[15:-16], pwb[-16:]
print("saved password:", AES.new(aeskey, AES.MODE_GCM, nonce=nonce).decrypt_and_verify(ct, tag).decode())
04-veracrypt-decrypt.py - VeraCrypt volume -> raw plaintext image (AES-256-XTS):
#!/usr/bin/env python3
# Decrypt a standard VeraCrypt volume (AES-256-XTS, HMAC-SHA512, 500000 iters)
# to a raw plaintext image, without VeraCrypt/FUSE. Then mount the image read-only.
# usage: python3 04-veracrypt-decrypt.py <volume> <password> <out.img>
import sys, hashlib, struct
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
vol, pw, outp = sys.argv[1], sys.argv[2].encode(), sys.argv[3]
f = open(vol, 'rb'); head = f.read(512)
salt, enc = head[:64], head[64:512]
def xts(key64, unit, data):
return Cipher(algorithms.AES(key64), modes.XTS(unit.to_bytes(16, 'little'))).decryptor().update(data)
hk = hashlib.pbkdf2_hmac('sha512', pw, salt, 500000, dklen=64) # AES-256-XTS header key
dec = xts(hk, 0, enc)
assert dec[:4] == b'VERA', "wrong password or non-default PRF/cipher"
eas = struct.unpack('>Q', dec[44:52])[0] # encrypted area start
eal = struct.unpack('>Q', dec[52:60])[0] # encrypted area length
mk = dec[192:192+64] # volume master key (XTS)
f.seek(eas); out = open(outp, 'wb'); off = eas
while off < eas + eal:
sec = f.read(512)
if len(sec) < 512: break
out.write(xts(mk, off // 512, sec)) # data unit = absolute sector index
off += 512
out.close()
print("decrypted ->", outp)
$ python3 01-ntlm.py "$C/Windows/System32/config" | grep vera
vera:1000:...:1241186a4aac4f34f4bf7ace71b396a8:::
$ python3 02-crack.py 1241186a4aac4f34f4bf7ace71b396a8 rockyou.txt
password: minivera
$ python3 03-dpapi-chrome.py "$C/Users/vera" S-1-5-21-2529683458-431225740-1723070931-1000 minivera "$PW"
saved password: Wh4t1sV3raD0inG0nTh1sH0st
$ python3 04-veracrypt-decrypt.py "$C/Users/vera/Documents/backup" Wh4t1sV3raD0inG0nTh1sH0st /tmp/vol.img
decrypted -> /tmp/vol.img
$ hdiutil attach -imagekey diskimage-class=CRawDiskImage /tmp/vol.img && pdftoppm -r 200 -png /tmp/invoice.pdf /tmp/invoice
-> invoice image: Flag: THM{1t_w4s_V3r4_A11_Al0ng?!}
| # | Stage | Mechanism |
|---|---|---|
| 1 | Triage recon | KAPE image of Vera's laptop: Chrome "For Testing" profile, registry hives, DPAPI keys, and a
100 MiB headerless container (Documents/backup). |
| 2 | Saved login | Chrome Login Data holds a v10 saved password for
VeraSecretVault - encrypted, not brute-forceable, but the browser "remembers" it. |
| 3 | Windows password | NTLM from SAM+SYSTEM (impacket) -> rockyou -> minivera. |
| 4 | DPAPI decrypt | minivera -> DPAPI master key -> Chrome AES key (Local State) -> AES-256-GCM
-> vault password Wh4t1sV3raD0inG0nTh1sH0st. |
| 5 | VeraCrypt | Decrypt the volume in Python (AES-256-XTS / HMAC-SHA512 / 500000) -> FAT32 image, mount with
hdiutil. |
| 6 | Flag | Inside, an "invoice" PDF whose image reads
THM{1t_w4s_V3r4_A11_Al0ng?!}. |