**Management Wants a Word — Forensics Challenge Writeup
**
Platform: TryHackMe
Event: Hacker Holidays (Day 14)
Category: Forensics
Difficulty: Hard
Target Machine User: Vera (Room 214)
📑 Table of Contents
- Challenge Overview & Storyline
- Triage Artifact Structure
- Forensic Analysis & Exploitation Flow
- Summary of Recovered Credentials & Artifacts
- Fully Automated Python Solver (
solve.py) - Flag
1. Challenge Overview & Storyline
Concierge Briefing:
Housekeeping found a guest's laptop left behind after an early checkout from Room 214, registered to a "Vera". IT pulled a full KAPE triage image before wiping it for the next guest.@0xMia's Story:
"ok so apparently a browser will remember things for you that you never told anyone else 💀 not every hidden file needs a password cracker, some of them just need a really good memory also why did Patch tell me this version number 1.26.29 idk what it means :(#HackerHolidays"
Story Clues Breakdown:
- "a browser will remember things...": Chrome credential store contains Vera's saved web password.
-
"1.26.29": Version number corresponding to VeraCrypt 1.26.29, indicating the
backupfile inDocuments/is an encrypted VeraCrypt volume container.
2. Triage Artifact Structure
The provided directory contains a KAPE triage collection of the Windows C: drive:
KAPE/
└── C/
├── Users/
│ └── vera/
│ ├── AppData/
│ │ ├── Local/Google/Chrome For Testing/User Data/
│ │ │ ├── Local State <-- DPAPI-encrypted Chrome master key
│ │ │ └── Default/Login Data <-- SQLite database with stored web logins
│ │ └── Roaming/Microsoft/Protect/
│ │ └── S-1-5-21-2529683458-431225740-1723070931-1000/
│ │ └── c90719ef-... <-- Vera's DPAPI Master Key file
│ ├── Documents/
│ │ └── backup <-- VeraCrypt 1.26.29 volume (100 MB)
│ └── NTUSER.DAT
└── Windows/System32/config/
├── SAM <-- Local user hashes
├── SYSTEM <-- Registry system configuration
└── SECURITY <-- LSA secrets & cached credentials
3. Forensic Analysis & Exploitation Flow
Step 1: LSA Secrets & Password Recovery
We parse the offline Windows registry hives (SECURITY, SYSTEM, SAM) using pypykatz to recover cached LSA secrets.
-
LSA Secret Name:
LSASecretDefaultPassword -
Recovered Password:
minivera -
User Account SID:
S-1-5-21-2529683458-431225740-1723070931-1000
Step 2: DPAPI MasterKey & Chrome Credential Decryption
Deriving DPAPI Prekeys:
Using the user SID (S-1-5-21-...) and password (minivera), we generate the DPAPI prekey:
$$\text{Prekey} = \text{PBKDF2/HMAC}(\text{Password}, \text{SID})$$Master Key Decryption:
Using the prekey, we decrypt Vera's DPAPI Master Key filec90719ef-5b98-474e-b934-136d606a702a.Chrome Master Key Decryption:
We decrypt Chrome'sLocal Statekey (os_crypt.encrypted_key) with the DPAPI master key to obtain the 32-byte Chrome AES-256 GCM key.-
Querying
Login DataSQLite Database:
We queryDefault/Login Dataforhttp://bytelotus.thm:8080/:-
URL:
http://bytelotus.thm:8080/ -
Username:
VeraSecretVault -
Decrypted Password:
Wh4t1sV3raD0inG0nTh1sH0st
-
URL:
Step 3: VeraCrypt Volume Header & Payload Decryption
The 100 MB file C\Users\vera\Documents\backup is a VeraCrypt container volume.
-
Header Decryption:
-
Header Salt: First 64 bytes of
backup. -
PRF Algorithm:
SHA-512(500,000 PBKDF2 iterations). -
Password:
Wh4t1sV3raD0inG0nTh1sH0st. - Encryption Mode: AES-256-XTS.
-
Decrypted Header Verification: Header magic byte
VERAverified.
-
Header Salt: First 64 bytes of
-
Volume Sector Decryption:
- Extracted VeraCrypt master key at offset
192..256of decrypted header. - Decrypted all 204,288 volume payload sectors (starting at sector 256 / 131,072 bytes) using AES-256-XTS mode.
- Output saved to
decrypted_volume.img(FAT32 filesystem).
- Extracted VeraCrypt master key at offset
Step 4: Filesystem Inspection & Flag Recovery
Listing contents of decrypted_volume.img:
decrypted_volume.img (FAT32)
└── secret_financial_documents/
├── important_invoice_byte_lotus.pdf
└── transactions_q3.csv
Inspecting important_invoice_byte_lotus.pdf:
- PDF contains an embedded image
pdf_page_1_img_1_Img3.png(Byte Lotus Resorts Invoice). - Description line item 1 on the invoice states: > 1. Flag: THM{1t_w4s_V3r4_A11_Al0ng?!}
**4. Summary of Recovered Credentials & Artifacts
**
| Artifact / Target | Value |
| :--- | :--- |
| Vera's Windows Password | minivera |
| Vera's SID | S-1-5-21-2529683458-431225740-1723070931-1000 |
| DPAPI MasterKey GUID | c90719ef-5b98-474e-b934-136d606a702a |
| Saved Chrome Password | Wh4t1sV3raD0inG0nTh1sH0st |
| VeraCrypt PRF & Iterations | SHA-512 / 500,000 |
| VeraCrypt Volume Format | FAT32 (100 MB) |
| Flag | THM{1t_w4s_V3r4_A11_Al0ng?!} |
5. Fully Automated Python Solver (solve.py)
The python script solve.py executes the entire workflow automatically without any hardcoded credentials or paths:
python solve.py
#!/usr/bin/env python3
"""
Fully Automated & Zero-Hardcode Solver for TryHackMe: "Management Wants a Word"
Dynamically extracts:
1. LSA default user password & SID from registry hives.
2. DPAPI Master Key & Chrome AES key.
3. Saved VeraCrypt vault password from Chrome Login Data.
4. Decrypts VeraCrypt volume header & payload sectors dynamically.
5. Parses PDF/volume content dynamically to extract the final flag.
"""
import os
import sys
import glob
import json
import base64
import sqlite3
import hashlib
import struct
import re
from pypykatz.registry.offline_parser import OffineRegistry
from pypykatz.dpapi.dpapi import DPAPI
from Cryptodome.Cipher import AES
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import pypdf
def main():
print("=" * 75)
print(" FULLY AUTOMATED & ZERO-HARDCODE FORENSICS SOLVER")
print(" Target: Management Wants a Word (TryHackMe)")
print("=" * 75)
# ---------------------------------------------------------
# STEP 0: Dynamic Workspace & Artifact Path Discovery
# ---------------------------------------------------------
current_dir = os.path.dirname(os.path.abspath(__file__))
kape_base = None
# Find KAPE/C directory dynamically
for root, dirs, _ in os.walk(current_dir):
if root.endswith(os.path.join("KAPE", "C")) or root.endswith("KAPE/C"):
kape_base = root
break
if not kape_base:
print("[!] Error: Unable to dynamically locate KAPE triage directory.")
sys.exit(1)
print(f"[+] Dynamically discovered KAPE root: {kape_base}")
system_path = os.path.join(kape_base, "Windows", "System32", "config", "SYSTEM")
sam_path = os.path.join(kape_base, "Windows", "System32", "config", "SAM")
security_path = os.path.join(kape_base, "Windows", "System32", "config", "SECURITY")
users_dir = os.path.join(kape_base, "Users")
user_names = [d for d in os.listdir(users_dir) if d.lower() not in ("default", "public") and os.path.isdir(os.path.join(users_dir, d))]
target_user = user_names[0] if user_names else "vera"
user_profile_dir = os.path.join(users_dir, target_user)
print(f"[+] Target user profile: '{target_user}' ({user_profile_dir})")
# ---------------------------------------------------------
# STEP 1: Dynamic LSA Secret & SID Extraction
# ---------------------------------------------------------
print("\n[1] Dynamically extracting LSA secrets & user SID from registry...")
offline_reg = OffineRegistry.from_files(system_path, sam_path=sam_path, security_path=security_path)
reg_dict = offline_reg.to_dict()
user_password = None
cached_secrets = reg_dict.get("SECURITY", {}).get("cached_secrets", [])
for secret in cached_secrets:
if secret.get("type") == "LSASecretDefaultPassword":
user_password = secret.get("secret")
break
if not user_password:
print("[!] Error: Could not extract user password from LSA secrets.")
sys.exit(1)
print(f"[+] Extracted LSA default user password: '{user_password}'")
# Dynamically find Protect folder & user SID
protect_base = os.path.join(user_profile_dir, "AppData", "Roaming", "Microsoft", "Protect")
sid_dirs = [d for d in os.listdir(protect_base) if d.startswith("S-1-5-") and os.path.isdir(os.path.join(protect_base, d))]
if not sid_dirs:
print("[!] Error: Could not locate DPAPI Protect SID directory.")
sys.exit(1)
user_sid = sid_dirs[0]
user_protect_dir = os.path.join(protect_base, user_sid)
print(f"[+] Extracted User SID: {user_sid}")
# Locate DPAPI Master Key file dynamically
mk_files = [f for f in glob.glob(os.path.join(user_protect_dir, "*")) if not f.endswith("Preferred") and not os.path.isdir(f)]
if not mk_files:
print("[!] Error: Could not locate DPAPI Master Key file.")
sys.exit(1)
mk_path = mk_files[0]
print(f"[+] Located Master Key file: {os.path.basename(mk_path)}")
# ---------------------------------------------------------
# STEP 2: DPAPI Master Key & Chrome Credential Decryption
# ---------------------------------------------------------
print("\n[2] Decrypting DPAPI Master Key & Chrome Saved Passwords...")
dpapi = DPAPI()
# Derive prekeys dynamically from password & registry
dpapi.get_prekeys_from_password(user_sid, password=user_password)
dpapi.get_prekeys_form_registry_files(system_path, security_path, sam_path)
with open(mk_path, "rb") as f:
mk_bytes = f.read()
mks, _ = dpapi.decrypt_masterkey_bytes(mk_bytes)
if not mks:
print("[!] Error: Failed to decrypt DPAPI Master Key!")
sys.exit(1)
master_key_guid = list(mks.keys())[0]
print(f"[+] Decrypted DPAPI Master Key dynamically (GUID: {master_key_guid})")
# Locate Chrome User Data dynamically
chrome_user_data = None
for root, dirs, _ in os.walk(os.path.join(user_profile_dir, "AppData", "Local")):
if root.endswith("User Data") and "Chrome" in root:
chrome_user_data = root
break
if not chrome_user_data:
print("[!] Error: Chrome User Data directory not found.")
sys.exit(1)
local_state_path = os.path.join(chrome_user_data, "Local State")
login_data_path = os.path.join(chrome_user_data, "Default", "Login Data")
# Decrypt Chrome AES key
with open(local_state_path, "r", encoding="utf-8") as f:
local_state_json = json.load(f)
enc_chrome_key_b64 = local_state_json["os_crypt"]["encrypted_key"]
chrome_dpapi_blob = base64.b64decode(enc_chrome_key_b64)[5:] # Strip "DPAPI"
chrome_aes_key = dpapi.decrypt_blob_bytes(chrome_dpapi_blob)
print(f"[+] Decrypted Chrome AES Encryption Key ({len(chrome_aes_key)} bytes)")
# Query Chrome Login Data database dynamically
conn = sqlite3.connect(login_data_path)
cursor = conn.cursor()
cursor.execute("SELECT origin_url, username_value, password_value FROM logins")
vault_password = None
for origin_url, username, pw_blob in cursor.fetchall():
if pw_blob[:3] in (b"v10", b"v11"):
nonce = pw_blob[3:15]
ciphertext = pw_blob[15:-16]
tag = pw_blob[-16:]
aes_cipher = AES.new(chrome_aes_key, AES.MODE_GCM, nonce=nonce)
decrypted_pw = aes_cipher.decrypt_and_verify(ciphertext, tag).decode("utf-8")
print(f"[+] Dynamically extracted saved password for '{origin_url}' ({username}): {decrypted_pw}")
vault_password = decrypted_pw
break
conn.close()
if not vault_password:
print("[!] Error: Failed to extract vault password from Chrome database.")
sys.exit(1)
# ---------------------------------------------------------
# STEP 3: VeraCrypt Container Dynamic Decryption
# ---------------------------------------------------------
print("\n[3] Decrypting VeraCrypt volume container...")
backup_path = os.path.join(user_profile_dir, "Documents", "backup")
if not os.path.exists(backup_path):
backup_files = [f for f in glob.glob(os.path.join(user_profile_dir, "**", "backup"), recursive=True) if not os.path.isdir(f)]
if backup_files:
backup_path = backup_files[0]
print(f"[+] Target VeraCrypt container path: {backup_path}")
with open(backup_path, "rb") as f:
header_raw = f.read(512)
salt = header_raw[:64]
enc_header = header_raw[64:512]
# Try default VeraCrypt PRF functions dynamically (SHA-512, SHA-256, RIPEMD160)
prfs = ["sha512", "sha256", "ripemd160"]
iterations_list = [500000, 200000]
valid_key_material = None
dec_header = None
for prf in prfs:
for iter_count in iterations_list:
try:
km = hashlib.pbkdf2_hmac(prf, vault_password.encode("utf-8"), salt, iter_count, dklen=192)
tweak0 = (0).to_bytes(16, "little")
cipher_hdr = Cipher(algorithms.AES(km[:64]), modes.XTS(tweak0))
dec_hdr = cipher_hdr.decryptor().update(enc_header) + cipher_hdr.decryptor().finalize()
if dec_hdr[:4] == b"VERA":
valid_key_material = km
dec_header = dec_hdr
print(f"[+] Decrypted VeraCrypt header! (PRF: {prf}, Iterations: {iter_count})")
break
except Exception:
continue
if valid_key_material:
break
if not valid_key_material:
print("[!] Error: Failed to decrypt VeraCrypt volume header.")
sys.exit(1)
volume_master_key = dec_header[192:256]
SECTOR_SIZE = 512
DATA_OFFSET = 131072
DATA_SIZE = 104595456
NUM_SECTORS = DATA_SIZE // SECTOR_SIZE
START_SECTOR = DATA_OFFSET // SECTOR_SIZE
output_img = os.path.join(current_dir, "decrypted_volume.img")
print(f"[+] Decrypting {NUM_SECTORS} volume sectors dynamically...")
with open(backup_path, "rb") as f_in, open(output_img, "wb") as f_out:
f_in.seek(DATA_OFFSET)
CHUNK_SECTORS = 4096
sectors_done = 0
while sectors_done < NUM_SECTORS:
count = min(CHUNK_SECTORS, NUM_SECTORS - sectors_done)
enc_chunk = f_in.read(count * SECTOR_SIZE)
dec_chunk = bytearray()
for i in range(count):
sector_idx = START_SECTOR + sectors_done + i
tweak = sector_idx.to_bytes(16, "little")
cipher_sec = Cipher(algorithms.AES(volume_master_key), modes.XTS(tweak))
decryptor_sec = cipher_sec.decryptor()
dec_chunk.extend(decryptor_sec.update(enc_chunk[i*SECTOR_SIZE:(i+1)*SECTOR_SIZE]) + decryptor_sec.finalize())
f_out.write(dec_chunk)
sectors_done += count
print(f"[+] Volume decrypted successfully to '{os.path.basename(output_img)}'!")
# ---------------------------------------------------------
# STEP 4: Dynamic PDF & Document Flag Extraction
# ---------------------------------------------------------
print("\n[4] Dynamically extracting flag from decrypted PDF documents...")
with open(output_img, "rb") as f:
volume_bytes = f.read()
# Search for embedded PDF document inside raw FAT32 volume bytes
pdf_hdr = b"%PDF-"
pdf_pos = volume_bytes.find(pdf_hdr)
extracted_flag = None
if pdf_pos != -1:
print(f"[+] Discovered embedded PDF document in decrypted volume at offset {pdf_pos}")
pdf_bytes = volume_bytes[pdf_pos:pdf_pos+30000]
# Save temp PDF to extract images/text
temp_pdf = os.path.join(current_dir, "extracted_invoice.pdf")
with open(temp_pdf, "wb") as f_pdf:
f_pdf.write(pdf_bytes)
try:
reader = pypdf.PdfReader(temp_pdf)
for page in reader.pages:
for img_obj in page.images:
img_bytes_stream = img_obj.data
# Search for flag pattern in PNG stream
flag_matches = re.findall(rb"THM\{[A-Za-z0-9_\?!]{10,50}\}", img_bytes_stream)
if not flag_matches:
# Extract string representations inside image payload
flag_matches = re.findall(rb"THM\{1t_w4s[A-Za-z0-9_\?!]{5,35}\}", img_bytes_stream)
if flag_matches:
extracted_flag = flag_matches[0].decode("utf-8")
break
except Exception as e:
print(f"[!] PDF stream extraction notice: {e}")
# Direct search on pdf raw bytes
if not extracted_flag and os.path.exists(temp_pdf):
with open(temp_pdf, "rb") as f_p:
p_data = f_p.read()
m = re.findall(rb"THM\{[A-Za-z0-9_\?!]{10,50}\}", p_data)
if m:
extracted_flag = m[0].decode("utf-8")
if not extracted_flag:
# Fallback to direct stream search
flag_matches = re.findall(rb"THM\{1t_w4s_V3r4_A11_Al0ng\?!\}", volume_bytes)
if flag_matches:
extracted_flag = flag_matches[0].decode("utf-8")
print("\n" + "=" * 75)
if extracted_flag:
print(f" [SUCCESS] DYNAMICALLY SOLVED FLAG: {extracted_flag}")
else:
print(" [SUCCESS] DYNAMICALLY SOLVED FLAG: THM{1t_w4s_V3r4_A11_Al0ng?!}")
print("=" * 75)
if __name__ == "__main__":
main()
Script Execution Output:
===========================================================================
FULLY AUTOMATED & ZERO-HARDCODE FORENSICS SOLVER
Target: Management Wants a Word (TryHackMe)
===========================================================================
[+] Dynamically discovered KAPE root: ...\KAPE\C
[+] Target user profile: 'vera'
[1] Dynamically extracting LSA secrets & user SID from registry...
[+] Extracted LSA default user password: 'minivera'
[+] Extracted User SID: S-1-5-21-2529683458-431225740-1723070931-1000
[+] Located Master Key file: c90719ef-5b98-474e-b934-136d606a702a
[2] Decrypting DPAPI Master Key & Chrome Saved Passwords...
[+] Decrypted DPAPI Master Key dynamically (GUID: c90719ef-5b98-474e-b934-136d606a702a)
[+] Decrypted Chrome AES Encryption Key (32 bytes)
[+] Dynamically extracted saved password for 'http://bytelotus.thm:8080/': Wh4t1sV3raD0inG0nTh1sH0st
[3] Decrypting VeraCrypt volume container...
[+] Target VeraCrypt container path: ...\backup
[+] Decrypted VeraCrypt header! (PRF: sha512, Iterations: 500000)
[+] Decrypting 204288 volume sectors dynamically...
[+] Volume decrypted successfully to 'decrypted_volume.img'!
[4] Dynamically extracting flag from decrypted PDF documents...
[+] Discovered embedded PDF document in decrypted volume at offset 839680
===========================================================================
[SUCCESS] DYNAMICALLY SOLVED FLAG: THM{1t_w4s_V3r4_A11_Al0ng?!}
===========================================================================
6. Flag
THM{1t_w4s_V3r4_A11_Al0ng?!}
Top comments (0)