Logo
Web
Overview

Web

n nalo_
March 7, 2026
8 min read

L’agence s’SQL risque

L'agence s'SQL risque
Difficulty
easy
Category
web
Points
50
Solves
12
Solved in time?
yes
Flag
HackHer{N0t_ThaT_H4rD}

Notre agence est persuadée d’être totalement sécurisée. Prouvez-lui le contraire en vous connectant à l’espace de monitoring de l’agence en récupérant le nom de la mission classifiée.

Format : HackHer{M1ss1on}

En ouvrant le site, on est accuillies par un site aussi beau qu’inutile (aucun bouton ne fonctionne), mais un bouton est intéressant : “Accès Sécurisé”, menant à la page /login.html

Formulaire de connexion

En remplissant le formulaire avec des données aléatoires, on peut vérifier quelles informations sont vraiment utilisées en regardant la requête et sa réponse dans les outils de développement (onglet réseau) :

Envoie du formulaire Réponse du formulaire

On voit donc qu’il n’y a pas besoin du niveau d’habilitation ni de la 2FA, seulement l’identifiant et le code d’accès. Il n’y a évidemment pas non plus d’authentification biométrique, et la connexion n’est certainement pas sécurisée par chiffrement AES-256.

Dans ce contexte de formulaire sur une épreuve web, la première idée qui vient à l’esprit est de tenter une injection SQL. On peut confirmer la faisabilité en soulevant une erreur SQL et en regardant la réponse dans un premier temps.

En rentrant ' a dans le champ de l’identifiant, on obtient une erreur SQL :

Erreur SQL

Bingo !

On peut alors créer notre injection :

admin' or 1=1; --
L'injection SQL, comment ça marche ?

Je ne vais pas vous apprendre le fonctionnement de SQL, mais on peut parler rapidement des injection SQL avec du code simple et détaillé.

Dans ce contexte, la requête SQL pour vérifier les identifiants rentrés doit ressembler à ça :

SELECT * FROM users WHERE username = '<input_username>' AND password = '<input_password>';

Si un résultat est retourné, alors l’utilisateur avec ce nom existe, et il a rentré le bon mot de passe (tous les champs sont vérifiés). Le serveur peut alors autoriser l’accès à la page suivante. Si aucun résultat n’est retourné, alors soit le nom d’utilisateur n’existe pas, soit le mot de passe est incorrect, et l’accès est refusé.

Sur le serveur, le code du backend doit ressembler à ça, avec des variables pour les champs du formulaire :

$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password';";
$result = execute_query($query);
if ($result) {
// Accès autorisé
} else {
// Accès refusé
}

Il nous faut alors un moyen de retourner un résultat à tous les coups, sans connaitre ni identifiant, ni mot de passe. Il faudrait alors modifier la requête SQL directement. Mais comment faire, si on y a pas accès ?

C’est la qu’intervient l’injection SQL : on peut injecter dans nos champs de formulaire les bons caractères pour faire en sorte que la requête SQL soit modifiée, et retourne un résultat à tous les coups.

Par exemple, si on rentre admin' OR 1=1; -- dans le champ de l’identifiant, et n’importe quoi dans le champ du mot de passe, voici ce qu’il se passe :

SELECT * FROM users WHERE username = '$username' AND password = '$password';
↓ │ └───────┐ │ │
SELECT * FROM users WHERE username = 'admin' OR 1=1; --' AND password = 'something';

Le contenu de la variable $username a été écrit telle quel dans la requête, la modifiant à notre avantage.

Décomposons notre injection :

  • admin : le nom d’utilisateur, mais il n’importe pas, puisque c’est la suite qui va nous garantir un accès
  • ' : ce caractère permet de fermer la chaîne de caractères dans laquelle le nom d’utilisateur est censé être, pour pouvoir injecter du code SQL juste après
  • OR 1=1 : cette condition est toujours vraie, quelques soient les autres conditions, elle garantit que la requête retourne au moins un résultat, même si le nom d’utilisateur n’existe pas
  • ; : ce caractère permet de terminer la requête SQL, pour éviter que le reste de la requête ne soit exécuté
  • -- : ce sont des commentaires en SQL, tout ce qui suit est ignoré, c’est une sécurité qui permet d’ignorer la partie de la requête qui vérifie le mot de passe (ou tout autre condition qui pourrait poser problème)

La requête retournant maintenant un résultat, le site nous laisse accéder à la page sécurisée.

Un peu de lecture : https://book.hacktricks.wiki/en/pentesting-web/sql-injection/index.html

On a alors accès à la page sécurisée /user.php, qui contient la mission recherchée :

Page confidentielle


L’inconnu devenu famous

L'inconnu devenu famous
Difficulty
medium
Category
web
Points
50
Solves
11
Solved in time?
yes
Flag
HackHer{T3m3raire_SQL_R34L_D4T4B4S3}

