$ 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.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
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.
...
$ grep --similar