The Block City Times V2
umasscybersec
Task: a token-gated news site exposed a Flask launcher, Spring Boot app, editorial bot, and report-runner bot. Solution: reuse the upload-to-stored-XSS bug, switch the app into dev mode through Actuator, then reflect a duplicate-tag exception at /api/tags to execute JavaScript in the FLAG-bearing browser.
$ ls tags/ techniques/
The Block City Times V2 — UMass Cybersecurity CTF
Description
Organizer description was not preserved in the local task files.
English summary: the public entrypoint was only a Flask wrapper that required a team CTFd token and then spawned a per-team instance. After creating a live instance first, the real target was a Spring Boot newspaper app with an editorial Puppeteer bot, a report-runner Puppeteer bot, and a dev-only reporting feature.
Analysis
The V2 source tree in v2_assets/ showed that the core upload bug from V1 still existed. In StoryController.java, /submit accepted uploads based only on MultipartFile.getContentType(), which is attacker-controlled in a multipart request. Later, /files/{filename} served the stored file using Files.probeContentType(filePath), so a file uploaded as text/plain but named story.html was accepted and then rendered as HTML when fetched.
That became stored XSS because editorial/server.js logged in as admin and automatically visited /files/<filename> after every submission. Sanitizing the launch step is important here: the public wrapper at blockcitytimesv2.web.ctf.umasscybersec.org:5000 required a private team token and created an ephemeral per-team instance, so the safe writeup wording is simply: create a live instance first.
The obvious V1 route no longer worked. ReportController.java now rejected endpoints containing .. or %, normalized the path, and required it to match ^/api/[a-zA-Z0-9/_-]+$. So the old /api/../files/<payload> trick was patched out in V2.
The new path came from combining three details:
application.ymlexposedenvandrefresh, withmanagement.endpoint.env.post.enabled: true.AppProperties.javawas under@RefreshScope, and the report feature only worked whenapp.active-config == dev.GlobalExceptionHandler.javareturned500 Internal Server Error: <message>in dev mode.
So the editorial-bot XSS could send:
POST /actuator/envwith{"name":"app.active-config","value":"dev"}POST /actuator/refresh
After waiting briefly, /admin exposed the dev reporting form.
...
Permission denied (requires auth)
Sign in to read this free writeup
This writeup is free — just sign in with GitHub to read it.
$ssh [email protected]