Writeup on THM Holiday Hack 2026:
Day 14 - Forensics / Management Wants a Word

Author: Hubert Feyrer / hubertf, 2026-08-09


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."

Challenge description

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.

1. Download

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

2. Docker/nc - what we get

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.

3. Analysis steps

3.1 The browser remembered a vault password (success)

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.

3.2 Recover Vera's Windows password (SAM -> NTLM -> rockyou) (success)

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.

3.3 DPAPI chain -> decrypt the saved password (success)

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.

3.4 Decrypt the VeraCrypt volume (no VeraCrypt/FUSE) (success)

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 volume mounted as 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.)

3.5 The hidden documents -> the flag in an image (success)

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
Decrypted volume contents and the rendered invoice with the flag

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.

4. Solution

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)

5. Run it

$ 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?!}

6. Summary of how the exploit works

#StageMechanism
1Triage recon KAPE image of Vera's laptop: Chrome "For Testing" profile, registry hives, DPAPI keys, and a 100 MiB headerless container (Documents/backup).
2Saved login Chrome Login Data holds a v10 saved password for VeraSecretVault - encrypted, not brute-forceable, but the browser "remembers" it.
3Windows password NTLM from SAM+SYSTEM (impacket) -> rockyou -> minivera.
4DPAPI decrypt minivera -> DPAPI master key -> Chrome AES key (Local State) -> AES-256-GCM -> vault password Wh4t1sV3raD0inG0nTh1sH0st.
5VeraCrypt Decrypt the volume in Python (AES-256-XTS / HMAC-SHA512 / 500000) -> FAT32 image, mount with hdiutil.
6Flag Inside, an "invoice" PDF whose image reads THM{1t_w4s_V3r4_A11_Al0ng?!}.