1.0 Fallback [CTF][Blockchain Security]

1.0 Fallback [CTF][Blockchain Security]

ยท

4 min read

Ethernaut is a series of fun challenges that teach you how to hack smart contracts on the Ethereum platform. One of the challenges is called "Fallback." In this challenge, you will need to use your coding skills to exploit a vulnerability in a contract to take control of it and change its behavior.


To beat this challenge, you will need to:

  1. Claim ownership of the contract

  2. Reduce its balance to 0

If you're a beginner, don't worry! You'll learn a lot as you go. Just make sure to try the challenge and then come back to the walkthrough for guidance.


Now, let's analyze the code to see how we can achieve these goals:

Code Analysis

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Fallback {
    // ...
}

These are basic things for all the smart contracts we look into.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Fallback {

  mapping(address => uint) public contributions;
  address public owner;

  constructor() {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

  // ...
}

The contract has a few state variables, including a mapping called contributions and an address called owner

The constructor function sets the owner to the deployer's address and adds the deployer's address to the contributions mapping with a value of 1000 ETH. It sets the owner's address ๐Ÿค” but but but... we can't call a constructor as

constructor is a special function that is invoked only once at the time of deployment

  modifier onlyOwner {
        require(
            msg.sender == owner,
            "caller is not the owner"
        );
        _;
    }

There is also a modifier called onlyOwner which restricts access to certain functions to the owner only.

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

The contribute function is payable (meaning it can receive ETH) and publicly accessible. It updates the amount that a contributor has contributed and if the contributor's contribution is greater than the owner's a contribution, sets the owner to the contributor.

However, there is a restriction on the amount that can be contributed (it must be less than 0.001 ETH).

So there might be fewer chances of exploiting it as numerous transactions are needed to be performed to achieve our goal.

  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }

The getContribution function is a view (meaning it does not change the blockchain state) and publicly accessible. It simply returns the contribution amount for the msg.sender . This acts like a utility function to check the contribution amount.

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

The withdraw function is publicly accessible, but can only be called by the owner. It allows the owner to transfer the entire contract balance to their own address.

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

Finally, there is a receive function (also known as a fallback function) which can be called by external contracts and is payable. It requires the msg.value to be greater than 0 and the contributions of the msg.sender to be greater than 0. If these conditions are met, it sets the owner to the msg.sender.

Fallback functions are called when a contract receives ETH without any other function being specified.


There are a few things to note about this code. ๐Ÿ“

First, the logic in the receive function is questionable โ€“ it only requires the msg.value to be greater than 0 and the contributions of the msg.sender to be greater than 0, which may make it easier to exploit.

Additionally, the contribute function has a low limit on the amount that can be contributed, which may make it difficult to achieve our goal of claiming ownership of the contract.


With these points in mind, let's think about how we can exploit this contract to achieve our goals. One possibility is to try and call the receive function with a msg.value that is greater than 0 and a contributions value that is also greater than 0. This may allow us to claim ownership of the contract.

Once we have ownership, we can call the withdraw function to transfer the contract balance to our address, reducing the balance to 0.

Remember, if you get stuck or need help, don't hesitate to come back to this walkthrough for guidance. In the next blog post, we go through the actual challenge hands-on. Good luck! โœจ

ย