webfreehard

The Block City Times

umasscybersec

Task: a token-gated news site chained a Flask launcher, a Spring Boot app, admin bots, file upload, and Actuator exposure. Solution: upload HTML as text/plain for stored XSS, switch the app into dev mode, abuse the report runner, and exfiltrate the FLAG cookie through article tags.

$ ls tags/ techniques/
stored_xss_via_content_type_confusionactuator_env_overrideconfig_refresh_abusepath_normalization_bypassbot_cookie_exfiltration

The Block City Times — UMass Cybersecurity CTF

Challenge Summary

Organizer description was not preserved in the local task files.

The public entrypoint was a Flask and gunicorn gate that required a team-specific CTFd access token to spawn a per-team instance. After creating a live instance, the real target was a Spring Boot news site with file submission, an authenticated editorial bot, Actuator endpoints, and a developer report feature that only appeared in dev mode.

The final exploit was a chained web attack: upload an .html file while claiming text/plain, get stored XSS in the editorial bot, use that admin session to switch app.active-config to dev, trigger the report runner on /api/../files/<payload>, and reuse the same payload in the report-runner browser where a FLAG cookie was present. The flag was exfiltrated by overwriting article 1 tags through the authenticated API.

Reconnaissance

Local validation with docker compose up -d --build reproduced the challenge stack and made the attack path easy to verify before running it remotely. The source tree showed two important components:

  1. A public Flask gate that created a team-specific host after a valid CTFd token was supplied.
  2. A Spring Boot application behind that gate, plus two Puppeteer bots: an editorial bot for uploaded stories and a report-runner service for developer diagnostics.

Reading StoryController.java showed that /submit only validated MultipartFile.getContentType() against app.outbound.allowed-types, which included text/plain and application/pdf. However, /files/{filename} later served uploads using Files.probeContentType(filePath), so the response type was recalculated from the stored filename instead of the original claimed type.

...

🔒

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]