Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
BlockchainEasy

Money Trail

CTF writeup for Money Trail from niteCTF

//Money Trail

>TL;DR

  • Start from the 100k FLOW deposit into 0xfb1e691adac8361ba054626733bc4b701568da8a that predates the CTF launch and recursively follow every outbound hop on Flow's EVM testnet.
  • Build a breadth-first crawler around Flowscan's account.txlist endpoint to enumerate every descendant externally owned account (EOA) and collect transactions whose calldata is non-empty.
  • The traversal uncovers exactly one contract-style transaction: 0x632fe7300bbcace53a84282ca415708cabe502d0dd9307ea7886742f5ba1c1df, where the input blob hex-decodes to the hidden flag nite{6o0d_J0b_n0w_Tr4c3_mY_m0n3r0}.

>1. Challenge Brief

  • Network: Flow EVM Testnet (testnet.evm.nodes.onflow.org RPC / evm-testnet.flowscan.io explorer API)
  • Premise: Someone laundered funds through a daisy chain of EOAs before the CTF went live; our job is to trace the path and surface any "sus messages" embedded in contract calls along the way.
  • Constraints: Stay within the pre-CTF block range, prefer open data sources (Flowscan REST API + public JSON-RPC), and produce a reproducible audit trail suitable for contest judging.

>2. Manual Recon and Baseline Data

  1. Seed transaction: Pull the saver transaction for the Flow-provided hash 0x9ba8… via Blockscout and confirm that 0xfb1e…da8a receives exactly 100,000 FLOW.

  2. Handshake check: Query that address through Flowscan's REST API to dump its chronological history: This shows early fan-out transactions toward EOAs such as 0x538e…, 0x6ec5…, 0x7e79…, 0x60ca…, and 0xec46….

    bash
    curl "<https://evm-testnet.flowscan.io/api?module=account&action=txlist&address=0xfb1e691adac8361ba054626733bc4b701568da8a&sort=asc>"
    
  3. Sanity filter: Because each hop is a plain value transfer, we log every recipient, track timestamps (all < 2024-12-01T00:00:00Z to satisfy the "before CTF" rule), and note that no calldata is attached yet. At this point, doing the remainder manually would be tedious, so automation is warranted.


>3. Automating the Crawl

To ensure every downstream hop is inspected, I wrote a BFS crawler in trace_flow.py:

  • Queue-driven traversal: Start from the seed EOA, pop addresses one at a time, and push any to address observed in outbound transactions.
  • Dedup + halt: Maintain a visited set to avoid loops; stop when the queue empties, meaning all reachable EOAs (from that provenance chain) have been explored.
  • Signal extraction: For every transaction whose from equals the current node, copy it into a contract_calls list whenever the input field is anything other than 0x.
  • Resilience: Wrap the Flowscan API call in a 5-attempt exponential back-off so intermittent 429/5xx responses do not derail the crawl, and print compact JSON stats every 25 nodes to monitor progress.

Key portions of the script (abridged for readability):

python
API = "<https://evm-testnet.flowscan.io/api>"
START_ADDRESS = "0xfb1e691adac8361ba054626733bc4b701568da8a"

queue = deque([START_ADDRESS])
visited: Set[str] = set()
contract_calls: List[Dict[str, str]] = []

while queue:
    addr = queue.popleft().lower()
    if addr in visited:
        continue
    visited.add(addr)

    txs = fetch_transactions(addr)
    if txs is None:
        continue

    for tx in txs:
        if tx.get("from", "").lower() != addr:
            continue
        if tx.get("input", "0x") not in ("0x", "0X"):
            contract_calls.append(tx)
        if tx.get("to"):
            queue.append(tx["to"].lower())

Execution

Run the crawler inside the project virtualenv:

bash
cd /home/noigel/Desktop/next_hunt/ByteDoubleCross
. .venv/bin/activate
python trace_flow.py

Progress snapshots (emitted every 25 processed addresses) show both queue depth and discovery of contract calls:

{"processed": 200, "queue_size": 231, "visited": 200, "contract_calls_found": 0} ... {"processed": 275, "queue_size": 128, "visited": 275, "contract_calls_found": 1}

After 350 nodes the crawl completes with 367 unique EOAs visited and exactly one transaction containing calldata.


>4. Surfacing the Suspicious Transaction

The crawler's final JSON payload highlights the lone contract-style interaction:

json
{
  "blockNumber": "83335968",
  "from": "0x00758cf756f8834da912d9f5cc9cf801d013515f",
  "to": "0x00758cf756f8834da912d9f5cc9cf801d013515f",
  "hash": "0x632fe7300bbcace53a84282ca415708cabe502d0dd9307ea7886742f5ba1c1df",
  "input": "0x6e6974657b366f30645f4a30625f6e30775f54723463335f6d595f6d306e3372307d",
  "value": "0",
  "gasUsed": "22360",
  "timeStamp": "1765280704"
}

Noteworthy observations:

  • It is a self-call (from == to), so no new EOA enters the crawl, which explains why only one contract call surfaced.
  • Gas usage (22360) indicates the transaction executed logic rather than a bare transfer, reinforcing that the payload might hide a message.
  • The timestamp 1765280704 converts to 2025-11-11 05:25:04 UTC, still before the challenge launch window, satisfying the "historic trail" requirement.

Cross-verify via RPC if desired:

bash
curl -H "Content-Type: application/json" \\
  -d '{"jsonrpc":"2.0","method":"eth_getTransactionByHash","params":["0x632fe7300bbcace53a84282ca415708cabe502d0dd9307ea7886742f5ba1c1df"],"id":1}' \\
  <https://testnet.evm.nodes.onflow.org>

>5. Decoding the Payload

The input field is short and lacks function selectors, so treating it as ASCII hex is the natural next step. A single Python oneliner does the trick:

bash
python - <<'PY'
data = "6e6974657b366f30645f4a30625f6e30775f54723463335f6d595f6d306e3372307d"
print(bytes.fromhex(data).decode())
PY

Output:

nite{6o0d_J0b_n0w_Tr4c3_mY_m0n3r0}

No further decoding is required—the author literally embedded the flag as plain ASCII once the entire laundering tree had been enumerated.