← Back to Writeups
CUSTOMMediumBlockchain

[Blackbox] Web3_Governance

XESXOR9/2/20262 min read
#blockchain#custom#medium#proxy_storage_slot_analysis#push32_string_extraction#evm_bytecode_reverse_engineeri#implementation_takeover#uups_uninitialized_implementat

[Blackbox] Web3_Governance

Platform: caplag | Category: Blockchain | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-02-22 | Status: Solved Techniques: proxy_storage_slot_analysis, push32_string_extraction, evm_bytecode_reverse_engineering, implementation_takeover, uups_uninitialized_implementation

Summary

Task: UUPS proxy with TreasuryImplementation contract. Solution: Implementation contract was never initialized, allowing ownership takeover via direct initialize() call, then upgradeToAndCall() to hijack the proxy.

[Blackbox] Web3_Governance — CapLag CTF

Description

Same infrastructure as Web3_Lending. The governance aspect involves a TreasuryProxy (EIP-1967 proxy pattern) and TreasuryImplementation contract with UUPS upgrade functionality. The implementation contract was deployed but never initialized, leaving it vulnerable to ownership takeover.

Analysis

  1. Contract Discovery: From /runtime/config.json, identified TreasuryProxy (0x68B1D87F95878fE05B998F19b66F4baba5De1aed) and TreasuryImplementation (0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  2. Bytecode Reverse Engineering: Extracted and analyzed TreasuryImplementation bytecode (2,564 chars / 1,281 bytes). Identified function selectors:

    • 0x4f1ef286 → upgradeToAndCall(address,bytes) (UUPS upgrade)
    • 0x5b116ab8 → sweepETH(address,uint256) (withdraw ETH)
    • 0x5c60da1b → implementation() (get current impl)
    • 0x8da5cb5b → owner() (ownership check)
    • 0xc4d66de8 → initialize(address) (initializer)
  3. Vulnerability Identification: Classic UUPS pattern vulnerability — the implementation contract itself was never initialized (only the proxy was). Anyone can call initialize(address) directly on the implementation contract to become its owner, then use upgradeToAndCall to hijack the proxy. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  4. Flag Discovery: Found the flag embedded in TreasuryImplementation bytecode at offset 1023 as PUSH32 instructions.

Solution

python
#!/usr/bin/env python3
"""
UUPS Uninitialized Implementation Takeover

The vulnerability:
- TreasuryProxy delegates calls to TreasuryImplementation
- Proxy was initialized (owner set in proxy storage)
- But implementation contract itself was NOT initialized
- initialize() on implementation has no protection against re-init
- Anyone can call implementation.initialize(attacker) directly
- Then call implementation.upgradeToAndCall(malicious, data)
- This corrupts the proxy's implementation slot → full takeover
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Exploit steps:
1. Call initialize(attacker_addr) on TreasuryImplementation directly
2. Now attacker is owner of the implementation contract
3. Deploy malicious implementation that drains treasury
4. Call upgradeToAndCall(malicious_impl, drain_calldata) on implementation
5. Proxy now delegates to malicious contract → treasury drained

Bytecode analysis approach:
- Flag found at TreasuryImplementation bytecode offset 1023
"""
from web3 import Web3

w3 = Web3(Web3.HTTPProvider("http://not-web.caplag-task.ru:30647"))
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

impl_addr = "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE"

# Check if implementation is initialized (owner should be zero)
# EIP-1967 implementation slot: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
owner_slot = 0  # slot 0 typically stores owner in simple contracts
owner = w3.eth.get_storage_at(impl_addr, owner_slot)
print(f"Implementation owner: {owner.hex()}")
# If 0x000...000 → not initialized, vulnerable!

# Extract flag from bytecode
code = w3.eth.get_code(impl_addr).hex()
i = 0
while i < len(code):
    if code[i:i+2] == '7f':  # PUSH32
        data = bytes.fromhex(code[i+2:i+66])
        text = data.decode('ascii', errors='ignore').strip('\x00')
        if text and all(32 <= ord(c) < 127 for c in text if c != '\x00'):
            print(f"Offset {i//2}: {text}")
        i += 66
    else:
        i += 2
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Output: caplag{REDACTED}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