Locked and Loaded
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.
$ ls tags/ techniques/
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Locked and Loaded — Hack The Box
Summary
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.
Description
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.
Vulnerability Analysis
Relevant contracts
- Setup.sol funds the target with 2 ether and marks the instance solved when the target balance reaches zero.
- Lockers.sol manages users, items, transfers, and sales.
The important pricing detail was that price[Rarity.Mythic] = 1 ether, so selling the same Mythic item twice is enough to empty the contract.
Storage layout and why private is readable
In 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:
-
itemsis a dynamic array at slot3- slot
3stores the array length - element data starts at
keccak256(3) - each
Itemoccupies 3 slots:nameownerrarity
So item
istarts at:base = keccak256(3) item_i = base + i * 3 - slot
-
usersismapping(string => string)at slot0For a username key, the value slot is derived from the mapping base slot:
user_slot = keccak256(bytes(username) || pad32(0))
...
$ grep --similar
Similar writeups
- [blockchain][free]Magic Vault— hackthebox
- [blockchain][free]Honor Among Thieves— hackthebox
- [blockchain][free]Token to Wonderland— hackthebox
- [blockchain][free]False Bidding— hackthebox
- [web][free]Prison Pipeline— hackthebox_business_ctf_2024