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 https://foundry.paradigm.xyz | 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 variableowner
is of address type, which is 20 bytes, and occupies storage slot 0randomData
is a constant-size array of bytes32 and occupies 160 bytes, from storage slot 1 to slot 5addressToKeys
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 6a
and b
are both of type uint128 which together pack to 32 bytes and as such both occupy slot 7secretHash
therefore occupies storage slot 8Regardless 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 https://eth-goerli.g.alchemy.com/v2/${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 https://eth-goerli.g.alchemy.com/v2/${ALCHEMY_GOERLI_ID} --private-key ${PRIVATE_KEY} "takeOwnership(uint256)()" 0x43223b17c0688af05eea8efbd2b6c424174874f5945a09fce2e3fa1afb6984b7
And view the current owner with:
cast call 0x620E0c88E0f8F36bCC06736138bDEd99B6401192 --rpc-url https://eth-goerli.g.alchemy.com/v2/${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!