Ethernaut Challenge #1 Solution — Fallback

·

4 min read

Ethernaut Challenge #1 Solution — Fallback

Photo by Ju Guan on Unsplash

This is Part 1 of the "Let’s play OpenZeppelin Ethernaut CTF" series, where I will explain how to solve each challenge.

The Ethernaut is a Web3/Solidity based wargame created by OpenZeppelin. Each level is a smart contract that needs to be 'hacked'. The game acts both as a tool for those interested in learning ethereum, and as a way to catalogue historical hacks in levels. Levels can be infinite and the game does not require to be played in any particular order.

Challenge #1: Fallback

The goal of this challenge is to claim ownership of the Fallback contract and reduce its balance to 0.

Study the contracts

First thing that we notice, the Solidity compiler version used is < 0.8.x. This mean that the contract would be prone to math underflow and overflow bugs.

This contract is importing and using OpenZeppelin SafeMath library, but they are not using it. There is still no way to exploit it by overflow, at least in this specific case.

The only way to drain the contract is via the withdraw function that can be called only if the msg.sender equal to the value of the variable owner (see the onlyOwner function modifier). This function will transfer to the owner address, all the funds in the contract.

Let's look at the code:

function withdraw() public onlyOwner {
    owner.transfer(address(this).balance);
}

So if we find a way to change the owner value to our address, we will be able to drain all the ether from the contract.

There are actually two places in the contract where the owner variable is updated with msg.sender

1) The contribute function 2) The receive function

The contribute function

function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if (contributions[msg.sender] > contributions[owner]) {
        owner = msg.sender;
    }
}

This function allows the msg.sender to send wei to the contract. Those wei will be added to the user's balance, tracked by the contributions mapping variable.

If the total contribution made by the user is greater than the one made by the actual owner (contributions[msg.sender] > contributions[owner]) the msg.sender will become the new owner.

The problem is that the contribution made by the owner is equal to 1000 ETH. It's not written anywhere in the description of the challenge, but we can think that our user will start with a limited amount of ETH, an amount that does not allow us to contribute more than the owner. So, we need to find another way.

The receive function

This is a "special" function that is called "automatically" when someone sends some ether to a contract without specifying anything in the "data" field of the transaction.

Quoting from the official Solidity blog post when the function has been introduced:

A contract can now have only one receive function, declared with the syntax: receive() external payable {…} (without the function keyword). It executes on calls to the contract with no data (calldata), e.g. calls made via send() or transfer(). The function cannot have arguments, cannot return anything and must have external visibility and payable state mutability.

Here's the code:

receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
}

In the receive function, the owner is updated with msg.sender only if the amount of wei sent with the transaction is > 0 and our contribution in contributions[msg.sender] is > 0

At this point, we have all the pieces to build the puzzle and win the challenge. Let's see the solution!

Solution code

Here's what we need to do:

1) Contribute to the contract with at max 0.001 ether (to pass the require check) calling the contribute function so the contributions[msg.sender] will be greater than zero 2) Send 1 wei directly to the contract to trigger the receive function and become the new owner 3) Call withdraw and bring home all the ETH stored in the contract!

And here's the Solidity code:

function exploitLevel() internal override {
    vm.startPrank(player);

    // send the minimum amount to become a contributor
    level.contribute{value: 0.0001 ether}();

    // send directly to the contract 1 wei, this will allow us to become the new owner
    (bool sent, ) = address(level).call{value: 1}("");
    require(sent, "Failed to send Ether to the level");

    // now that we are the owner of the contract withdraw all the funds
    level.withdraw();

    vm.stopPrank();
}

You can read the full solution of the challenge opening Fallback.t.sol

Further reading

Disclaimer

All Solidity code, practices and patterns in this repository are DAMN VULNERABLE and for educational purposes only.

I do not give any warranties and will not be liable for any loss incurred through any use of this codebase.

DO NOT USE IN PRODUCTION.