Exploitation des injections SQL au sein de la clause ORDER BY
15 aout 2024
Les injections SQL peuvent surgir de manière inattendue, même dans les clauses ORDER BY. Comprenez les subtilités de ces attaques et maîtriser les techniques de défense pour protéger vos applications.
De nombreuses API permettant de récupérer des informations offrent à l'utilisateur la possibilité de trier les résultats selon ses préférences, grâce à la clause ORDER BY, et d'utiliser la pagination avec les clauses LIMIT et OFFSET. Bien que moins facilement exploités, les paramètres de la clause ORDER BY peuvent être tout aussi vulnérables aux injections SQL qu'une valeur passée plus classiquement au sein d'une clause WHERE.
Le service, qui sera utilisé dans les différents exemples de cet article, comprend une base de données, nommée "sql_injection_in_pagination_clauses_database" ainsi que deux tables : la table "users", qui répertorie les utilisateurs de la plateforme, et la table "secrets", qui stocke les secrets de certains de ces utilisateurs.
MariaDB [sql_injection_in_pagination_clauses_database]> show tables;
+--------------------------------------------------------+
| Tables_in_sql_injection_in_pagination_clauses_database |
+--------------------------------------------------------+
| secrets |
| users |
+--------------------------------------------------------+Le contenu de la table "users" est le suivant :
MariaDB [sql_injection_in_pagination_clauses_database]> select * from users;
+----+-------------+-----------+----------+---------------+
| id | username | firstname | lastname | email |
+----+-------------+-----------+----------+---------------+
| 1 | admin | John | Doe | admin@poc.com |
| 2 | reck | Jean | Lafond | reck@poc.com |
| 3 | tim | Timothée | Durand | tim@poc.com |
| 4 | lola | Lola | Bouetté | lola@poc.com |
| 5 | trin | Elise | Judion | trin@poc.com |
+----+-------------+-----------+----------+---------------+Tandis que le table "secrets" contient les informations suivantes :
Afin de permettre aux utilisateurs d'interroger la base d'utilisateurs, le service met à disposition une API de recherche :
Un exemple d'appel peut être :
L'API retournant alors la réponse au format JSON :
Exemples d'implémentations
PHP, PDO et MySQL
Le développeur, sensibilisé aux risques d'injections SQL, a implémenté la solution suivante en PHP et l'interface PDO avec une requête préparée utilisant des placeholders :
Pour plus de convivialité, la recherche est effectuée dans les colonnes "username", "firstname" et "lastname" et l'utilisation de l'opérateur LIKE permet une recherche partielle. Malheureusement, lors de ses tests, le développeur identifie un dysfonctionnement des paramètres "sort" et "direction", qui ne semblent pas être pris en compte, mais la requête retourne tout de même un résultat :
Afin que cela fonctionne, il n'est pas rare de voir le développeur modifier sa requête de la manière suivante, rendant l'application sensible à une injection SQL par la même occasion :
PHP, PDO et PostgreSQL
Avec PostgreSQL, l'utilisation d'un paramètre lié pour le nom de la colonne dans la clause ORDER BY n'est également pas pris en compte et ne provoque pas d'erreur. Cependant, son utilisation pour la direction du tri lève une alerte :
Similairement à l'exemple précédent, le développeur modifie généralement sa requête de la façon la plus simple :
Node.js et MySQL
A titre d'exemple, et afin de s'assurer que cela n'est pas lié à PHP, voici le même exemple en utilisant Node.js et MySQL :
La requête est exécutée sans erreur mais le tri n'est également pas respecté :
Pour les mêmes raisons que précédemment, la requête sera naturellement adaptée de la façon suivante afin de la faire fonctionner :
Exploitation des injections SQL
Dans le cas d'une injection SQL sur le nom de la colonne ou la direction de tri dans la clause ORDER BY, l'utilisation des injections de type UNION n'est pas possible. Pour contourner cette limitation, et en fonction de la configuration de la plateforme vulnérable, il est toutefois possible d'utiliser des injections de type error-based, boolean-based, time-based, ou encore des stack queries.
Error-based
Une exploitation de type error-based nécessite que le serveur renvoie des messages d'erreur techniques détaillés :
Si les messages d'erreur sont masqués ou trop génériques, il ne sera pas possible d'exploiter cette technique.
MySQL
Exploitation du paramètre "sort"
Récupération de la version de MySQL :
Récupération de l'utilisateur MySQL :
Récupération du nom de la base de données :
Récupération du nombre de tables :
Récupération des noms des tables :
Récupération du nombre de colonnes de la table "secrets" :
Récupération des noms des colonnes :
Récupération du secret de l'administrateur :
Exploitation du paramètre "direction"
L'exploitation du paramètre "direction" est très similaire à celle du paramètre "sort". Pour cette raison, seule la récupération du secret de l'administrateur sera illustrée :
PostgreSQL
Exploitation du paramètre "sort"
Récupération de la version de PostgreSQL :
Récupération de l'utilisateur PostgreSQL :
Récupération du nom de la base de données :
Récupération du nombre de tables :
Récupération des noms des tables :
Récupération du nombre de colonnes de la table "secrets" :
Récupération des noms des colonnes :
Récupération du secret de l'administrateur :
Exploitation du paramètre "direction"
L'exploitation du paramètre "direction" est très similaire à celle du paramètre "sort". Pour cette raison, seule la récupération du secret de l'administrateur sera illustrée :
Boolean-based
Une exploitation de type boolean-based est une technique d'injection SQL qui nécessite la capacité de distinguer les cas où l'application répond par vrai ou faux en fonction des conditions logiques insérées dans les requêtes SQL.
L'exploitation de ce type est plus longue que celle basée sur les erreurs. C'est pourquoi seule une partie des requêtes sera présentées. De plus, les exemples utilisent l'opérateur = pour tester un caractère spécifique. Lors d'une attaque sans connaître la base de données, il est préférable d'utiliser les opérateurs > et < et d'effectuer une attaque par dichotomie pour gagner en rapidité.
MySQL
Exploitation du paramètre "sort"
La payload utilisée est celle-ci :
Lorsque la condition est vraie, le résultat sera trié par la colonne "lastname". Sinon, selon l'application, une erreur générique ou une page blanche sera renvoyée.
CASE : Permet d'exécuter une instruction conditionnelle.
WHEN : Condition de l'instruction conditionnelle.
ORD : Retourne le code ASCII du premier caractère d'une chaîne de caractères.
SUBSTRING : Extrait une sous-chaîne.
THEN : Lorsque le condition est vérifiée, le résultat est trié par la colonne "lastname".
ELSE : Lorsque la condition n'est pas vérifiée, l'utilisation de l'UNION retourne un résultat composé de deux valeurs, ce qui provoque une erreur "SQLSTATE[21000]: Cardinality violation: 1242 Subquery returns more than 1 row".
Récupération de la version de MySQL :
La version de la base de données est "10.11.6-MariaDB".
Une solution plus rapide permettant d'identifier le type de base de données peut être la suivante :
Récupération de l'utilisateur MySQL :
L'utilisateur MySQL est "sql_injection_in_pagination_clauses@localhost".
Récupération du nom de la base de données :
Le nom de la base de donnéest est "sql_injection_in_pagination_clauses_database".
Récupération du nombre de tables :
La base de données possède deux tables.
Récupération des noms des tables :
Le nom de la première table est "users".
Le nom de la seconde table est "secrets".
Récupération du nombre de colonnes de la table "secrets" :
La table "secrets" possède trois colonnes.
Récupération des noms des colonnes :
La première colonne se nomme "id".
La seconde colonne se nomme "username".
La troisième et dernière colonne se nomme "secret".
Récupération du secret de l'administrateur :
Le nom d'utilisateur est "admin".
Son secret est "Mon mot de passe est passw0rd"
Exploitation du paramètre "direction"
L'exploitation du paramètre "direction" est très similaire à celle du paramètre "sort". Pour cette raison, seule la récupération du secret de l'administrateur sera illustrée :
PostgreSQL
Exploitation du paramètre "sort"
La payload utilisée est celle-ci :
Lorsque la condition est vraie, le résultat sera trié par la colonne "lastname". Sinon, selon l'application, une erreur générique ou une page blanche sera renvoyée.
CASE : Permet d'exécuter une instruction conditionnelle.
WHEN : Condition de l'instruction conditionnelle.
ASCII : Retourne le code ASCII du premier caractère d'une chaîne de caractères.
SUBSTRING : Extrait une sous-chaîne.
THEN : Lorsque le condition est vérifiée, le résultat est trié par la colonne "lastname".
ELSE : Lorsque la condition n'est pas vérifiée, la division par zéro va provoquer une erreur "Error: SQLSTATE[22012]: Division by zero: 7 ERREUR: division par zéro".
Récupération de la version de PostgreSQL :
La version de la base de données est "PostgreSQL 15.7 (Debian 15.7-0+deb12u1)".
Une solution plus rapide permettant d'identifier le type de base de données peut être la suivante :
Récupération de l'utilisateur PostgreSQL :
L'utilisateur PostgreSQL est "sql_injection_in_pagination_clauses".
Récupération du nom de la base de données :
Le nom de la base de donnéest est "sql_injection_in_pagination_clauses_database".
Récupération du nombre de tables :
La base de données possède deux tables.
Récupération des noms des tables :
Le nom de la première table est "users".
Le nom de la seconde table est "secrets".
Récupération du nombre de colonnes de la table "secrets" :
La table "secrets" possède trois colonnes.
Récupération des noms des colonnes :
La première colonne se nomme "id".
La seconde colonne se nomme "username".
La troisième et dernière colonne se nomme "secret".
Récupération du secret de l'administrateur :
Le nom d'utilisateur est "admin".
Son secret est "Mon mot de passe est passw0rd"
Exploitation du paramètre "direction"
L'exploitation du paramètre "direction" est très similaire à celle du paramètre "sort". Pour cette raison, seule la récupération du secret de l'administrateur sera illustrée :
Time-based
Une injection SQL "time-based" est un type d'attaque SQL qui exploite le temps de réponse du serveur de base de données pour extraire des informations. Contrairement aux techniques précédentes, qui reposent sur des messages d'erreur ou des résultats vrai/faux, les injections SQL "time-based" exploitent les délais pour déduire si une requête est vraie ou fausse. Ce type d'exploitation étant assez long, seules quelques requêtes seront présentées ici.
MySQL
Exploitation du paramètre "sort"
La payload utilisée est celle-ci :
Si la condition est vraie, aucun délai n'est appliqué. Sinon, un délai de 5 secondes est ajouté.
IF : Exécute une expression conditionnelle et retourner des résultats différents en fonction de la condition évaluée.
ORD : Retourne le code ASCII du premier caractère d'une chaîne de caractères.
SUBSTRING : Extrait une sous-chaîne.
La console de développement du navigateur est suffisante pour visualiser le temps de réponse de la requête :

