$ cat writeup.md…
$ cat writeup.md…
d3c2026
Task: An Express static host exposes guest uploads, a reviewer bot, webhooks, and a protected admin dashboard, with inconsistent URL decoding. Solution: Double-decoding traversal plants an admin-scoped service worker whose Navigation Preload response is exfiltrated.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
A tiny static hosting platform for guest pages, reviewer workflows, and webhook testing! Just upload your files, submit them for review, and keep your deployment notes organized.
The application lets guests upload files under /u/guest/, ask a reviewer bot to open a URL through /bot?url=..., and receive callbacks at /webhook/guest, which are displayed by /inbox. The objective is to read the reviewer-only /u/admin/dashboard.
Reconnaissance identified an Express application with the following relevant surfaces:
POST /upload, with path and content form fields;GET /bot?url=..., which accepts the submitted HTTP URL for review;/u/guest/, where uploaded guest content is served;/webhook/guest and /inbox, providing a same-origin exfiltration sink and its log;/u/admin/dashboard, which returns 403 to an unauthenticated request.Upload validation and final storage did not interpret the path identically. The form path
/u/guest/%252e%252e/admin/scope-drift-sw4.js
passed the guest-prefix validation. One decoding pass changes %25 into %, leaving the apparently guest-local path /u/guest/%2e%2e/admin/scope-drift-sw4.js. A later decode and path normalization turns %2e%2e into .., so the file is ultimately served from:
/u/admin/scope-drift-sw4.js
This is enough to move attacker-controlled JavaScript from the guest namespace into the admin URL namespace.
By default, a service worker's maximum and default scope are derived from the directory containing its script URL. A worker loaded from /u/admin/scope-drift-sw4.js therefore receives /u/admin/ scope, even though registration is initiated by a page under /u/guest/. No broader Service-Worker-Allowed header is needed because the desired scope is exactly the worker script's own directory.
Consequently, the worker controls later navigations to /u/admin/dashboard. The trigger page confirmed that the reviewer's browser registered the worker with scope http://localhost:3000/u/admin/.
...
$ grep --similar