webfreemedium

Building Blocks Market

umasscybersec

Task: a Flask marketplace sits behind nginx and a custom Python cache proxy, while an authenticated Puppeteer admin bot reviews user-submitted URLs. Solution: abuse a CRLF-based cache key/path desync plus first-header-wins Cache-Control parsing to cache the admin submissions page, leak the deterministic admin CSRF token and pending submission id, then use a normal cross-site top-level form POST to approve the submission and unlock /flag.

$ ls tags/ techniques/
cache_key_vs_upstream_path_desynccrlf_path_splittingfirst_cache_control_winscsrf_token_leak_via_cachehost_scoped_cookie_cross_origincross_site_top_level_form_postadmin_bot_form_autosubmit

Building Blocks Market — UMassCTF 2026

Overview

This challenge is a great example of a multi-layer web exploit where every layer matters:

  • a Flask marketplace backend
  • an nginx reverse proxy
  • a custom Python cache proxy in front of nginx
  • a Flask admin renderer for moderation
  • a Puppeteer admin bot visiting attacker-controlled URLs

The goal is to make any product public. Once at least one product has is_public=True, /flag returns the flag.

At first glance this looks like a standard admin-bot CSRF problem. It is not. The intended chain is a combination of:

  1. cache deception through a CRLF-suffixed path,
  2. broken Cache-Control handling in the custom proxy,
  3. leakage of the admin CSRF token and pending submission id from a cached admin page,
  4. a cross-site top-level form POST that still carries the admin session cookie.

The flag pun is the whole challenge: do not mess with nginx and Chromium. You need both the nginx/header-ordering side and the Chromium/cookie-navigation side for the exploit to work.

Target behavior

The flag endpoint only checks whether any product has been approved:

@marketplace_bp.route('/flag') @login_required def flag(): has_public_listing = Product.query.filter_by(is_public=True).first() is not None if not has_public_listing: return "No flag for you :(", 200 return current_app.config.get('FLAG'), 200

So the real problem is: how do we force an admin approval?

Architecture

The deployed stack is effectively:

attacker/browser | v cache_proxy:5555 ---> nginx ---> backend Flask \-> admin Flask bot (Puppeteer + Chromium) - logs in as admin - stores the admin session cookie for http://cache_proxy:5555 - visits submitted URLs

Two details matter immediately:

  1. the cache proxy is the public entry point,
  2. the admin bot authenticates specifically to http://cache_proxy:5555.

...

🔒

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]