Safe space, where we can be vulnerable

Safe space is a series of hackable smart contracts (CTF) where community members will dive deep into some solidity vulnerabilities.

Author: Giovannidisiena.eth

Also, follow Stermi.eth for another good explanation:

Reading Private Data

If you have some familiarity with Solidity smart contracts then you will have come across state variable visibility – public, internal, and private. Although it sounds counter to what you might expect, the key takeaway from this challenge is that anyone can read the value stored in a private variable.

Repeat after me:

private variables aren’t private

This keyword refers only to the ability of an external contract to access the variable, while to Shadowy Super Coders th

e value is accessible via inspecting the contract storage layout and reading the corresponding slot directly.

To pull off the hack we are going to be using Foundry (big up Georgios & Co). If you don’t already have it installed then go ahead and run curl -L | bash before running foundryup in a new session. Additional documentation is available in the official repo and book.

We will first create a new directory and initialize a new Forge project like so:

mkdir private-data && cd $_ && forge init

Go ahead and copy the contract code to PrivateData.sol. We’ll need this to inspect the contract storage layout (and if you wanted to run tests against a local fork using anvil).

The easiest method for inspecting the storage layout really is as simple as:

forge inspect PrivateData storage —pretty

From this, we can see that the variable secretKey corresponds to storage slot 8.

To arrive at the same result without the power of Forge, you would need to apply storage layout rules manually, understanding that the EVM stores state variables in 32-byte slots and performs slot packing, such that these variables get stored in a single slot, if successive declarations sum to no more than 32 bytes:

  • NUM is inlined to contract bytecode at compile time since it is a constant variable
  • owner is of address type, which is 20 bytes, and occupies storage slot 0
  • randomData is a constant-size array of bytes32 and occupies 160 bytes, from storage slot 1 to slot 5
  • addressToKeys mapping has an unpredictable size, so the elements it contains are stored starting at a different storage slot that is computed using a keccak hash which is stored at storage slot 6
  • a and b are both of type uint128 which together pack to 32 bytes and as such both occupy slot 7
  • secretHash therefore occupies storage slot 8

Regardless of which method you choose, once you have the desired storage slot it is again as simple as running the following command to read the value stored:

cast storage 0x620E0c88E0f8F36bCC06736138bDEd99B6401192 8 --rpc-url${ALCHEMY_GOERLI_ID}

And there it is! We have the secret key.

Now to take ownership of the contract, send a transaction with:

cast send 0x620E0c88E0f8F36bCC06736138bDEd99B6401192 --rpc-url${ALCHEMY_GOERLI_ID} --private-key ${PRIVATE_KEY} "takeOwnership(uint256)()" 0x43223b17c0688af05eea8efbd2b6c424174874f5945a09fce2e3fa1afb6984b7

And view the current owner with:

cast call 0x620E0c88E0f8F36bCC06736138bDEd99B6401192 --rpc-url${ALCHEMY_GOERLI_ID} "owner()(address)”

Of course, you could also use our newfound superpower and read the storage slot 0 directly.

As a quick aside, it is important to note that you should not rely on block data for randomness, as explained here and rndString is visible on Etherscan as a decoded constructor argument.

A slightly improved solution could be storing the keccak hash of the secret key in a commit-reveal type fashion, so as not to expose the key itself, and validating input against this; however, it is still of course flawed in that the correct key will become visible to everyone once someone crafts a successful transaction.

I hope that was helpful – happy hacking!

Subscribe to EthernautDAO
Receive the latest updates directly to your inbox.
This entry has been permanently stored onchain and signed by its creator.