$ cat writeup.md…
$ cat writeup.md…
asisctf2026
Task: a spreadsheet SPA evaluates attacker formulas server-side through `/api/sheet` in a persistent session realm. Solution: leak the live scope, plant a persistent setter on `secret`, and repeat a `Symbol.unscopables` trigger until `shared` captures the hidden value.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Partial organizer text preserved in local notes: "strange calculations are simply features" and "perfectly secure".
English summary: The target at http://91.107.252.227:3000 is a spreadsheet-like single-page application. The frontend references only /api/sheet; GET /api/sheet returns sheet state as JSON and POST /api/sheet evaluates formulas server-side. The solve was to turn that evaluator into a same-session side channel and capture the hidden secret value.
web-recon.json showed a single HTML page at / and one API endpoint at /api/sheet; common extra routes returned 404.nginx/1.31.4.= are evaluated on the server./api/sheet, so the attack surface was the evaluator rather than a hidden secondary API.The first important primitive was a getter that exposed the live evaluation scope:
={}.__proto__.__defineGetter__("leak",valueOf)
After that, =constructor.keys(leak) revealed the interesting bindings:
cells,user,shared,secret
This immediately explained the app model:
cells holds the workbook state.user was the literal string u in the tested session.shared is a session-local array that persists across requests.secret exists in scope, but direct reads were unhelpful: =secret returned an empty string and =secret.length returned 0.That meant the flag was not recoverable with a simple one-shot =secret read.
The second key observation was that prototype and setter/getter changes in the session realm persisted across later requests in the same session. In particular, this setter survived and fired later:
={}.__proto__.__defineSetter__("secret", shared.push.bind(shared,"secret"))
This proved two useful facts:
...
$ grep --similar