$ cat writeup.md…
$ cat writeup.md…
sunshinectf2026
Task: Express/Node cookie-fabrication app where an inspector bot turns attacker ingredients into document.cookie writes; /api/seal awards a golden seal only when the bot's role cookie equals chief, but role=baker is HttpOnly and cannot be shadowed by JS. Solution: overflow Chromium's ~180-cookie-per-domain jar with ~250 padding ingredients to LRU-evict the HttpOnly role=baker, then a final role=chief ingredient becomes the effective role (session survives via Priority=High), yielding the golden seal and flag.
CookieCorp is a custom-cookie fabrication service: design a batch of any ingredients you can dream up, submit it to our robotic Quality Inspector for a full inspection, and earn an official seal. Only the Chief can award the Golden Seal — the grand prize.
English summary: An Express/Node web app (nginx 1.24.0 front). A "baker" account
designs a batch of ingredients ({name,value} pairs). A headless-Chrome
"Quality Inspector" bot reviews the batch; client-side JS converts each
ingredient into a browser cookie and then asks the server to seal the batch. The
goal is to make the bot obtain the Golden Seal, which renders the flag on the
recipe page.
session cookie and an
HttpOnly role=baker cookie (both path=/)./builder): create a recipe with ingredients
[{name,value}] — up to 300 ingredients, name<=48, value<=64 chars.POST /api/recipe → returns a recipe id.POST /api/recipe/<id>/submit → queues the recipe for review./review/<id>, whose
HTML embeds window.__recipe = {...} and loads /static/js/mixer.js.For each ingredient the mixer executes:
document.cookie = name + '=' + value + '; path=/';
then calls:
fetch('/api/seal', { method: 'POST', credentials: 'same-origin', body: JSON.stringify({ recipeId }) });
So the attacker controls a set of cookies written into the bot's cookie jar.
/api/seal)/api/seal directly — it returns inspector authorization required. The seal
must be triggered by the bot.seal:'chief') when the request's role cookie
equals chief; otherwise a STANDARD seal (seal:'reviewed')..seal.gold .flag on GET /recipe/<id>.role=chief fails)The golden decision reads the role cookie — but the bot already carries an
HttpOnly role=baker cookie at path=/. document.cookie in JavaScript
cannot shadow/overwrite an HttpOnly cookie of the same name/path/domain, so
a plain role=chief ingredient is silently ignored and always yields a
STANDARD seal. The mixer also hardcodes ; path=/, defeating path-based cookie
ordering / "sandwich" tricks.
; = " , SPACE CR LF (keep $ / < { } | : @ % # +).; " , SPACE CR LF \ (keep = $ / < > ...).%3B, %22, %2C, %09, %0d%0a) survive
verbatim in storage, but do not help: the browser document.cookie setter
treats them literally (not as separators), so no attribute/cookie splitting.cookie library: value is decodeURIComponent'd, names are not
decoded, and it is first-wins on duplicate names.role=chief (and Chief/CHIEF/ROLE/... case variants) → STANDARD, due to
the HttpOnly shadow.chief/golden/true/1,
is_chief, golden, seal, ...) → all STANDARD. Golden is not keyed on a
new cookie name.role:chief, seal, status) → forced
back to baker; no effect.$Version/quoted values) does not apply to Node's
cookie library./admin, /chief, /flag, /source, /.git; no default creds./api/seal auth is a bot-only secret cookie — not header/UA/Origin based.The decisive insight: the golden check reads a cookie, the blocker is the
HttpOnly role=baker the JS mixer cannot overwrite — but it can get the
browser to evict that cookie.
Chromium cookie-jar overflow (LRU eviction). Chromium enforces roughly 180
cookies per domain and, when exceeded, evicts existing cookies (least-recently-
used). By having the mixer set ~250 padding cookies first, the bot's HttpOnly
role=baker gets evicted. A subsequent non-HttpOnly role=chief ingredient
then becomes the effective role cookie because nothing is shadowing it.
Session survives. The session cookie is set with Priority=High, so it
is preferentially retained during eviction — inspector authorization stays valid
and the bot can still successfully call /api/seal.
name = pad0..pad249, value = "y"*60 (arbitrary
distinct names, long values to consume jar quota).name = role, value = chief.The mixer runs document.cookie = name + '=' + value + '; path=/' for each in
order, overflowing the jar (evicting HttpOnly role=baker) and finally setting
non-HttpOnly role=chief. When it POSTs /api/seal, the server reads
role=chief → Golden Seal. The flag renders in .seal.gold .flag on
GET /recipe/<id>.
#!/usr/bin/env python3 # CookieCorp — cookie-jar-overflow eviction of an HttpOnly role cookie in an # inspector bot, escalating role=baker -> role=chief for the Golden Seal. import requests BASE = "https://TARGET_HOST" # sunshinectf .games web target s = requests.Session() # 1) Register a fresh baker (server issues HttpOnly session + HttpOnly role=baker) import random, string user = "baker_" + "".join(random.choices(string.ascii_lowercase, k=8)) s.post(f"{BASE}/api/register", json={"username": user, "password": "Passw0rd!"}) # (login endpoint may differ; register auto-logs-in and sets cookies) # 2) Build the overflow recipe: # ~250 padding ingredients to overflow Chromium's ~180-cookie/domain jar, # then a FINAL role=chief once the HttpOnly role=baker has been evicted. ingredients = [{"name": f"pad{i}", "value": "y" * 60} for i in range(250)] ingredients.append({"name": "role", "value": "chief"}) # must be LAST r = s.post(f"{BASE}/api/recipe", json={"ingredients": ingredients}) recipe_id = r.json()["id"] # 3) Queue it for the Quality Inspector bot. s.post(f"{BASE}/api/recipe/{recipe_id}/submit") # 4) The headless-Chrome bot visits /review/<id>, mixer.js writes all cookies # (evicting HttpOnly role=baker; session survives via Priority=High), then # POSTs /api/seal -> server reads role=chief -> GOLDEN seal. # # 5) Poll /recipe/<id>; a golden batch renders the flag inside .seal.gold .flag. import time for _ in range(30): time.sleep(3) page = s.get(f"{BASE}/recipe/{recipe_id}").text if "seal gold" in page or "seal.gold" in page: # Flag is in the .seal.gold .flag element. Format: sun{REDACTED} print("GOLDEN — flag rendered on the recipe page") break
Use this technique when:
document.cookie
writes, but the privilege-deciding cookie is HttpOnly and cannot be
overwritten from JavaScript (naïve role=chief is silently ignored).; path=/; server uses Node's first-wins cookie parser).Priority=High, meaning it survives
eviction while ordinary cookies are dropped — a strong hint that overflow is
intended.$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar