$ cat writeup.md…
$ cat writeup.md…
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.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
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:
<script>console.log(document.cookie)</script> in the one unsafe box,/view/<uid>/<seed> URL,flag cookie because it is explicitly set with httpOnly: false,/admin/visit returns Puppeteer's stdout.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.
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.
templates/index.html is where the bug lives:
{% if i == vuln_index %} {{ content | safe }} {% else %} {{ content }} {% endif %}
...
$ grep --similar