Création de site web dynamique avec php
...
2 Les failles de PHP :
2.1 Vulnerabilite escape shell :
Le language PHP possède une fonction 'system()' permettant d'utiliser directement des commandes systemes. Ainsi il est évident que cette fonction peut s'avérer dangereuse puisque si l'attaquant réussi à détourner un script utilisant cette fonction, il gagne un acces partiel, voire même complet (euh ca existe encore des httpd avec les droits root :p). Voyons tout d'abord un schéma classique. Le webmaster désire afficher l'uptime de son server, dans le cadre d'une page de statistique. Il pourra alors porcéder ainsi :
ou encore de parser certains caracteres comme les slash, pour éviter le changement de répertoire :
$page = ereg_replace("/","subk"); // Modification de '/' par 'subk' $page = ereg_replace("%2f","subk); // Idem mais avec l'equivalent unicode de / (%2f) Une autre solution serait d'intervenir directement au niveau du système, en changeant les authorisation des utilisateurs (chown), pour éviter que l'user 'httpd' ne puisse accéder aux fichier en dehors de sa racine (htdocs). Pour plus d'information man chown :).
2.3 Fonction mail() :
La fonction mail() permet d'envoyer des emails par l'intermédiaire d'un script PHP. Il n'existe pas vraiment de 'failles' utilisables avec cette fonction, cependant il est possible de l'utiliser dans le but de spamer/mailbomber des emails.
Il est évident que vous n'utiliserez pas cette fonction à cet usage, cependant si vous hébergez des sites web, il est possible que vos utilisateurs soient tenté de détournerl'utilisation normale de cette fonction en pensant que c'est votre IP qui apparaitra dans les headers des mails. En effet, un mailbomber est un script très facile à mettre en place. Exemple :
Pour eviter que l'un de vos utilisateur n'effectue des spam/mailbomb, le plus simple et le plus efficace est de désactiver cette fonction. Cependant à la manière de certains hébergeurs, vous pouvez aussi recoder une fonction mail personnalisée, que vous nommerez par exemple email().
2.4 Les fichiers de logs de Apache :
Comme nous l'avons vu plus haut la fonction include() permet d'inclure un fichier dans un script PHP. Cependant, il existe une autre méthode pour exploiter cette fonction. Cette méthode permet non pas d'afficher le contenu de certains fichiers sensibles, mais d'obtenir un accès partiel voire complet au système (comme toujours, cela dépend des droits du daemon http). Reprenons ainsi le meme exemple que précédemment :
en URL-Encode, ce qui donne %3c%3f%20system%28%22ls%20%2dal%22%29%3b%20%3f%3e. Etudions maintenant le résultat de nos requetes URL-Encodées :
…
# Nous consultons le fichier subkulture.org-error_log.
RESULTATS :
[Tue Jul 24 22:12:30 2001] [error] [client xxx.xxx.xxx.xxx] File does not exist: /home/www/subkulture/total 408
drwxr-xr-x 7 mariston mariston 4096 Mar 19 19:18 .
drwxr-xr-x 30 mariston mariston 4096 Jul 20 15:34 ..
-rw-r--r-x 1 mariston mariston 20992 Nov 15 2000 Donnelly_Tragedy.doc
drwxr-xr-x 2 mariston mariston 4096 Nov 15 2000 MRtoilet
-rw-r--r-x 1 mariston mariston 4109 Nov 15 2000 MRtoilet_article
-rw-r--r-x 1 mariston mariston 2123 Nov 15 2000 MRtoilet_review.htm
-rw-r--r-x 1 mariston mariston 403 Aug 23 2000 archive_list_ls
drwxr-xr-x 2 mariston mariston 4096 Aug 23 2000 barpics
-rw-r--r-x 1 mariston mariston 9220 Sep 14 2000 bars.html
-rw-r--r-x 1 mariston mariston 20115 Nov 15 2000 bathroom_wall.jpg
-rw-r--r-x 1 mariston mariston 10635 Oct 10 2000 bathrooms
-rw-r--r-x 1 mariston mariston 4415 Sep 6 2000 bookstore.html
[ ... ]
Comme vous pouvez le constater, si nous insérons du code PHP dans les fichiers error-logs, puis que nous consultons ces fichier par l'intermédiaire de Apache, le code qui à été déposé va être interpreté par PHP. Ainsi, l'attaquant aura plusieures possibilitées : envoyer une requete avec la fonction system() pour obtenir un 'pseudo-shell', écrire uen backdoor PHP dans un fichier (voir plus loin), modifier des fichiers du htdocs, ... Bref il s'agit la d'une faille très importante, surtout si votre Apache tourne en root (ahem !). Pour résoudre ce probleme d'execution de code, il vous suffit de procéder comme avec la fonction include(), à savoir changer les permissions pour le repertoire, parser les slash. Pour plus de sureté, vous pouvez aussi installer apache dans un autre path que celui habituel (/usr/local/bin).
2.5 Script d'upload :
Les scripts d'upload de fichiers sont vraiment très pratique, puisqu'il permette d'effectuer une mise à jour très rapide, et d'autant plus rapide si les données sont gérés avec une base de donnée comme par exemple MySQL. Ainsi, il est normal de constater que beaucoup de site aient recours à de tels scripts. Cependant, il existe plusieures vulnérabilitées liées à ce type de script.
Pour mieux comprendre comment fonctionne un script d'upload en PHP, voici un petit extrait de code (extrait de l'advisory de zorgon à ce sujet) :
$uploaddir = "./uploadfiles";
if(ereg("^\.", "$filename_name") || ereg("[ %/,;:+~#````'$%&\\()?!^|\]\[]", $filename_name))
{
...
}
elseif(file_exists("$uploaddir/$filename_name"))
{
...
}
elseif($filename_size <= $max_uploadsize)
{ copy($filename, "$uploaddir/$filename_name");
...
}
La variable $uploaddir contient le répertoire où seront stocké les fichiers uploadés. La premiere condition vérifie les caracteres contenu dans le nom du fichier à uploader, puis execute une instruction (non présente ici) si ils sont présent, à savoir un message d'erreur affichant que ces caracteres ne sont pas authorisés. La seconde condition vérifie que le fichier que nous voulons uploader n'existe pas déja, dans le but d'éviter d'écraser ce fichier. Si il existe déja, le script affichera un message d'erreur, puis un break. Enfin la derniere condiftion vérifie que la taille du fichier que nous voulons uploader est inférieure à la taille maximale authorisée pour un fichier. Si cette condition est remplie, le script copiera le fichier file dans /uploadfiles/file.
Comme vous pouvez le constater, le script ne présente aucun controle de vérification de l'extension. Ainsi il est possible aussi bien d'uploader des .png que des .txt. Cependant la faille existe lorsque nous uploadons un fichier avec l'extension .php, .php3 ou encore .cgi (tout dépend de la configuration du serveur). En uploadant un fichier avec une des ces extensions, nous pourrons alors excuter le code contenu dans ces fichiers, par le biais de notre navigateur. A partir de ce point la, l'attaquant dispose de plusieures méthodes d'attaques, toutes basées sur le même principe.
Tout d'abord il peut créer un petit script utilisant la fonction copy(). Ce script aura pour effet de copier le contenu d'un fichier PHP dans un fichier texte. Ainsi il sera possible d'obtenir le code source du fichier PHP copié. L'intérêt de cette méthode est qu'aujourd'hui la plupart des sites utilisant PHP ont recours à des bases de données, protégées par un système de login/password. Ce login/password doit être stipulé dans le code source pour que la connexion à la base de donnée puisse s'effectuer. Ainsi en récupérant le code source d'un script PHP, l'attaquant risque aussi de récupérer un ou des login/password de MySQL par exemple. Voici un petit exemple de script PHP à uploader :
<?
copy("page.php","page_source.txt");
? >
Il suffit maintenant à l'attaquant de consulter le fichier page_source.txt pour récupérer vos mots de passe d'administration de MySQL. La deuxieme méthode d'attaque est basée sur le même principe, mais est légerement plus complexe. Il s'agit là de gagner un acces au systeme lui même en créant une backdoor PHP. Il ne faut pas oublier que PHP offre des fonctions gérant les connexions sockets, et que donc toute sortes de scripts génant pour l'administrateur peuvent exister : bot IRC, backdoor, scanner de port, ....
Pour résoudre ce problème il existe, une fois encore, plusieures solutions. Tout d'abord, il est conseillé soit de rajouter une extension, soit de parser les extensions. Voici donc deux exemple :
$filename = $filename.".subk"; # Rajoute l'extension .subk
if( ereg("php$", $filename) || ereg("php3$", $filename) || ereg("cgi$", $filename) ) {
# Verifie l'extention de $filename
echo "Type de fichiers interdits pour raison de sécurité !";
break();
}
Une autre méthode serait de faire en sorte que l'attaquant n'aie pas accès au répertoire d'upload (/uploadfiles dans notre exemple). Pour ce faire utiliser les .htaccess de Apache par exemple, ou encore placez ce repertoire en dehors de la racine du httpd (htdocs).
3 PHP & MySQL :
3.1 Requetes MySQL multiples :
MySQL est un système de base de données facile à utiliser, et de se fait la plupart des scripts PHP utilise MySQL pour gérer leurs données. La faille qui suit existe non seulement pour PHP, mais pour beaucoup d'autre languages tels que l'ASP, le CFM ou encore le JSP. Voyons tout d'abord comment fonctionne un script avec MySQL :
[ ... ]
$table ="newsletter":
$query = "SELECT * FROM $table";
$result = mysql_query($query);
[ ... ]
Le systeme est simple : au début de chaque script, il faut établir une connexion à la base de données MySQL. Pour cela nous utilisons la fonctions mysql_connect(), avec comme argument un login et un password. Ensuite il faut selectionner une base de donnée avec la fonction mysql_select_bdd(). Une fois ces étapes remplies, il faudra sélectionner le nom d'une table. La table est un tableau contenant différente données. Dans notre exemple il s'agit de newsletter, qui contient deux champ : nom de l'utilisateur et email. De meme pour insérer quelqu'un dans cette meme table nous pourrions effectuer un code comme ceci, ou $nom est le nom d'utilisateur et $email son email.
[ ... ] $table ="newsletter":
$query = "INSERT INTO $table ('$nom', '$email')";
$result = mysql_query($query);
[ ... ]
Rien de bien compliqué n'est ce pas ? De plus il faut savoir que MySQL est aussi capable d'effectuer deux requetes en meme temps. Ainsi pour insérer deux lignes dans une table MySQL, nous pourrions procéder ainsi :
$query = "INSERT INTO newsletter ('subkulture', '[email protected]'); INSERT INTO newsletter ('medgi', '[email protected]')";<
$result = mysql_query($query);
C'est la que le premier probleme se pose : imaginez en effet que nous ayons un formulaire d'inscription qui a comme input nom et email. Rentrons alors des valeures comme ceci :
[ NOM ] medgi
[ MAIL ] '[email protected]'); INSERT INTO newsletter ('subkulture', '[email protected]')
Le script recevra donc une requete double qui sera insérée dans la table 'newsletter'. Dans cette exemple, l'attaque a très peu d'intérêt, si ce n'est surcharger la table MySQL. Bien sur nous pourrions effectuer d'autres requete comme par exemple "SELECT * FROM login" cependant cela ) la aussi peu d'intéret et ce pour deux raison : tout d'abord, il n'est pas sur que la table login existe, et ensuite meme si la requete MySQL récuperera tout le contenu de la table login (login/password/telephone/email/...), cela ne sera pas forcément disponible pour nous.
En effet, l'affichage de donnée MySQL se fait en général ainsi : nous récupérons les champs qui nous intéresse par leur nom ($nom, $email, $password, ...) puis nous les affichons. Cependant si par exemple la page que nous désirons attaquer est une page d'enregistrement, aucune fonction ne sera préseente pour l'affichage de données. Ainsi la deuxieme requete que l'attaquant effectuera, n'affichera en général pas les données que celui ci attend. Le probleme vient du fait qu'avec MySQL, il est aussi possible d'effectuer des requetes effacant une ligne, voir toute la table. Ainsi dans notre exemple précédent si nous rentrions une requete comme celle la :
[ NOM ] medgi
[ MAIL ] '[email protected]'); DROP TABLE newsletter
Toute la table newsletter serait effacée, et c'est la que cette méthode devient dangereuse. Voyons donc comment sécuriser vos scripts. Le probleme vient de la requete passée par un champ de forumlaire, il suffit donc de parser la variable de ce formulaire, pour eviter toute mauvaise requete. Pour cela il suffit d'utiliser la fonction addslashes(), qui rajoutera un backslash '\' devant les caracteres suivant : quote ("), apostrophe (') et backslash(\). Voici donc une partie de code pour parser votre variable : $nom = addslashes($nom);
$email = addslashes($email);
# Definition de la requete :
$query = "INSERT INTO $table ('$nom', '$email');
Vous pouvez aussi activer l'option MAGIC_QUOTES dans votre php.ini (MAGIC_QUOTES = on).
3.2 Fake posts :
La plupart des moteurs de news actuels laissent la possibilté à leur utilisateurs de poster des commentaires. Dans certains scripts, l'utiisateur doit s'authentifier, et donc laisser ses coordonées, ce qui limite le nombre de personne indésirable, voir mal intentionnée. Cependant il existe des sites qui permettent de poster des commentaires s'en s'identifier. C'est ce cas précis qui intéressera un attaquant. Ainsi voici un extrait de code qui s'averera vulnérable :
if ( $action=="ajout" ) { $date=date("Y/m/d H:i"); $ajout_sql = mysql_query("insert into $table (nom, auteur, email, texte, date) values ('$nom', '$auteur', '$email', '$texte', '$date')",$connexion); }
Comme vous pouvez le constater, le formulaire transmet à la page de script une variable 'hidden'. Si son contenu est ajout, le scripts executera une requete d'ajout dans la base de donnée. Le nom de la table est variable, et correspond au numéro de la news : si il s'agit d'un commentaire pour la news 40, le nom de la table sera par exemple news_40. Dans cet exemple je ne comprend pas vraiment comment le devellopeur a fait pour faire un script aussi insensé, il n'empeche que celui ci existe :) !
Voici donc la pseudo-faille, qui va vous paraitre inutile certes, mais qui peut s'avérer agacante pour le webmaster. L'attaquant va poster un commentaire "fantome". Ainsi imaginons que la requete pour ajouter une news soit : [email protected]&;texte=subkulture%200wnz%20y0u.">http://subkulture/addcommentaire?newsID=40&nom=medgi& [email protected]&;texte=subkulture%200wnz%20y0u. Si la news numéro 40 existe, le commentaire va être posté. Cependant si la news numéro 40 n'existe pas, la news va quand meme être postée (erf) :) !
Comme vous pouvez le constater, il est ainsi possible de poster des commentaires sur des news qui n'existe pas. Peu d'intérêt certes, cependant le webmaster soucieux de son site doit vite être agacé de ces posts de gamin. Ainsi pour sécuriser ce petit bug, rien de plus simple :
$query = "SELECT * FROM table_news";
$requete = mysql_query($query);
$nb = mysql_numrows($requete);
if ( ($action=="ajout") && ($newsID < $nb) )
{
$date=date("Y/m/d H:i");
$ajout_sql = mysql_query(...);
}