← Back to Writeups
CUSTOMMediumForensics

Silk Road

XESXOR9/2/20266 min read
#forensics#custom#medium#vmdk_extraction_with_7z#ext4_analysis_without_mounting#bash_history_forensics#png_text_chunk_metadata_extrac#firefox_password_decryption_fi#multi_fragment_flag_assembly#trash_artifact_analysis#binary_file_comparison#linux

Silk Road

Platform: hackerlab | Category: Forensics | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-05 | Status: Solved Techniques: vmdk_extraction_with_7z, ext4_analysis_without_mounting, bash_history_forensics, png_text_chunk_metadata_extraction, firefox_password_decryption_firepwd, multi_fragment_flag_assembly, trash_artifact_analysis, binary_file_comparison

Summary

Task: a 4 GB VirtualBox VM disk image (Debian, user 'ross') themed around Silk Road with 3 hidden flag fragments. Solution: extract ext4 from VMDK with 7z, recover fragment 1 from bash_history (failed deletion), fragment 2 from PNG tEXt Author metadata in Trash, fragment 3 from Firefox backup logins decrypted with firepwd.py.

Silk Road — HackerLab

Description

Шёлковый путь. Не повторяйте чужих ошибок.

English summary: a 3.9 GB ZIP archive containing a VirtualBox VM (Debian 64-bit). The VM belongs to user "ross" — a reference to Ross Ulbricht, creator of the Silk Road darknet marketplace. The flag is split into 3 fragments hidden across different forensic artifacts. The subtitle "Don't repeat other people's mistakes" hints at Ulbricht's OPSEC failures that led to his arrest.

Analysis

Initial reconnaissance

The archive task.zip contains two files:

  • vm.ovf — VirtualBox VM descriptor (Debian_64)
  • vm-disk001.vmdk — virtual disk (~4 GB) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Using 7z to inspect the VMDK without mounting reveals 3 partitions:

  • 0.img (18 GB) — ext4 main filesystem
  • 1.img (1 GB) — swap
  • 2 (1 MB) — boot
bash
7z l vm-disk001.vmdk
# Shows 3 partitions inside the VMDK

7z x vm-disk001.vmdk "0.img" -y
file 0.img  # Linux rev 1.0 ext4 filesystem

Filesystem exploration

Listing the ext4 contents with 7z l 0.img reveals a single user ross with a rich set of forensic artifacts:

PathSignificance
home/ross/.bash_historyCommand history with flag fragment
home/ross/Desktop/.desktop_config2 MB VeraCrypt encrypted volume
home/ross/Desktop/org.keepassxc.KeePassXC.desktopKeePassXC shortcut
home/ross/Desktop/veracrypt.desktopVeraCrypt shortcut
home/ross/.\./.config.kdbxKeePassXC database in hidden dot-backslash directory
home/ross/.local/share/Trash/files/config.kdbxKeePassXC database in Trash
home/ross/.local/share/Trash/files/avatar_forum.pngModified avatar in Trash
home/ross/.local/share/Trash/files/avatar_forum.png_originalOriginal avatar in Trash
home/ross/.mozilla/firefox/u0kkkne8.default-esr/logins.jsonFirefox saved passwords
home/ross/.mozilla/firefox/u0kkkne8.default-esr/logins-backup.jsonFirefox logins backup
home/ross/.mozilla/firefox/u0kkkne8.default-esr/key4.dbFirefox NSS key database
home/ross/.config/VeraCrypt/Configuration.xmlVeraCrypt config
home/ross/.config/keepassxc/keepassxc.iniKeePassXC config
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The presence of KeePassXC, VeraCrypt, Firefox with saved passwords, and deleted files in Trash all point to multiple potential hiding spots for flag fragments.

Solution

Fragment 1: bash_history (failed deletion)

bash
7z x 0.img "home/ross/.bash_history" -oextracted -y
cat extracted/home/ross/.bash_history

Contents:

CODEBY{w4s_
ls
cat ~/.bash_history 
nano .bash_history 
ls -ahli
unlink .bash_history 
ls -ahli
nano .bash_history
ls
cat .bash_history
shutdown

Ross accidentally typed the flag start CODEBY{w4s_ as a shell command. He then noticed it in his history and tried to delete it with unlink .bash_history, but subsequent commands created a new history file that still contained the original entry. This mirrors the real Ross Ulbricht's OPSEC failures — he was caught partly because of digital traces he failed to properly clean up. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Fragment 1: CODEBY{w4s_

Fragment 2: PNG metadata steganography

Two avatar images were found in Trash with slightly different file sizes:

  • avatar_forum.png_original — 55,487 bytes
  • avatar_forum.png — 55,516 bytes (29 bytes larger)
bash
7z x 0.img "home/ross/.local/share/Trash/files/avatar_forum.png" -oextracted -y
7z x 0.img "home/ross/.local/share/Trash/files/avatar_forum.png_original" -oextracted -y

cmp -l avatar_forum.png_original avatar_forum.png

Binary comparison showed the IDAT (pixel data) chunks are byte-for-byte identical. The only difference: the modified file has an extra tEXt PNG chunk containing an Author field. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
from PIL import Image
img = Image.open("avatar_forum.png")
print(img.text)  # {'Author': 'th3_s1lly_'}

The tEXt chunk is a standard PNG metadata chunk — not visible when viewing the image, but trivially readable with any PNG metadata tool (exiftool, PIL, pngcheck).

Fragment 2: th3_s1lly_

Fragment 3: Firefox saved passwords

Extracted the Firefox profile artifacts:

bash
7z x 0.img "home/ross/.mozilla/firefox/u0kkkne8.default-esr/key4.db" -oextracted -y
7z x 0.img "home/ross/.mozilla/firefox/u0kkkne8.default-esr/logins.json" -oextracted -y
7z x 0.img "home/ross/.mozilla/firefox/u0kkkne8.default-esr/logins-backup.json" -oextracted -y

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The logins.json and logins-backup.json contain encrypted credentials for http://sinkroad.cdb (a play on "Silk Road" + CODEBY). The username is DreadPirateRoberts — Ulbricht's famous alias.

Key technical details:

  • key4.db uses PBES2 (PBKDF2 + HMAC-SHA256 + AES256-CBC) to protect the master key
  • No master password was set (empty string)
  • Login credentials are encrypted with 3DES-CBC using the first 24 bytes of the 32-byte master key

Used firepwd.py (pure Python Firefox password decryptor) since native firefox_decrypt failed due to NSS library issues on macOS: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bash
python firepwd.py -d extracted/home/ross/.mozilla/firefox/u0kkkne8.default-esr/

Custom solve script based on firepwd approach:

python
#!/usr/bin/env python3
"""Decrypt Firefox saved passwords from key4.db and logins.json"""
import json, sqlite3
from base64 import b64decode
from hashlib import pbkdf2_hmac, sha1
from pathlib import Path
from Crypto.Cipher import AES, DES3
from Crypto.Util.Padding import unpad
from pyasn1.codec.der import decoder

PROFILE = Path("extracted/home/ross/.mozilla/firefox/u0kkkne8.default-esr")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def decryptPBE(decodedItem, masterPassword, globalSalt):
    entrySalt = decodedItem[0][0][1][0][1][0].asOctets()
    iterations = int(decodedItem[0][0][1][0][1][1])
    keyLength = int(decodedItem[0][0][1][0][1][2])
    k = sha1(globalSalt + masterPassword).digest()
    key = pbkdf2_hmac("sha256", k, entrySalt, iterations, dklen=keyLength)
    iv = b"\x04\x0e" + decodedItem[0][0][1][1][1].asOctets()
    cipherT = decodedItem[0][1].asOctets()
    return AES.new(key, AES.MODE_CBC, iv).decrypt(cipherT)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def getKey(masterPassword=b""):
    conn = sqlite3.connect(str(PROFILE / "key4.db"))
    c = conn.cursor()
    c.execute("SELECT item1, item2 FROM metadata WHERE id='password'")
    globalSalt, item2 = c.fetchone()
    # Verify empty master password
    decoded = decoder.decode(item2)
    check = decryptPBE(decoded, masterPassword, globalSalt)
    assert b"password-check" in check, "Wrong master password!"
    # Extract master key
    c.execute("SELECT a11 FROM nssPrivate")
    a11 = c.fetchone()[0]
    conn.close()
    return decryptPBE(decoder.decode(a11), masterPassword, globalSalt)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def decrypt_logins(key, path):
    final_key = key[:24]  # 3DES uses first 24 of 32 bytes
    with open(path) as f:
        data = json.load(f)
    for login in data["logins"]:
        for field in ["encryptedUsername", "encryptedPassword"]:
            seq = decoder.decode(b64decode(login[field]))[0]
            iv, ct = bytes(seq[1][1]), bytes(seq[2])
            plain = unpad(DES3.new(final_key, DES3.MODE_CBC, iv).decrypt(ct), 8)
            label = "Username" if "User" in field else "Password"
            print(f"  {label}: {plain.decode()}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

key = getKey()
for f in ["logins.json", "logins-backup.json"]:
    print(f"\n=== {f} ===")
    print(f"  Hostname: http://sinkroad.cdb")
    decrypt_logins(key, PROFILE / f)

Results:

FileUsernamePassword
logins.json (current)DreadPirateRobertsfa1r?} (truncated)
logins-backup.json (backup)DreadPirateRobertsREDACTED} (complete)

The current logins.json has a truncated password — the password was changed at some point, but the backup retains the full version. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Fragment 3: REDACTED}

Flag assembly

Combining all 3 fragments in order:

#SourceFragment
1bash_historyCODEBY{w4s_
2PNG tEXt Author metadatath3_s1lly_
3Firefox backup loginsREDACTED}

Red herrings and alternative paths

  • KeePassXC databases (both in hidden .\./ directory and Trash) — had 76M+ iterations for key derivation, not quickly crackable. Likely a red herring or an alternative harder path.
  • VeraCrypt volume (.desktop_config, 2 MB on Desktop) — encrypted container, password possibly stored in KeePassXC. Not needed for the flag.
  • VeraCrypt Configuration.xml — showed SaveHistory=0, UseKeyfiles=0, no history saved. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