Récupération de la version de MySQL :
La version de la base de données est "10.11.6-MariaDB".
Une solution plus rapide permettant d'identifier le type de base de données peut être la suivante :
Récupération de l'utilisateur MySQL :
L'utilisateur MySQL est "sql_injection_in_pagination_clauses@localhost".
Récupération du nom de la base de données :
Le nom de la base de donnéest est "sql_injection_in_pagination_clauses_database".
Récupération du nombre de tables :
La base de données possède deux tables.
Récupération des noms des tables :
Le nom de la première table est "users".
Le nom de la seconde table est "secrets".
Récupération du nombre de colonnes de la table "secrets" :
La table "secrets" possède trois colonnes.
Récupération des noms des colonnes :
La première colonne se nomme "id".
La seconde colonne se nomme "username".
La troisième et dernière colonne se nomme "secret".
Récupération du secret de l'administrateur :
Le nom d'utilisateur est "admin".
Son secret est "Mon mot de passe est passw0rd"
Exploitation du paramètre "direction"
L'exploitation du paramètre "direction" est très similaire à celle du paramètre "sort". Pour cette raison, seule la récupération du secret de l'administrateur sera illustrée :
PostgreSQL
Exploitation du paramètre "sort"
La payload utilisée est celle-ci :
Si la condition est vraie, aucun délai n'est appliqué. Sinon, un délai de 5 secondes est ajouté.
CASE : Permet d'exécuter une instruction conditionnelle.
WHEN : Condition de l'instruction conditionnelle.
ASCII : Retourne le code ASCII du premier caractère d'une chaîne de caractères.
SUBSTRING : Extrait une sous-chaîne.
THEN : Lorsque le condition est vérifiée, un délai de 0 seconde est ajouté.
ELSE : Lorsque la condition n'est pas vérifiée, un délai de 5 secondes est ajouté.
La console de développement du navigateur est suffisante pour visualiser le temps de réponse de la requête :

