Speakeasy Storage Audit CTF: Finding a Key Hidden in an NTFS Alternate Data Stream
Speakeasy Storage Audit: Challenge Brief
A TruffleTrotter courier tablet was recovered from a damp basement after a delivery gone cold. Forensic engineers extracted a metadata dump of the device's "DashBox" storage volume. The goal: recover a Gourmet Access Key, a sensitive token staged on the device before its failure.
The challenge exposed five API endpoints:
| Method | Path | Description |
|---|---|---|
| GET | / | Case briefing |
| GET | /evidence | Evidence artifact listing |
| GET | /evidence/<path:filename> | Download evidence file |
| POST | /submit | Submit flag |
| GET | /submit | Submit form |
Speakeasy Storage Audit Reconnaissance
The challenge web frontend showed a JSON preview of filesystem metadata, but the actual evidence lived on the API. First step was to enumerate what was downloadable.
curl -s http://target:5000/evidence
<h1>Recovered Artifacts</h1>
<ul>
<li><a href='/evidence/disk_metadata.json'>disk_metadata.json</a></li>
</ul>
One file. Download and pretty-print it:
curl -s http://target:5000/evidence/disk_metadata.json | jq .
The Recovered Evidence Dump
The metadata dump described five inodes on the recovered storage volume:
[
{ "inode": 501, "filename": "boot_config.sys", "size": 2048, "status": "corrupted" },
{ "inode": 502, "filename": "courier_manifest.db", "size": 40960, "status": "active" },
{
"inode": 503,
"filename": "delivery_log.txt",
"size": 124,
"status": "active",
"streams": [
{ "name": "zone_id", "content": "QjEtMTI=" },
{ "name": "gourmet_key", "content": "TEVWRUxVUHtGWFpMRWxaWW5FcEZqOWZtU2Jld05Wd25Ea0JKVlJBN30=" }
]
},
{ "inode": 504, "filename": "temp_sensor.log", "size": 8192, "status": "active" },
{ "inode": 505, "filename": "deleted_note.tmp", "size": 512, "status": "deleted", "unallocated_cluster": 4201 }
]
Five inodes. Four were the usual suspects (boot config, manifest database, sensor log, deleted note in unallocated space). The fifth one, delivery_log.txt, had something the others didn't: a streams array.
Speakeasy Storage Audit: Alternate Data Streams Spotted
delivery_log.txt (inode 503) has two named streams attached, a classic Windows NTFS feature that allows arbitrary data blobs to be hidden alongside a file's primary content. Normal directory listings (dir, ls) do not show them. They are invisible unless you know to look.
zone_id— this is a legitimate Windows artifact. Zone.Identifier streams mark files downloaded from the internet (Zone 1 = Local intranet, Zone 2 = Trusted, Zone 3 = Internet). Decoded value:B1-12. Metadata noise for this challenge.gourmet_key— the name gives it away. This is the staged token the scenario described.
Decoding the Gourmet Key
Both stream contents are base64-encoded. Decode the gourmet_key:
echo "TEVWRUxVUHtGWFpMRWxaWW5FcEZqOWZtU2Jld05Wd25Ea0JKVlJBN30=" | base64 -d
LEVELUP{REDACTED}
Speakeasy Storage Audit CTF Flag
LEVELUP{REDACTED}
Speakeasy Storage Audit Attack Chain
Hit /evidence to enumerate downloadable artifacts (one file: disk_metadata.json)
|
v
Download and parse the JSON metadata dump
|
v
Spot the unusual "streams" array on delivery_log.txt (inode 503)
|
v
Recognise NTFS Alternate Data Streams: named blobs hidden alongside the primary file content
|
v
Base64-decode the gourmet_key stream
|
v
Flag
NTFS ADS in Real-World Forensics
Alternate Data Streams (ADS) are an NTFS feature allowing multiple data streams to be associated with a single file. The syntax is filename:streamname. They are a well-known forensic and malware persistence vector. Attackers use them to hide payloads, configuration, or exfiltrated data inside innocuous-looking files because standard directory listings will not surface them.
On a live Windows system you can enumerate ADS with:
# PowerShell
Get-Item -Path .\delivery_log.txt -Stream *
# Or via Streams from Sysinternals
streams.exe delivery_log.txt
On a disk image, tools like Sleuth Kit's icat or Autopsy will surface them during filesystem analysis. Volatility plugins exist for ADS hunting in memory dumps too.
Zone.Identifier is a real-world ADS that Windows automatically creates when files are downloaded from the internet. The "do you want to run this file?" Mark-of-the-Web prompt reads from it. Seeing Zone.Identifier in forensic evidence is a reliable indicator that a file was fetched externally and not created locally, which makes it useful for timeline reconstruction during incident response.
Speakeasy Storage Audit Lessons Learned
- Always read the raw API, not the rendered UI. The challenge web frontend displayed a JSON preview that matched the API data, but
/evidencereturned HTML. Following the link in the HTML to the actual JSON endpoint was the move. Rendered previews can drop, reformat, or escape data. - Treat unfamiliar JSON keys as the signal. Four of the five inodes were standard; only one had a
streamsfield. When a single record has a structural difference from its peers, that difference is almost always where the puzzle lives. - NTFS ADS are textbook hiding spots. If a forensics challenge mentions Windows or NTFS and any file's metadata includes a
streamsor:streamnamenotation, ADS is the answer 90% of the time. The other 10% is usually steganography in a sparse file or slack space. - Base64 first, decode types second. Both streams were base64. When the content of an unknown field is roughly multiples of 4 characters, ends in
=padding, and uses the standard alphabet, decode it before assuming anything about the contents.
Speakeasy Storage Audit: Final Thoughts
This was a clean introductory forensics challenge. The signal lived in a single structural anomaly across five otherwise uniform records, and the rest of the work was recognising the technique and running it through base64 -d. Beginner challenges that teach a specific real-world artifact (NTFS ADS, in this case) tend to age well, because the muscle memory of "list the streams" carries forward to actual incident response on Windows systems.
The platform itself had a couple of UX quirks worth noting. The CTF web frontend showed a JSON preview, but the API returned HTML at /evidence, not JSON. Always hit the raw API rather than the rendered UI. There was also some timing oddness around when the submission window opens, but submitting via the POST form endpoint worked reliably.
Challenge hosted on LevelUpCTF. Writeup completed: May 2026.