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 | for (uint8 i = 0; i < targets.length;) { |
先执行再判断,这就是漏洞,我们可以先通过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 | // SPDX-License-Identifier: MIT |
3.2 FakeVault.sol
1 | // SPDX-License-Identifier: MIT |
3.3 challenge.js
1 | it('Execution', async function () { |
运行结果
由结果不难看出我的想法是正确的,FakeVault
合约的sweepFunds
函数是在climberVault
中执行的
其中,采用低级调用address(vault).call(abi.encodeWithSignature("sweepFunds(address,address)", token, msg.sender))
,而不直直接用vault
调用是因为该函数已经重写了,编译报错。
解题成功。