CTF: MCTF 2025
Challenge name: Sepolia Heist
Category: Blockchain / Forensics
Difficulty: ~Medium
We’re given a text file:
challenge.txtIt contains an almost complete BIP-39 seed phrase:
thumb ten glide antique ready jaguar gap ______ despair trophy slide weapon
We’re told:
MCTF25{...} and may contain non-alphanumeric characters.Goal: recover the flag.
m/44'/60'/0'/0/0.First I created a Python virtual environment and installed the needed libraries:
python3 -m venv venv
source venv/bin/activate
pip install mnemonic eth-account requests
Then I wrote a small script to:
mnemonic,mnemo.check).# candidates.py
from mnemonic import Mnemonic
mnemo = Mnemonic("english")
wordlist = mnemo.wordlist
BASE_WORDS = [
"thumb", "ten", "glide", "antique", "ready", "jaguar",
"gap", # index 6
None, # index 7 -> missing
"despair", "trophy", "slide", "weapon"
]
MISSING_INDEX = 7
def build_phrase(w):
words = BASE_WORDS.copy()
words[MISSING_INDEX] = w
return " ".join(words)
candidates = []
for w in wordlist:
phrase = build_phrase(w)
if mnemo.check(phrase):
candidates.append((w, phrase))
print(f"Checksum-valid candidates: {len(candidates)}")
for w, phrase in candidates:
print(w, "->", phrase)
Running this:
python candidates.py > candidates.txt
Result: 134 checksum-valid candidate mnemonics (out of 2048 possible words).
Next, for each candidate mnemonic I derived the Ethereum address at the standard Metamask path m/44'/60'/0'/0/0:
# derive_addresses.py
from mnemonic import Mnemonic
from eth_account import Account
Account.enable_unaudited_hdwallet_features()
mnemo = Mnemonic("english")
BASE_WORDS = [
"thumb", "ten", "glide", "antique", "ready", "jaguar",
"gap", None, "despair", "trophy", "slide", "weapon"
]
MISSING_INDEX = 7
def build_phrase(w):
words = BASE_WORDS.copy()
words[MISSING_INDEX] = w
return " ".join(words)
candidates = []
for w in mnemo.wordlist:
phrase = build_phrase(w)
if mnemo.check(phrase):
candidates.append((w, phrase))
for w, phrase in candidates:
acct = Account.from_mnemonic(phrase, account_path="m/44'/60'/0'/0/0")
print(w, acct.address)
I saved the output:
python derive_addresses.py > addresses.txt
Each line looks like:
person 0xADEceCb8f16670F0E132e6B248cDebA4321416Cc
...
So for each potential missing word we now have the corresponding Ethereum address.
I initially tried to automate this using the Sepolia Etherscan API, but the free endpoint kept returning status=0, message='NOTOK'.
So I switched to a manual (but still fast) approach using the Etherscan web UI.
From addresses.txt I extracted just the addresses:
awk '{print $2}' addresses.txt > just_addresses.txt
Then:
sepolia.etherscan.io in a browser.just_addresses.txt, I pasted it into the search bar.Eventually I found an address that had transactions:
0xADEceCb8f16670F0E132e6B248cDebA4321416Cc
This address corresponds to the missing word:
person
So the full mnemonic is:
thumb ten glide antique ready jaguar gap person despair trophy slide weapon
On the Etherscan address page for
0xADEceCb8f16670F0E132e6B248cDebA4321416Cc:
From address equals this wallet (the first tx the owner made, not just received).There was an earlier incoming tx with empty input data (0x), which is just a funding tx and not relevant.
The second transaction, sent from the wallet, is the one we want.
On the transaction details page:
MCTF25{b1p39_jungl3_tr3@sur3_hunt}
Replace the placeholder with the exact flag you saw in the UTF‑8 view.
thumb ten glide antique ready jaguar gap person despair trophy slide weapon
0xADEceCb8f16670F0E132e6B248cDebA4321416Cc
MCTF25{b1p39_jungl3_tr3@sur3_hunt}