Table of Contents
PassCode — TryHackMe CTF Writeup #
Platform: TryHackMe (Hackfinity Battle CTF)
Category: Blockchain / Web3
Difficulty: Easy
Date: 2026-03-23
Author: t0nt0n
Reading time: ~4 min
Overview #
PassCode is a smart contract challenge. A deployed Ethereum contract holds a passcode that must be submitted to flip isSolved() to true. The trick: the passcode is stored in a private storage variable — which is NOT actually private on a public blockchain.
Reconnaissance #
The challenge API provides everything needed to get started:
1RPC_URL=http://10.129.160.37:8545
2API_URL=http://10.129.160.37
3
4curl -s ${API_URL}/challenge | jq .
API response (player wallet + contract address)
1{
2 "name": "blockchain",
3 "player_wallet": {
4 "address": "0x738fB6Ba7A06E89F58df5d3da7c36C3Fb3e58ead",
5 "private_key": "0x8954bc7b973945979af2c8ab8440fea0ca0a161d33dd3338a6e41f88ac61e80d",
6 "balance": "1.0 ETH"
7 },
8 "contract_address": "0xf22cB0Ca047e88AC996c17683Cee290518093574"
9}
Exploitation #
Reading contract storage #
All contract storage is publicly readable via eth_getStorageAt, regardless of whether variables are declared private in Solidity. Reading slots 0–3:
1for i in 0 1 2 3; do
2 echo -n "Slot $i: "
3 curl -s -X POST http://10.129.160.37:8545 \
4 -H "Content-Type: application/json" \
5 -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getStorageAt\",
6 \"params\":[\"0xf22cB0Ca047e88AC996c17683Cee290518093574\",
7 \"0x$(printf '%064x' $i)\",\"latest\"],\"id\":1}" \
8 | jq -r '.result'
9done
Raw storage output
Slot 0: 0x54484d7b776562335f6834636b316e675f636f64657d0000000000000000002c
Slot 1: 0x0000000000000000000000000000000000000000000000000000000000000000
Slot 2: 0x000000000000000000000000000000000000000000000000000000000000014d
Slot 3: 0x54686520636f646520697320333333000000000000000000000000000000001e
Decoding the storage #
1# Slot 0 — Solidity short string encoding (last byte = length)
2bytes.fromhex('54484d7b776562335f6834636b316e675f636f64657d').decode()
3# → THM{w_redacted_e}
4
5# Slot 2 — uint256 passcode
60x14d # → 333
7
8# Slot 3 — hint string
9bytes.fromhex('54686520636f646520697320333333').decode()
10# → "The code is 333"
The flag is directly readable from slot 0. Slot 3 confirms the passcode is 333 (also stored as 0x14d in slot 2).
Flag #
Reveal Flag
THM{web3_h4ck1ng_code}
Answers #
Q — What is the flag?
Reveal Flag
THM{web3_h4ck1ng_code}
Tools Used #
curl— JSON-RPC calls to the Ethereum node (eth_getStorageAt,eth_getCode)python3— hex decoding of storage slot valuesjq— JSON parsing
What Didn't Work #
eth_sendTransactionwithout signing — the node rejects unsigned transactions withunknown account. Would need to sign with the private key via web3.py or cast (Foundry) to actually call the contract function. Not needed here since the flag was already in storage.
Lessons Learned #
- In Solidity,
privateonly prevents other contracts from reading a variable — it does NOT hide it from anyone who can query the blockchain. All storage is public. - Slot layout follows declaration order: first
string→ slot 0, firstbool→ slot 1, firstuint256→ slot 2, etc. - Short strings (≤ 31 bytes) are packed inline in their slot: the last byte encodes
length * 2, the string data starts at the left of the slot. - Always read raw storage before attempting contract interaction — the answer may already be there.