Récupération de la version de PostgreSQL :
La version de la base de données est "PostgreSQL 15.7 (Debian 15.7-0+deb12u1)".
Une solution plus rapide permettant d'identifier le type de base de données peut être la suivante :
Récupération de l'utilisateur PostgreSQL :
L'utilisateur PostgreSQL est "sql_injection_in_pagination_clauses".
Récupération du nom de la base de données :
Le nom de la base de donnéest est "sql_injection_in_pagination_clauses_database".
Récupération du nombre de tables :
La base de données possède deux tables.
Récupération des noms des tables :
Le nom de la première table est "users".
Le nom de la seconde table est "secrets".
Récupération du nombre de colonnes de la table "secrets" :
La table "secrets" possède trois colonnes.
Récupération des noms des colonnes :
La première colonne se nomme "id".
La seconde colonne se nomme "username".
La troisième et dernière colonne se nomme "secret".
Récupération du secret de l'administrateur :
Le nom d'utilisateur est "admin".
Son secret est "Mon mot de passe est passw0rd"
Exploitation du paramètre "direction"
L'exploitation du paramètre "direction" est très similaire à celle du paramètre "sort". Pour cette raison, seule la récupération du secret de l'administrateur sera illustrée :
Stack queries
Les stack queries consistent à ajouter plusieurs requêtes SQL dans un seul appel à la base de données, en les séparant par un point-virgule ;. Cependant, leur utilisation peut être limitée ou non supportée en fonction de l'API SQL ou du moteur SQL utilisé.
Comment s'en protéger
La limitation vient du fait qu'il n'est pas possible de lier des paramètres utilisés comme identifiants, tels que les noms de tables ou de colonnes, y compris dans une clause ORDER BY. Cependant, plusieurs solutions permettent de contourner ce problème.
Validation par liste blanche (PHP + extension Mysqli/PDO)
L'utilisation d'une liste blanche permet de restreindre les options de l'utilisateur à certains termes spécifiquement approuvés par le développeur. Cette technique est adaptée au PHP utilisant les extensions mysqli ou PDO :
Echappement des identifiants SQL (NodeJS + mysql package)
Certaines extensions peuvent gérer correctement les identifiants SQL provenant de l'utilisateur. Par exemple, l'extension mysql de Node.js peut traiter les noms de tables ou de colonnes de manière sécurisée en utilisant une méthode d'échappement ou un placeholder spécifique. Toutefois, le sens du tri devrait être contrôlé en utilisant une liste blanche.
Méthode escapeId()
Dorénavant, lorsque l'utilisateur fournit une colonne invalide, l'erreur suivante est générée :
Placeholders d'identifiants
De la même manière, lorsque l'utilisateur fournit une colonne invalide, la même erreur que précédemment est générée.
Object-Relational Mapping (ORM)
Sous certaines conditions, les ORM peuvent offrir une protection contre les injections SQL. Quel est son comportement lorsque des données utilisateur sont utilisées dans la clause ORDER BY, même en utilisant les méthodes sécurisées ?
NodeJS + Sequelize
L'avantage ici est que l'ORM échappe le nom des colonnes de tri et valide également la direction (https://sequelize.org/docs/v6/core-concepts/model-querying-basics/#ordering) :
The column will be escaped correctly and the direction will be checked in a whitelist of valid directions (such as ASC, DESC, NULLS FIRST, etc).
L'erreur suivante est générée lorsque le nom de la colonne de tri est incorrect :
Il en est de même pour une direction invalide :
Conclusion
La clause ORDER BY est fréquemment utilisée et accessibles aux utilisateurs d'API, et, en raison de leurs limitations, les extensions natives semblent particulièrement vulnérables aux injections SQL dans ce contexte. De manière plus générale, ces limitations affectent les identifiants représentant les noms des tables et des colonnes utilisés au sein des requêtes. De plus, l'utilisation des paramètres liés pour ces identifiants peuve laisser penser aux développeurs que cela fonctionne correctement (absence d'erreur à l'exécution). Cela doit inciter à une vigilance accrue lorsqu'on permet à l'utilisateur de choisir les colonnes à sélectionner (SELECT), à ordonner (ORDER BY) ou à regrouper (GROUP BY).
Mis à jour