webfreehard

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/
stored_xss_via_content_type_confusionactuator_env_overrideconfig_refresh_abusebot_cookie_exfiltrationduplicate_element_exception_reflection

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:

  1. application.yml exposed env and refresh, with management.endpoint.env.post.enabled: true.
  2. AppProperties.java was under @RefreshScope, and the report feature only worked when app.active-config == dev.
  3. GlobalExceptionHandler.java returned 500 Internal Server Error: <message> in dev mode.

So the editorial-bot XSS could send:

  • POST /actuator/env with {"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]