Un ami, devenu complètement fan du Téméraire après l’annonce du Président, a décidé de créer un site dédié à ce sous-marin. Il vous assure que celui-ci est totalement sécurisé.

Retrouvez le flag.

Format : HackHer{Fl4g}

Exploration

De la même manière que le premier challenge, en ouvrant ce site, on est à nouveau accueillies par un site aussi beau qu’inutile, et cette fois-ci le seul élément intéressant est la barre de recherche permettant d’interroger la base de données :

Magnifique ce site

Des propositions de recherches sont listées sous la barre de recherche, et seulement 2 d’entre elles ne retournent un résultat valide.

En regardant la requête dans l’onglet réseau, on se rend compte que la requête retourne une liste de plusieurs résultats potentiels, eux-mêmes formés d’un champ title et text :

Résultat de la recherche dans les outils développeur

La différence avec le challenge précédent, c’est que cette fois-ci, il ne suffit pas d’obtenir un résultat pour accéder à une page secrète. D’ailleurs, on ne même pas où se trouve le flag. Peut être est-ce une entrée de la table qui est interogée dans la barre de recherche, ou bien est-ce même une autre table.
Dans tous les cas, l’objectif cette fois va être de récupérer la structure de la base de données pour récupérer des informations.

Identification

Comme pour le challenge précédent, la première étape est de tenter une injection SQL pour voir si c’est possible. En rentrant ' a dans la barre de recherche, on obtient une erreur SQL :

Erreur SQL

Maintenant, il nous faut identifier le logiciel de base de données utilisé, pour pouvoir utiliser la bonne syntaxe. Pour cela, il existe différentes méthodes consistant à exécuter des fonctions ou commandes spécifiques à chaque logiciel, et à observer si elles fonctionnent ou non pour en déduire le logiciel utilisé.

Logiciel (Engine)Recherche (injection)Erreur
MySQL' or connection_id()=connection_id(); --General error: 1 no such function: connection_id
Oracle' or RAWTOHEX('AB')=RAWTOHEX('AB'); --General error: 1 no such function: RAWTOHEX
PostgreSQL' or current_database()=current_database(); --General error: 1 no such function: current_database
SQLite' or sqlite_version()=sqlite_version(); --Résultat valide
MSAccess' or val(cvar(1))=1; --General error: 1 no such function: cvar

Grâce à ces tests, on peut voir que la fonction sqlite_version() est reconnue, et donc que le logiciel de base de données utilisé est SQLite !

Injection

Cette fois-ci, nous allons utiliser une injection SQL un peu plus avancée que la précédente, qui va nous permettre de faire une union entre notre requête de recherche et une requête qui va nous permettre de récupérer des informations sur la base de données.

Seulement, pour qu’une union fonctionne, il faut que les deux requêtes retournent le même nombre de champs. Sans ça, les requête ne peuvent pas être combinées, résultat en une erreur.

On a vu plus haut que la requête de recherche ne retourne que 2 champs : title et text. Il va donc falloir faire en sorte que notre requête d’union retourne également 2 champs, même si ces champs ne sont pas forcément utilisés.

Commençons par récupérer les différentes tables disponibles. Avec SQLite, c’est possible grace à “super table” sqlite_schema :

aaa' UNION SELECT sql, 'x' FROM sqlite_schema; --

Comme tout à l’heure, décomposons notre injection :

  • aaa : le terme de recherche, qui n’a pas d’importance, et qui peut même être laissé vide
  • ' : on ferme la chaîne de caractères du terme de recherche
  • UNION SELECT : on fait une union entre la requête de recherche et notre requête personnalisée
  • sql, 'x' FROM sqlite_schema : on sélectionne le champ sql de la table sqlite_schema, qui contient les requêtes de création des différentes tables, et un champ fictif 'x' vide pour que notre requête retourne 2 champs.
  • ; -- : on termine la requête et on commente le reste pour éviter les erreurs

Pour votre plaisir personnel, la requête SQL générée par cette injection ressemble à ça :

SELECT name, description FROM data WHERE name LIKE '%$search';
↓ │ └──────────────────────────────────────────┐
SELECT name, description FROM data WHERE name LIKE '%aaa' UNION SELECT sql, 'x' FROM sqlite_schema; --%';

En exécutant cette injection, on obtient le résultat suivant :

Structure de la base de données

On voit bien la table SONAR_DATA(name TEXT, description TEXT) qui est interrogée pour les recherche, ainsi qu’une deuxième table SECRET_VAULT(flag_key TEXT, flag_val TEXT) qui semble intéressante !

Il ne reste alors plus qu’à faire la requête qui va intéroger cette table pour récupérer le flag :

aaa' UNION SELECT flag_key, flag_val FROM secret_vault; --

Tada !

Flag