webfreemedium

ORDER66

umasscybersec

Task: a Flask grid stores one user-controlled value per session in Redis, but only one order slot is rendered unsafely and its index is derived from a leaked PRNG seed. Solution: recover the unsafe slot from the shared seed, store JavaScript there, then send the admin bot to the shared URL so console.log(document.cookie) returns the non-httpOnly flag cookie.

$ ls tags/ techniques/
predictable_prng_reconstructionstored_xss_cookie_theftadmin_bot_console_exfiltrationshare_url_seed_leak

ORDER66 — UMassCTF 2026

Summary

This challenge is a compact stored XSS bot chain with one extra twist: execution order matters. The title is the hint. There are 66 possible order slots, but only one is rendered with Jinja's |safe, and the application leaks enough state to predict exactly which slot that is.

Once the unsafe slot is known, the rest of the attack is straightforward:

  1. store <script>console.log(document.cookie)</script> in the one unsafe box,
  2. send the admin bot to the shared /view/<uid>/<seed> URL,
  3. let the bot execute the stored XSS,
  4. steal the flag cookie because it is explicitly set with httpOnly: false,
  5. read it back because /admin/visit returns Puppeteer's stdout.

Description

Execute Order... wait which one was it?

The page presents a 66-cell grid where only one box may contain data at a time. A share URL is exposed for the current session, and an admin bot can be asked to visit a supplied page.

Source Analysis

Flask application

app.py assigns each visitor a user_id and a numeric seed in the session. The important helper is:

def get_grid_context(uid, seed): random.seed(seed) v_index = random.randint(1, 66) data = {i: (db.get(f"{uid}:box_{i}") or "") for i in range(1, 67)} return data, v_index

So the dangerous slot is not random per render in any strong sense. It is fully determined by the leaked seed.

The main page also leaks a share URL directly in the HTML:

value="http://{{ host }}/view/{{ user_id }}/{{ seed }}"

That is enough to recover both values needed to recompute the vulnerable index locally.

Template sink

templates/index.html is where the bug lives:

{% if i == vuln_index %} {{ content | safe }} {% else %} {{ content }} {% endif %}

...

🔒

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]