Category: Forensics / Misc
Difficulty: Easy
Flag: bronco{3ve4yth1ng_1s_aw3s0me}
Challenge
We're given lego_bricks_challenge.zip, a 63 KB archive.
$ zipinfo lego_bricks_challenge.zip
Archive: lego_bricks_challenge.zip
Zip file size: 63307 bytes, number of entries: 333
-rw---- ... lego_bricks_challenge\.DS_Store
-rw---- ... lego_bricks_challenge\.cache\sessions\.npmrc
-rw---- ... lego_bricks_challenge\.git\objects\pack\docker-compose.yml
-rw---- ... lego_bricks_challenge\src\modules\network\protocols\.solver.py
... (329 more decoy entries) ...
333 files, 4434 bytes uncompressed, 4627 bytes compressed
The listing reveals 333 files buried in a deep, decoy-heavy directory tree (.cache, .config, .git, .local, data, docs, src, tmp, etc.) — a classic haystack pattern. Almost every file is tiny (0–4 bytes) and named either generically (Dockerfile, go.sum, config.ini, package.json) or as .part_NN, with NN ranging well past 250. The file sizes and naming make it clear most of this is padding meant to bury the real content.
Recon
Sorting the extracted tree by file size immediately isolates the outlier:
$ find lego_bricks_challenge/ -type f -printf '%s %p\n' | sort -n | tail -20
...
58 lego_bricks_challenge/src/modules/network/protocols/.legacy/Dockerfile
59 lego_bricks_challenge/assets/images/sprites/.sheets/README.md
59 lego_bricks_challenge/src/modules/package.json
60 lego_bricks_challenge/src/modules/.legacy/config.ini
61 lego_bricks_challenge/data/raw/.staging/robots.txt
61 lego_bricks_challenge/src/modules/network/protocols/.legacy/requirements.txt
61 lego_bricks_challenge/tmp/system_logs/kernel/.audit/.DS_Store
62 lego_bricks_challenge/docs/.drafts/app.py
906 lego_bricks_challenge/src/modules/network/protocols/.solver.py
Every other file is under 62 bytes; .solver.py at 906 bytes stands out by more than an order of magnitude — and it's hidden (dot-prefixed) inside an otherwise-decoy path.
Reading the solver
$ cat "lego_bricks_challenge/src/modules/network/protocols/.solver.py"
#!/usr/bin/env python3
"""Hidden solver for: LEt's a GO! concatenate .part_00 .. .part_49"""
import os, re, sys
def solve(start_dir: str) -> str:
pattern = re.compile(r"^\.part_(\d+)$")
parts: dict[int, str] = {}
for dirpath, _, filenames in os.walk(start_dir):
for fn in filenames:
m = pattern.match(fn)
if m:
idx = int(m.group(1))
if 0 <= idx <= 49:
with open(os.path.join(dirpath, fn)) as fh:
parts[idx] = fh.read()
flag = "".join(parts[i] for i in sorted(parts))
return flag
...
The script's docstring spells out the trick: "Hidden solver for: LEt's a GO! concatenate .part_00 .. .part_49".
Its logic:
- Walk the entire extracted tree.
- Collect every file matching
^\.part_(\d+)$whose index is between 0 and 49 inclusive (ignoring the hundreds of.part_NNNdecoys with indices outside that range or non-matching names). - Concatenate their contents in ascending numeric order — the file's location in the directory tree is irrelevant, only the number in its name matters.
So the real flag is fragmented one character (or a few bytes) at a time across 50 files scattered throughout the tree, hidden among ~280 similarly-named but irrelevant decoy files.
Solving
The provided .solver.py itself couldn't be executed directly — it has a stray non-UTF-8 byte (a mangled emoji) in its docstring, which Python's parser chokes on:
SyntaxError: Non-UTF-8 code starting with '\x97' in file .../.solver.py on line 2
Rather than fix the encoding, we reimplemented the same logic cleanly, reading each part in binary mode to sidestep any encoding issues in the data itself:
#!/usr/bin/env python3
import os, re, sys
def solve(start_dir: str) -> bytes:
pattern = re.compile(r"^\.part_(\d+)$")
parts = {}
for dirpath, _, filenames in os.walk(start_dir):
for fn in filenames:
m = pattern.match(fn)
if m:
idx = int(m.group(1))
if 0 <= idx <= 49:
with open(os.path.join(dirpath, fn), "rb") as fh:
parts[idx] = fh.read()
missing = [i for i in range(50) if i not in parts]
if missing:
print(f"WARNING: missing indices {missing}", file=sys.stderr)
return b"".join(parts[i] for i in sorted(parts))
if __name__ == "__main__":
start = sys.argv[1] if len(sys.argv) > 1 else "lego_bricks_challenge"
flag = solve(start)
print(flag.decode("utf-8", errors="replace"))
Before running it, we verified all 50 required indices (0–49) existed somewhere in the tree with a quick scan — confirmed Missing indices: [], Found count: 50.
Running the reconstruction script:
python3 reconstruct_flag.py lego_bricks_challenge
bronco{3ve4yth1ng_1s_aw3s0me}
Flag
bronco{3ve4yth1ng_1s_aw3s0me}
Takeaways
-
Sort by size, not by name. In a haystack-style challenge, the real content is usually one clear statistical outlier once you strip away the noise (
find ... -printf '%s %p\n' | sort -n). -
Hidden/dot-prefixed files deserve a look —
.solver.pywas intentionally invisible to a plainls. - Read the source before running it. The script was self-documenting (docstring told us exactly what to do), which made writing an independent implementation trivial once the provided script hit an encoding snag.
- Don't trust file content blindly. A stray non-UTF-8 byte broke the original script's execution but not its usefulness as a spec — worth distinguishing "this script is broken" from "this script's logic is wrong."
Top comments (0)