Ethernaut Walkthrough — Level 6: Delegation

Published on Dec 09, 2021

This level zooms in on a special Solidity method called delegatecall(). When deploying the level instance on Ethernaut, we can only work with the Delegation contract, which is the instance we get.

The delegatecall() method lets you call a function on another contract while keeping the original data context. Another way to think of it is that you are trusting the called contract with the state of your calling contract.

In this level, we need to understand that we will be running the function pwn() of the called contract as if that code was executed inside the calling contract, with our own data.

The first thing I tried, was creating a contract on Remix that called the function pwn() on the Delegation contract, like so:

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

contract Hack {
    event Response(bool success, bytes data);

    function exploit() public {
      // set the address below to the instance address of your Ethernaut level
      // this address points to your Delegation contract, not the Delegate contract
      address delegationContract = 0xb53DC644De818500AD1e6685235290a3A9EE4192;

      // we then try to call the pwn() function
      // since the function does not exist, it falls back to the fallback method
      (bool success, bytes memory data) = delegationContract.delegatecall(abi.encodeWithSignature("pwn()"));
    
      emit Response(success, data);
    }
}

Now, this worked and changed the owner of the Delegation contract but not into our own address. It changed the owner into the address of our remix contract. That is because the exploit() function is executed from the Hack contract, passing the msg.sender to the Delegation contract and then executing the pwn() function on the Delegate contract with the msg.sender being passed from our Hack contract.

I had a hard time understanding this the first time, so please read the explanation a couple of times. In the screenshot below I try to explain what's going on with in more detail.

The hack

Let's try to hack this level. If you read the explanation above you noticed that I failed at hacking the contract via remix, because that would set the owner of the Delegation contract to the address of the remix contract, not the address of our personal account in Ethernaut.

So that led me to look at other options of calling the pwn() function on the Delegation contract directly. The only option left was to use the web3 client library in the Ethernaut console. In order to call a method on a contract, we can use the the sendTransaction() method.

I was not too familiar with this method but after doing some digging in the docs, I found that we could pass in the ABI byte string of the function we'd like to call. Ok, so now we just needed to figure out how to get the byte string of the function. There are other methods (like doing this with web3 I suppose) but I opted to create a function in my remix contract to do just that. It looks like this:

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

import "hardhat/console.sol";

contract Hack {
     function getsig () public pure returns (bytes memory) {
      return abi.encodeWithSignature("pwn()"); // returns "0xdd365b8b"
    }

}

When you deploy the contract on Remix and call the getSig() method, you'll get the string you see above in the comments.

Now, we can call this method on our Delegation contract in the ethernaut console like so:

contract.sendTransaction({data: "0xdd365b8b"});

This will:

  1. try to call the pwn() function on the Delegation contract, which doesn't exist
  2. the fallback() method in Delegation will be called, calling pwn() in Delegate
  3. the pwn() method from Delegate will run as if it was running in Delegation
  4. that will set the owner of Delegation to the msg.sender, being the player address

Submit your level. Done.

Security lessons learned

I actually learned a lot about what delegatecall() does exactly and how it works. This pattern is really handy if you want to create a Library structure where you have a contract that has code you can now reuse easily from other contracts by calling the method as if it was running right inside your own contract.

Of course that poses a risk, especially because the delegate contract has access to the full state or your contract. Here is an explanation of how this technique was used in a 30 million dollar hack of the Parity Wallet.

The combination of using delegatecall() in combination with fallback methods, is a dangerous one. It's better to only allow specific methods to be able to use delegatecall on a contract.

Continue from here

Here's my solution for level 7

No comments? But that’s like a Gin & Tonic without the ice?

I’ve removed the comments but you can shoot me a message on LinkedIn to keep the conversation going.