Une injection SQL consiste à entrer des caractères précis dans une variable qui sera utilisée dans une requête SQL. Ces caractères feront en sorte que la requête originale sera déviée de son but afin d'ouvrir des portes aux utilisateurs malveillants. Ils pourraient, par exemple, s'authentifier sans connaître le mot de passe, créer un nouvel usager administrateur dont ils connaîtront le mot de passe, détruire une table, bousiller les données, etc.
Mais où ces injections peuvent-elles être entrées ? À n'importe quel endroit où les données peuvent être modifiées par un usager : dans une case de saisie, dans un URL, dans un cookie, etc.
Prenons l'exemple suivant : la page Web effectue une requête SQL pour retrouver les données d'une nouvelle dans la BD à l'aide de l'identifiant reçu en paramètre dans l'URL.
Attention : vous devez être conscients que les étapes réalisées par les utilisateurs malveillants pour effectuer une injection SQL seront différentes selon le type de requête. Par exemple, si la requête permet de vérifier l'authentification d'un usager, on fera face à une technique différente de celle présentée ici.
Le but de ces explications est de vous aider à comprendre les dangers potentiels et les précautions minimales pour protéger votre site.
Dans cet exemple, nous analyserons différents scénarios :
$id = $_GET['id'];
$requete = "SELECT nouvelle_titre, nouvelle_texte, nouvelle_date FROM nouvelle WHERE nouvelle_id=$id";
$resultat = $mysqli->query($requete);
if ($mysqli->affected_rows != 0) {
$enreg = $resultat->fetch_row();
echo "<h2>$enreg[0]</h2>$enreg[2] - $enreg[1]";
}
else {
echo "Il est impossible de retrouver les données de la nouvelle.";
}
$id = $_GET['id'];
$requete = "SELECT nouvelle_titre, nouvelle_texte, nouvelle_date FROM nouvelle WHERE nouvelle_id=$id";
$resultat = $mysqli->query($requete);
if ($resultat) {
if ($mysqli->affected_rows != 0) {
$enreg = $resultat->fetch_row();
echo "<h2>$enreg[0]</h2>$enreg[2] - $enreg[1]";
}
else {
echo "Il est impossible de retrouver les données de la nouvelle.";
}
}
else {
echo "<p>Nous sommes désolés, les données ne peuvent pas être affichées.</p><p>Cause de l'erreur : $mysqli->error</p>";
}
$id = $_GET['id'];
$requete = "SELECT nouvelle_titre, nouvelle_texte, nouvelle_date FROM nouvelle WHERE nouvelle_id=$id";
$resultat = $mysqli->query($requete);
if ($resultat) {
if ($mysqli->affected_rows != 0) {
$enreg = $resultat->fetch_row();
echo "<h2>$enreg[0]</h2>$enreg[2] - $enreg[1]";
}
else {
echo "Il est impossible de retrouver les données de la nouvelle.";
}
}
else {
echo "Nous sommes désolés, les données ne peuvent pas être affichées.";
}
$id = $_GET['id'];
$requete = "SELECT nouvelle_titre, nouvelle_texte, nouvelle_date FROM nouvelle WHERE nouvelle_id='$id'";
$resultat = $mysqli->query($requete);
if ($resultat) {
if ($mysqli->affected_rows != 0) {
$enreg = $resultat->fetch_row();
echo "<h2>$enreg[0]</h2>$enreg[2] - $enreg[1]";
}
else {
echo "Il est impossible de retrouver les données de la nouvelle.";
}
}
else {
echo "Nous sommes désolés, les données ne peuvent pas être affichées.";
}
$id = (int)$_GET['id'];
$requete = "SELECT nouvelle_titre, nouvelle_texte, nouvelle_date FROM nouvelle WHERE nouvelle_id=$id";
$resultat = $mysqli->query($requete);
if ($resultat) {
if ($mysqli->affected_rows != 0) {
$enreg = $resultat->fetch_row();
echo "<h2>$enreg[0]</h2>$enreg[2] - $enreg[1]";
}
else {
echo "Il est impossible de retrouver les données de la nouvelle.";
}
}
else {
echo "Nous sommes désolés, les données ne peuvent pas être affichées.";
}
$id = addslashes($_GET['id']);
$requete = "SELECT nouvelle_titre, nouvelle_texte, nouvelle_date FROM nouvelle WHERE nouvelle_id='$id'";
$resultat = $mysqli->query($requete);
if ($resultat) {
if ($mysqli->affected_rows != 0) {
$enreg = $resultat->fetch_row();
echo "<h2>$enreg[0]</h2>$enreg[2] - $enreg[1]";
}
else {
echo "Il est impossible de retrouver les données de la nouvelle.";
}
}
else {
echo "Nous sommes désolés, les données ne peuvent pas être affichées.";
}
$id = $_GET['id'];
$requete = "SELECT nouvelle_titre, nouvelle_texte, nouvelle_date FROM nouvelle WHERE nouvelle_id=$id";
$resultat = $mysqli->query($requete);
if ($resultat) {
if ($mysqli->affected_rows != 0) {
while ($enreg = $resultat->fetch_row()) {
echo "<h2>$enreg[0]</h2>$enreg[2] - $enreg[1]";
}
}
else {
echo "Il est impossible de retrouver les données de la nouvelle.";
}
}
else {
echo "Nous sommes désolés, les données ne peuvent pas être affichées.";
}
$id = $_GET['id'];
$requete = "SELECT nouvelle_titre, nouvelle_texte, nouvelle_date FROM nouvelle WHERE nouvelle_id=$id;";
$requete .= "INSERT INTO historique(histo_description) VALUES ('...')";
if ($mysqli->multi_query($requete)) { // si la première requête a fonctionné
if ($resultat = $mysqli->store_result()) {
if ($mysqli->affected_rows != 0) {
$enreg = $resultat->fetch_row();
echo "<h2>$enreg[0]</h2>$enreg[2] - $enreg[1]";
}
else {
echo "Il est impossible de retrouver les données de la nouvelle.";
}
}
else {
echo "Nous sommes désolés, les données ne peuvent pas être affichées.";
}
}
else {
echo "Nous sommes désolés, les données ne peuvent pas être affichées.";
}
La première chose que l'utilisateur malveillant fera consiste à vérifier si la requête est protégée ou non. Pour y arriver, il pourrait simplement ajouter un apostrophe à la fin du paramètre.
Ex :
http://mondomaine.com/nouvelle.php?id=1'
Voici les résultats obtenus par les différents scénarios :
Fatal error: Call to a member function fetch_row() on a non-object in C:Program Files (x86)EasyPHP-DevServer-13.1VC11datalocalwebmonsiteweb ouvelle.php on line 27
Nous sommes désolés, les données ne peuvent pas être affichées.
Cause de l'erreur : You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''' at line 1
Nous sommes désolés, les données ne peuvent pas être affichées.
Nous sommes désolés, les données ne peuvent pas être affichées.
Ici encore, les données s'affichent normalement. En effet, la fonction addslashes() ajoute un caractère d'échappement (un « slash » en anglais) devant différents caractères pouvant être interprétés, dont l'apostrophe ( ' ), le guillemet ( " ), la barre oblique inverse, ou backslash ( ) et le caractère NULL.
Attention : l'utilisation de addslashes() n'aura aucun effet si la variable n'est pas entourée d'apostrophes dans la requête.
Donc, grâce à addslashes(), l'apostrophe ne peut pas être utilisé pour terminer une chaîne de caractère puis ajouter une autre instruction SQL malveillante. L'utilisateur malveillant a, ici aussi, compris que ses chances sont amoindries...
Il est à noter qu'on aurait pu utiliser mysqli_real_escape_string() au lieu de addslashes() pour obtenir des résultats similaires.
Nous sommes désolés, les données ne peuvent pas être affichées.
Nous sommes désolés, les données ne peuvent pas être affichées.
Attention : la conversion en entier ou l'utilisation de addslashes() ou de mysqli_real_escape_string() n'est pas une protection à toute épreuve. Il s'agit d'une précaution minimale que vous devriez systématiquement utiliser lorsque vous effectuez une requête utilisant une donnée entrée par l'utilisateur.
Une des techniques utilisées pour effectuer une injection SQL consiste à ajouter une clause UNION à la fin de la requête originale. Pour que ça fonctionne, il doit connaître le nombre de champs que la requête originale va chercher. Pour y arriver, il ajoutera, à la fin du paramètre, la clause UNION SELECT NULL. Il augmentera graduellement le nombre d'occurence de NULL jusqu'à ce qu'il n'obtienne plus d'erreur.
Le « hacker » prendra soin de remplacer les espaces par le signe + dans l'URL. Il pourrait également utiliser les caractères %20 pour obtenir un résultat équivalent.
Débutons avec un seul NULL pour voir si la requête ne va chercher qu'un seul champ.
Ex :
http://mondomaine.com/nouvelle.php?id=1+UNION+SELECT+NULL
Voici les résultats obtenus par les différents scénarios :
Fatal error: Call to a member function fetch_row() on a non-object in C:Program Files (x86)EasyPHP-DevServer-13.1VC11datalocalwebmonsiteweb ouvelle.php on line 27
Nous sommes désolés, les données ne peuvent pas être affichées.
Cause de l'erreur : The used SELECT statements have a different number of columns
Nous sommes désolés, les données ne peuvent pas être affichées.
Ici encore, les données s'affichent normalement. Passons au test suivant.
Nous sommes désolés, les données ne peuvent pas être affichées.
Nous sommes désolés, les données ne peuvent pas être affichées.
L'utilisateur malveillant poursuivra ses tests en ajoutant plusieurs NULL dans la requête UNION et ce, jusqu'à ce qu'il obtienne un affichage sans erreur. Pour simplifier cette démonstration, nous passerons tout de suite à trois NULL puisque nous savons que la requête va chercher trois champs.
Ex :
http://mondomaine.com/nouvelle.php?id=1+UNION+SELECT+NULL,NULL,NULL
Avec cette requête, les données sont correctement affichées dans tous les scénarios. Si le « hacker » avait obtenu un message d'erreur avec un ou deux NULL mais n'en obtient pas avec trois NULL, il a la confirmation que la requête va effectivement chercher trois champs.
Une autre technique aurait pu permettre de connaître le nombre de champs : ajouter ORDER BY à la fin de l'URL, suivi d'un nombre correspondant à la position du champ dans la requête. La requête fonctionnera tant et aussi longtemps que ce nombre ne dépasse pas le nombre de champs dans la requête.
Notre utilisateur malveillant va maintenant tenter de faire afficher de l'information à l'aide de la clause UNION.
Pour faire afficher l'information désirée, l'utilisateur malveillant remplacera un ou plusieurs NULL dans la requête précédente par une des informations suivantes :
Souvent, les « hackers » aiment regrouper plusieurs informations dans une seule colonne à l'aide de la fonction CONCAT_WS(). Ils utiliseront comme séparateur un espace suivi de : suivi d'un autre espace comme suit : char(32, 58, 32).
Ex :
http://mondomaine.com/nouvelle.php?id=1+UNION+SELECT+CONCAT_WS(CHAR(32,58,32),user(),database(),version()),NULL,NULL
Voici les résultats obtenus par les différents scénarios :
monusager@localhost : mabasededonnees : 5.6.12-log
L'utilisateur malveillant soupçonne que les résultats de la requête n'étaient pas traités dans une boucle. Il tentera donc d'obtenir les informations piratées en utilisant la clause LIMIT.
Ex :
http://mondomaine.com/nouvelle.php?id=1+UNION+SELECT+CONCAT_WS(CHAR(32,58,32),user(),database(),version()),NULL,NULL+LIMIT+1,1
Voici les résultats obtenus par les différents scénarios :
monusager@localhost : mabasededonnees : 5.6.12-log
Les utilisateurs malveillants aimeraient tous pouvoir modifier le mot de passe d'un usager ayant les droits d'administration sur votre site Web. Pour cela, ils essayeront d'effectuer une injection SQL comme suit :
http://mondomaine.com/nouvelle.php?id=1;UPDATE+usagers+SET+password=MD5('monmotdepasse')
Voici les résultats obtenus par les différents scénarios :
Pour les scénarios 1 à 7, les données s'afficheront normalement et la requête malicieuse sera ignorée.
En effet, et c'est très heureux pour nous, la méthode $mysqli->query() ne permet pas d'exécuter deux requêtes de cette façon. En utilisant $mysqli->query() pour effectuer nos requêtes, nous sommes automatiquement protégés contre cette d'attaque en particulier (mais il existe d'autres attaques possibles...).
Pour le scénario 8, les données seront également affichées normalement. Cependant, la seconde requête sera elle aussi effectuée. OUCH !!!
Donc, vous devez être très prudents lors de l'utilisation de $mysqli->multi_query() car cette fonction vous expose aux requêtes malicieuses dans l'URL.
Dans la plupart des scénarios, il a été possible soit d'obtenir des informations sensibles, soit carrément d'injecter des données dangereuses dans la base de données. Les seuls scénarios où les données étaient toujours protégées contre les attaques menées sont les suivants :
Les scénarios suivants rendaient la tâche du « hacker » plus facile :
Dans tous les cas, il est important de se rappeler que cette démonstration n'est qu'un exemple d'attaque pouvant être perpétrée sur notre base de données. Il existe plusieurs autres types d'injections SQL et même les meilleurs scénarios de cette démonstration pourraient ouvrir des portes aux « hackers ».
Il existe une technique encore plus sécuritaire pour traiter les paramètres dans une requête SQL : utilisation de requêtes préparées (aussi appelées requêtes paramétrables).
Voici donc le meilleur scénario que vous pourriez utiliser pour vous prémunir contre les injections SQL :
// 1. Prépare la requête en plaçant un ? à la place de chaque paramètre (variable utilisée dans la requête).
$requete = "SELECT nouvelle_titre, nouvelle_texte, nouvelle_image FROM nouvelle WHERE nouvelle_id=?";
$stmt = $mysqli->prepare($requete);
if ($stmt) {
$id = $_GET['id'];
// 2. Indique que le paramètre est de type entier (i) et lui assigne la variable $id.
$stmt ->bind_param("i", $id);
// 3. Exécute la requête.
$stmt ->execute();
// Sans cette ligne, il ne sera pas possible de connaître le nombre de lignes retournées par un SELECT.
$stmt->store_result();
if ($mysqli->num_rows > 0) {
// 4. Fait le lien entre la position des champs et les variables qui seront initialisées lors du fetch.
$stmt ->bind_result($titre, $texte, $image);
// 5. Retrouve les données de la première ligne de résultats.
$stmt ->fetch();
echo "<h2>$titre</h2>";
echo "<img class='imagenouvelle' src='medias/fr/nouvelles/$image' alt='$titre' />$texte";
}
else {
echo "<div class='messageerreur'>Il est impossible de retrouver les données de la nouvelle.</div>";
}
// 6. Libère la mémoire.
$stmt ->close();
}
else {
// arrivera ici s'il y a une erreur dans la requête (ex : mauvais nom de champ).
echo "<div class='messageerreur'>Nous sommes désolés, la nouvelle ne peut pas être affichée.</p>";
// on affichera le message d'erreur MySQL seulement si on est en mode débogage donc ne sera jamais fait en ligne.
echo_debug($mysqli->error);
}
L'utilisation de requêtes préparées est souhaitable dans tous les langages de programmation qui le permettent!
« Les requêtes préparées ». PHP. http://www.php.net/manual/fr/mysqli.quickstart.prepared-statements.php
« What is SQL Injection? ». Infosec institute. http://resources.infosecinstitute.com/dumping-a-database-using-sql-injection/
« URL Based SQL Injection ». Ethical Hacking Tutorials. http://kyrionhackingtutorials.com/2012/01/url-based-sql-injection/
« Blind SQL Injection ». OWASP. https://www.owasp.org/index.php/Blind_SQL_Injection
« Roberto Salgado's Knowledge Base ». Websec. http://websec.ca/kb/sql_injection
« MySQL SQL Injection Cheat Sheet ». pentestmonkey. http://pentestmonkey.net/cheat-sheet/sql-injection/mysql-sql-injection-cheat-sheet
« SQL Injection cheat sheet Esp: for filter evasion ». ha.ckers. http://ha.ckers.org/sqlinjection/
▼Publicité