# MesaNet Portal — BugForge HARD Writeup **Lab**: MesaNet Portal (BugForge HARD weekly) **Date solved**: 2026-04-30 **Flag**: `bug{Q0z4fdlQhhOxUz1GOYiGta3gLLeND66V}` **Vulnerability class**: Stored Cross-Site Scripting (XSS) via unsanitized `message` field, exploited against a privileged headless bot (`sysbot`) that owns the confidential note containing the flag. --- ## 1. Executive Summary The MesaNet portal exposes a "Rail Broadcasts" application with two key behaviors: 1. **`POST /api/rail/create` (via `/gateway`)** stores an `announcement` whose `message` field is later rendered **unescaped** as HTML. 2. **`POST /api/rail/review`** queues the broadcast for an "Automated Oversight System" — a headless browser bot (`sysbot`, user id `4`) that opens `/apps/rail?view=` with its own privileged session. The browser-side rendering pipeline does: ```js fetch('/api/rail/' + view) .then(r => r.json()) .then(data => { document.getElementById('broadcast-content').innerHTML = data.html; }); ``` `data.html` is built server-side by interpolating `${message}` directly. Anyone with a session that has `create` permission can plant a stored XSS that fires inside the bot's browser. The bot is the only user who can read `notes` id `6` (`classification: confidential`, `ownerUsername: sysbot`), so we use the bot's session as a confused deputy: * Bot fetches `/api/notes/get` for note `6` (succeeds — bot owns it). * Bot calls `/api/rail/create` to publish the note's body as a public broadcast. * We list `/api/rail/announcements` and read the flag. --- ## 2. Reconnaissance ### 2.1 Surface map | Endpoint | Method | Reachable | Notes | |---|---|---|---| | `/api/rail/announcements` | POST via `/gateway` | gateway-only | Lists all broadcasts | | `/api/rail/create` | POST via `/gateway` | gateway-only | Creates broadcast — **XSS sink** | | `/api/rail/current` | POST via `/gateway` + GET direct | both | Selects "current" broadcast, returns `{html, announcement, systemTime}` | | `/api/rail/review` | POST via `/gateway` | gateway-only | Submits a broadcast for the Oversight bot | | `/api/rail/status` | POST via `/gateway` | gateway-only | Health info | | `/api/rail/display` | GET direct | direct-only | Returns `{html, skin, systemTime}`, server-side cached 60 s with header `X-Cache`. Distractor. | The hint that circulated in chat ("4 paths through the gateway and 2 in URL directly") was slightly off: the gateway exposes 5 endpoints; the cache headers on `display` are decoration. ### 2.2 Why the rendering is vulnerable Server-side template (reconstructed from the `current` response): ```html
${priority.upper}

${message}

${timestamp} [${type.upper}]
``` `type` and `priority` are validated against an allowlist. `message` is **not** sanitised, so any HTML survives the round trip. Because the client-side renderer assigns this HTML through `innerHTML`, `