源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/proxy/UpgradeableProxy.sol";
contract PuzzleProxy is UpgradeableProxy {
address public pendingAdmin;
address public admin;
constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) public {
admin = _admin;
}
modifier onlyAdmin {
require(msg.sender == admin, "Caller is not the admin");
_;
}
function proposeNewAdmin(address _newAdmin) external {
pendingAdmin = _newAdmin;
}
function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
admin = pendingAdmin;
}
function upgradeTo(address _newImplementation) external onlyAdmin {
_upgradeTo(_newImplementation);
}
}
contract PuzzleWallet {
using SafeMath for uint256;
address public owner;
uint256 public maxBalance;
mapping(address => bool) public whitelisted;
mapping(address => uint256) public balances;
function init(uint256 _maxBalance) public {
require(maxBalance == 0, "Already initialized");
maxBalance = _maxBalance;
owner = msg.sender;
}
modifier onlyWhitelisted {
require(whitelisted[msg.sender], "Not whitelisted");
_;
}
function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
require(address(this).balance == 0, "Contract balance is not 0");
maxBalance = _maxBalance;
}
function addToWhitelist(address addr) external {
require(msg.sender == owner, "Not the owner");
whitelisted[addr] = true;
}
function deposit() external payable onlyWhitelisted {
require(address(this).balance <= maxBalance, "Max balance reached");
balances[msg.sender] = balances[msg.sender].add(msg.value);
}
function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] = balances[msg.sender].sub(value);
(bool success, ) = to.call{ value: value }(data);
require(success, "Execution failed");
}
function multicall(bytes[] calldata data) external payable onlyWhitelisted {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success, ) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
}
分析:
代理合约:(26条消息) solidity代理合约_【03】的博客-CSDN博客_solidity 代理合约
代理模式:为其他对象提供一种代理以控制对某个对象的访问。也就是说,每次我要访问A,其实我是通过调用B的接口,而B中存有A的对象实例,并对外暴露与A相同的接口,这时候,当我们调用B时,我们仍以为自己在访问A,并对其中代理部分浑然不觉。
那么,代理模式的优点又在哪里呢?如果业务有更新,完全可以实现热部署,代理实例通过切换对象实例,此时使用者不会感觉到服务有中断或者发生了变化。
而在智能合约中,要使用代理模式,思路也是一样的,就是为了解决合约一旦上链无法更新的问题。当我们需要更新合约时,只要将代理合约中的合约实例指向新创建的合约即可。此时,对和代理合约交互的用户来说,并没有感到服务产生了变化。现在很多链游就是基于以上原理,可以不断的更新合约、更新游戏。而转发具体是怎么实现的呢?其实就是利用fallback函数,当用户访问不存在的函数时,会进入fallback,代理合约在此处即可完成转发。
对外暴露的应该是代理合约,实际合约应当藏在代理合约的后面。其实代理合约通过delegatecall调用实例合约,这里面有一个我们先前提过的问题,就是需要两个合约之间的存储槽不能产生冲突,否则会导致数据被随意修改
PuzzleProxy存储
PuzzleWallet存储
所以如果我们想修改admin其实可以从maxBalance入手。
由于代理的 pendingAdmin 和 钱包的 owner 位于同一个slot
想要通过setMaxBalance修改maxBalance有一个先决条件,那就是onlyWhitelisted,即用户需要在白名单中。而要添加到白名单,需要调用addToWhitelist方法,这又需要require(msg.sender == owner, "Not the owner");,所以我们可以先通过修改pendingAdmin修改owner,然后在逐一完成。
我们先生成selector将其和param合并生成交易中的data,以此可以发起对proposeNewAdmin(address)方法的调用。
selector=web3.utils.keccak256("proposeNewAdmin(address)").slice(0,10)
param=
000000000000000000000000+player.slice(2,)
await contract.sendTransaction({data:selector+param})
await contract.owner()
现在owner已经是我们自己啦,然后我们把自己加入白名单
await contract.addToWhitelist(player)//加入
await contract.whitelisted(player)//检查
现在就可以去setMaxBalance修改maxbalance.设置setMaxBalance需要满足条件require(address(this).balance == 0, "Contract balance is not 0");即合约本身余额不能为0,而我们通过await getBalance(contract.address)可以查询到合约还有余额0.001以太。我们应当办法将其移除。
此时我们可以查看到槽的存储情况如下,slot 0已变成了用户地址,而slot 1却是关卡合约的地址。
这是什么原因呢?这是因为,在初始化代理合约时,admin变量已经确定,所以当后续调用init时,由于存储冲突,所以maxBalance不为0,所以该方法其实调用就失败了,原始值也就没有更改。
我们想到multicall里面有这么一个限制:
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
只能存一次,如果在multicall里调用两次deposite函数,我们也不应当重复计算所存的数量。这里只是简单的对data的选择器作了单层校验,我们如果将其封装,似乎是可以绕过的.
我们需要找到调用时发送的数据。其中selector的数据为:
selector =web3.utils.keccak256("deposit()").slice(0,10)
组装成multicall时,其数据为:
再将其封装,data与selector一起传入,调用multicall,同时附上0.001Ether,此时由于没有两个deposit同时调用,就可以绕过。相当于msg.value被重复计算了两次。
通过await contract.execute(player,web3.utils.toWei('0.002'),0x0)取出所有的Ether,此时合约balance为0。此时,由于满足了我们先前所说的条件,在此之后我们就可以通过setMaxBalance去设置maxBalance从而改变admin了。
修改:
过关!
参考博客:(26条消息) [区块链安全-Ethernaut]区块链智能合约安全实战-已完结_YANG HANG的博客-CSDN博客_区块链安全实战