关卡链接: Ethernaut Level 24 - Puzzle Wallet
攻击类型: 存储布局冲突 / 逻辑漏洞
难度: ⭐⭐⭐⭐⭐
本关的目标是成为 PuzzleProxy
合约的 admin
。
这是一个涉及代理合约(Proxy)的复杂挑战,包含了多个漏洞的组合利用。我们需要分步解决几个子问题。
首先,我们检查 PuzzleProxy
(代理) 和 PuzzleWallet
(逻辑) 合约的存储布局。
PuzzleProxy
的存储:
1 | contract PuzzleProxy is Proxy { |
PuzzleWallet
的存储:
1 | contract PuzzleWallet is Ownable { |
由于代理模式使用 delegatecall
,PuzzleWallet
的代码会直接操作 PuzzleProxy
的存储。这就导致了存储槽的冲突:
PuzzleProxy
的 pendingAdmin
(slot 0) 实际上对应 PuzzleWallet
的 owner
(slot 0)。PuzzleProxy
的 admin
(slot 1) 实际上对应 PuzzleWallet
的 maxBalance
(slot 1)。我们的最终目标是成为 PuzzleProxy
的 admin
。根据存储冲突,我们只需要将 PuzzleWallet
的 maxBalance
设置为我们的地址即可。
owner
并加入白名单要调用 setMaxBalance()
,我们必须是白名单用户。要加入白名单,我们必须是 PuzzleWallet
的 owner
。
PuzzleWallet
的 owner
存储在 slot 0。PuzzleProxy
的 proposeNewAdmin()
函数可以修改 pendingAdmin
,也就是修改 slot 0。因此,第一步是通过调用 proxy.proposeNewAdmin(player_address)
来将 wallet.owner
设置为我们自己的地址。
成为 owner
后,我们就可以调用 wallet.addToWhitelist(player_address)
将自己加入白名单。
multicall
逻辑漏洞与清空合约余额现在我们是白名单用户了,但 setMaxBalance()
还有一个要求:require(address(this).balance == 0, "Contract balance is not 0")
。合约在部署时被存入了 0.001 ether,我们需要想办法将合约余额清空。
execute()
函数可以提款,但我们只能提出我们存入的金额。问题在于合约中已有的 0.001 ether。
关键在于 multicall()
函数:
1 | function multicall(bytes[] calldata data) external payable onlyWhitelisted { |
函数通过 depositCalled
标志位来防止在一次 multicall
中多次调用 deposit()
。但是,这个保护措施是有缺陷的。depositCalled
是一个局部变量,它的作用域仅限于单次 multicall
调用。如果我们在一个 multicall
调用中嵌套另一个 multicall
调用,那么内部的 multicall
会有自己的、全新的 depositCalled
标志位。
这允许我们绕过检查,实现“双重存款”:
multicall
发送 0.001 ether。multicall
的第一个调用是 deposit()
。这会把我们的 msg.value
(0.001 ether) 存入,并将我们的余额记录为 0.001 ether。multicall
的第二个调用是对 multicall
自身的嵌套调用。在这个嵌套调用中,我们再次调用 deposit()
。delegatecall
的特性,msg.value
在嵌套调用中保持不变。因此,第二次 deposit()
会再次将同一个 msg.value
(0.001 ether) 存入,使我们的记录余额变为 0.002 ether。我们只发送了 0.001 ether,但在合约中的存款记录却是 0.002 ether。现在,我们调用 execute(player, 0.002 ether, "")
,就可以提走合约中所有的资金(我们存入的0.001 + 合约原有的0.001)。
owner
: 调用 proxy.proposeNewAdmin(player)
。wallet.addToWhitelist(player)
。multicall
调用,发送 0.001 ether,使自己的存款记录变为 0.002 ether。wallet.execute(player, 0.002 ether, "")
提走所有资金。admin
: 调用 wallet.setMaxBalance(uint256(uint160(player)))
,将 maxBalance
(即 admin
) 设置为我们的地址。1 | // SPDX-License-Identifier: Unlicense |
multicall
漏洞: multicall
中的重入保护应该使用状态变量而不是局部变量。将 depositCalled
声明为合约的状态变量,并在 multicall
开始时设置,结束时清除,可以防止嵌套调用绕过检查。delegatecall
)。将功能分解为更小、更原子化的函数可以减少意外的交互。核心概念:
delegatecall
的核心风险之一。代理和逻辑合约的存储变量必须精确对齐,否则一个合约的变量可能会被另一个合约的函数意外地修改。delegatecall
: 对 delegatecall
的嵌套调用会继承原始调用的上下文(如 msg.sender
, msg.value
),但会创建新的局部变量作用域,这可能被用来绕过基于局部变量的安全检查。攻击向量:
proposeNewAdmin
)来修改一个关键的状态变量(owner
)。multicall
中基于局部变量的重入保护缺陷,通过嵌套调用实现双重记账,从而窃取合约资金。防御策略:
在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