$ cat writeup.md…
$ cat writeup.md…
hackthebox
Task: analyze a Solidity locker marketplace that stores usernames, passwords, and items on-chain. Solution: recover private data directly from storage, steal the Mythic item, and use reentrancy in sellItem() to sell it twice and drain 2 ether.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
The challenge combined two classic Solidity mistakes: trusting private storage for secrets and performing an external payment before internal state cleanup. By reading storage directly from the RPC endpoint, I recovered item owners and their passwords, transferred the only Mythic item to an attacker-controlled contract, and reentered sellItem() to sell the same item twice. That drained the full 2 ether from Lockers, making Setup.isSolved() return true.
Organizer description was not preserved in the local task notes.
The remote instance exposed /connection_info and /rpc. The goal was to satisfy Setup.isSolved(), which checks whether address(TARGET).balance == 0.
The important pricing detail was that price[Rarity.Mythic] = 1 ether, so selling the same Mythic item twice is enough to empty the contract.
private is readableIn Solidity, private only prevents other contracts from accessing a variable through Solidity's type system. It does not encrypt or hide storage. Any RPC user can call eth_getStorageAt and read raw storage slots.
Two layouts mattered here:
items is a dynamic array at slot 3
3 stores the array lengthkeccak256(3)Item occupies 3 slots:
nameownerraritySo item i starts at:
base = keccak256(3) item_i = base + i * 3
users is mapping(string => string) at slot 0
For a username key, the value slot is derived from the mapping base slot:
user_slot = keccak256(bytes(username) || pad32(0))
...
$ grep --similar