blockchainfreemedium

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/
storage_enumerationprivate_state_recoverymapping_slot_derivationreentrancy_drain

$ 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:

  1. items is a dynamic array at slot 3

    • slot 3 stores the array length
    • element data starts at keccak256(3)
    • each Item occupies 3 slots:
      • name
      • owner
      • rarity

    So item i starts at:

    base = keccak256(3) item_i = base + i * 3
  2. 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

Similar writeups