Dès qu'un site web est dynamique, des problèmes de sécurités inhérents au langage de programmation peuvent apparaître. PHP est sans doute le langage le plus proposé par les hébergeurs et le plus présent sur Internet. Cet article va aborder le problème de la sécurité posé par le langage PHP dans un site internet. Peut-on limiter les risques posés par d'éventuelles failles dans le code PHP ? Dans le cadre d'hébergements web mutualisés, quelles mesures un hébergeur peut-il prendre pour qu'un hébergement n'ait pas à pâtir du piètre développement d'un autre site ? Peut-il garantir un minimum de sécurité même en présence de code PHP hostile ? Certaines mesures pourront limiter les risques posés par des bugs de programmation dans le code d'un site web ou dans une application PHP. Elles peuvent être intéressantes à envisager dans le cadre de serveur dédié.
PHP permet notamment de manipuler des fichiers, d'exécuter
des commandes et d'ouvrir des connexions réseaux. Ces
propriétés le rendent potentiellement dangereux.
Cependant, il a été conçu comme une alternative
plus sûr aux scripts CGI et au langage Perl. Quand PHP est
utilisé comme module Apache, il s'exécute avec les
droits de l'utilisateur sous lequel le serveur web fonctionne,
généralement l'utilisateur apache. Si différents
utilisateurs ont des scripts PHP, tout les scripts se retrouvent avec
les mêmes droits! Le safe mode
tente de résoudre
les problèmes de sécurités des hébergements
mutualisés. Faute d'alternatives réalistes au niveau du
serveur web ou du système d'exploitation, le safe mode est la
mesure principale de sécurité utilisée
jusqu'ici.
Pour activer le safe mode
, il suffit de mettre
safe_mode = On
dans le fichier de configuration php.ini
et de relancer Apache.
Un système de wrapper
sur l'ouverture des
fichiers fopen
va renforcer les permissions Unix, il
vérifie si le propriétaire du script est aussi le
propriétaire du fichier et ce n'est qu'à cette
condition que le fichier pourra être accédé.
Les exécutions de commandes sont limitées à
celles se trouvant dans le répertoire safe_mode_exec_dir
.
Seules les variables d'environnement safe_mode_allowed_env_vars
peuvent être modifiés à l'exception des variables
commençant par les préfixes définis dans
safe_mode_protected_env_vars
. Par défaut lorsque
le safe mode
est activé, seules les variables
commençant par PHP_
sont modifiables à
l'aide de la fonction putenv()
. Cela évite les
manipulations des variables PATH, IFS, LD_LIBRARY_PATH,...
qui pourrait permettre l'execution d'autres programmes que ceux du répertoire
safe_mode_exec_dir
.
Certaines fonctions dangereuses comme dl
permettant
le chargement dynamique de librairie sont interdites et divers
contrôles sont ajoutés. Par exemple, la fonction chmod()
ne peut pas donner les permissions SUID ou SGID, ce qui aurait permis
à un utilisateur local de devenir l'utilisateur apache.
Un numéro de session va permettre d'identifier de manière
unique un utilisateur. Cette valeur est transmise par un cookie ou
via un paramètre dans l'URL. Véhiculé dans une
URL, elle peut être transmise involontairement dans un champs
Referer
HTTP. Pour limiter l'utilisation accidentelle de
cette valeur, php peut vérifier la présence d'une
chaîne défini par session.referer_check
dans le contenu du Referer
. Si cette chaîne de
texte est absente, le numéro de session n'est pas utilisé.
Évidemment, un pirate n'aura aucun mal à forger un
Referer
ou utiliser un cookie pour exploiter ce numéro
de session. Pour se protéger contre cela, il convient de
désactiver la gestion des sessions via les URLs:
session.use_trans_sid=0
au pris de diminuer la
compatibilité avec les clients ne gérant pas les
cookies.
En dehors du risque de diffusion du cookie, il y a aussi le risque
qu'un pirate fixe le numéro de session, C'est l'attaque par
session fixation
. Par exemple, le pirate vous invite à
suivre un lien vers un site sensible dont l'URL contient un numéro
de session. Si l'utilisateur s'identifie sur ce site, le pirate ayant
le numéro de session pourra effectuer les mêmes
opérations que l'utilisateur. Pour éviter cette
attaque, activer session.use_only_cookies=1
(PHP >=
4.3.0).
Si le site http://www.hebergeur.com/site1/
dépose
un cookie sur le poste de l'utilisateur, lorsque cet utilisateur va
accéder au site http://www.hebergeur.com/site2/
,
son navigateur va transmettre les cookies du site1! En effet, les
deux sites partagent le même domaine. Il faudra que la
configuration du serveur distingue le répertoire
session.cookie_path
pour chaque site hébergé.
Enfin, le répertoire où se trouve les sessions des
utilisateurs session.save_path
s'il est identique pour
tous ne doit pas pouvoir être listé via un script PHP.
On verra comment réaliser cela dans le paragraphe suivant. Si
chaque site a son propre répertoire pour les sessions, il ne
faut pas pour autant que ce répertoire se trouve accessible
depuis Internet. Cela peut sembler évident mais cette
situation a été observée chez certains hébergeurs.
Malgré la présence du safe_mode
, un
utilisateur peut se promener sur le système de fichier.
<?php function list_dir($dirname) { echo "list_dir($dirname)<BR>\n"; $handle=opendir($dirname); if($handle) { while ($file = readdir($handle)) { print $file."<BR>"; } closedir($handle); } } list_dir('/'); ?>
Le paramètre open_basedir
permet de spécifier
une racine en dessous de laquelle aucun fichier ne peut être
lu. Il convient de placer ce paramètre pour chaque virtualhost
ou site virtuel.
Exemple: <VirtualHost 1.2.3.4:80> ServerName www.toto.org ServerAdmin webmaster@toto.org DocumentRoot /home/toto/public_html/ User toto Group toto php_admin_flag engine on php_admin_value open_basedir "/home/toto/" </VirtualHost>
Le paramètre open_basedir
va empêcher un
accès direct aux fichiers de session sans pour autant en
empêcher le fonctionnement.
La commande copy
ne permet pas de déplacer le
fichier uploadé car celui-ci se trouve en dehors de la racine
open_basedir
et de toute façon le safe mode
refuserait de copier un fichier n'appartenant pas au propriétaire
du script PHP. Il faut utiliser l'instruction dédiée
move_uploaded_file
Formulaire HTML d'upload: <FORM ENCTYPE="multipart/form-data" ACTION="_URL_SCRIPT_PHP_" METHOD="POST"> <INPUT TYPE="hidden" name="MAX_FILE_SIZE" value="1000"> Envoyez ce fichier : <INPUT NAME="userfile" TYPE="file"> <INPUT TYPE="submit" VALUE="Send File"> </FORM> Code PHP: <?php $uploaddir = '/var/www/uploads/'; print "<pre>"; if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploaddir . $_FILES['userfile']['name'])) { print "Le fichier est valide, et a été téléchargé avec succès. Voici plus d'informations :\n"; print_r($_FILES); } else { echo "Attaque par upload potentielle. Voici plus d'informations :\n"; print_r($_FILES); } print "</pre>"; ?>
Attention à la présence de ".." dans le nom du fichier cependant...
Coté implémentation, la fonction interne de PHP
php_checkuid()
valide le propriétaire d'un fichier
tandis que la fonction php_check_open_basedir()
vérifie
si un fichier est bien dans l'un des répertoires de
open_basedir
. Malheureusement, toutes les fonctions php
n'y font pas appel.
Les fonctions suivantes:
ne vérifient pas la racine open_basedir
. Il
est donc possible de vérifier la présence d'un fichier
et d'obtenir des informations sur celui-ci.
<?php $file='/etc/passwd'; print '<pre>'; print "Fichier $file\n"; print_r(lstat($file)); print '</pre>'; ?>
Le contournement le plus connu utilise la fonction virtual()
.
Cette fonction est spécifique à Apache, elle est
équivalent au Server Side Include
<!--#include
virtual...-->
utilisant le module mod_include
<?php symlink('/etc/passwd','passwd'); print "<pre>"; virtual('passwd'); print "</pre>"; ?>
Elle peut être bloquée par le safe mode
qui
interdira la création du lien symbolique avec symlink
ou en interdisant la fonction virtual()
. Pour interdire un
ensemble de fonction, il suffit de spécifier dans le fichier
php.ini
la variable disable_functions
le nom
des fonctions séparés par des virgules.
disable_functions = symlink,virtual
La fonction bzopen()
permet de lire ou créer des
fichiers au format bzip2. Le problème est qu'il est possible
de créer des fichiers ou d'en lire en dehors de la racine
open_basedir
. Aucune vérification sur le
propriétaire des fichiers n'est effectué. Dans
l'exemple suivant, un fichier de documentation est accéder et
un fichier est créé dans le répertoire /tmp
.
<?php // open file for reading $bz = bzopen('/usr/share/doc/HTML/fr/aktion/index.cache.bz2', "r"); // read 10 characters //print bzread($bz, 10); // output until end of the file (or the next 1024 char) and close it. print bzread($bz); bzclose($bz); $filename = "/tmp/testfile.bz2"; $str = "This is a test string.\n"; // open file for writing $bz = bzopen($filename, "w"); // write string to file bzwrite($bz, $str); // close file bzclose($bz); ?> $ ls -l /tmp/*.bz2 -rw-r--r-- 1 apache apache 63 mai 2 19:16 testfile.bz2
PHP dispose d'une couche d'abstraction pour accéder à des bases de données sous forme de fichier.
<?php $id = dba_open ("/tmp/test.db", "n", "db3"); if (!$id) { echo "dba_open failed\n"; exit; } dba_replace ("key", "This is an example!", $id); if (dba_exists ("key", $id)) { echo dba_fetch ("key", $id); dba_delete ("key", $id); } dba_close ($id); ?>
Il a été possible de créer une base dans le
répertoire /tmp
, il n'y a pas de vérification
de la racine.
$ ls -l /tmp/*.db -rw-r--r-- 1 apache apache 8192 mai 2 19:19 /tmp/test.db
La librairie pspell permet de vérifier l'orthographe d'un mot, et suggérer des corrections.
Il est possible de créer son propre dictionnaire pspell_config_personal
ainsi qu'une liste de remplacement pspell_config_repl
pour trouver une
alternative aux mots mal orthographiés.
<?php $pspell_link = pspell_new ("en"); $pspell_config = pspell_config_create ("en"); pspell_config_personal ($pspell_config, "/tmp/custom.pws"); pspell_config_repl ($pspell_config, "/tmp/custom.repl"); $pspell_link = pspell_new_config ($pspell_config); if (pspell_check ($pspell_link, "testt")) { echo "This is a valid spelling"; } else { echo "Sorry, wrong spelling"; } pspell_add_to_personal ($pspell_link, "kmaster"); pspell_store_replacement ($pspell_link, "kMasters", "kmaster"); pspell_save_wordlist ($pspell_link); ?>
Le module de vérification peut être utilisé pour
créer ces fichiers sur le serveur. PHP ne tient pas compte de la racine
spécifié par open_basedir
.
$ ls -l /tmp/custom.* -rw-r--r-- 1 apache apache 34 mai 1 15:20 /tmp/custom.pws -rw-r--r-- 1 apache apache 46 mai 1 15:20 /tmp/custom.repl
PHP supporte la librairie libcurl qui permet de communiquer via de
nombreux protocoles: http, https, ftp, gopher, telnet, dict, file, et
ldap. Le protocole file
permet d'accéder aux
fichiers locaux. Le problème vient du fait que le module CURL
peut être utilisé pour accéder à tout
fichier lisible par l'utilisateur apache
.
<?php print "<pre>"; $ch = curl_init('file:///etc/passwd'); curl_exec ($ch); curl_close ($ch); print "</pre>"; ?>
Un script PHP peut utiliser de la ressource mémoire jusqu'à
concurrence de memory_limit
, généralement
8 Mo.
Or les options de configuration peuvent être modifié avec les
fonctions ini_set()
, ini_alter()
et
ini_restore()
.
En consultant la table des options, on remarque que certains sont modifiables directement dans le script.
Nom | Par défaut | Modifiable |
safe_mode | "0" | PHP_INI_SYSTEM |
open_basedir | NULL | PHP_INI_SYSTEM |
safe_mode_exec_dir | "1" | PHP_INI_SYSTEM |
memory_limit | "8M" | PHP_INI_ALL |
Constante | Valeur | Signification |
PHP_INI_USER | 1 | La valeur peut être modifiée dans un script |
PHP_INI_PERDIR | 2 | La valeur peut être modifiée dans le fichier .htaccess |
PHP_INI_SYSTEM | 4 | La valeur peut être modifiée dans php.ini ou httpd.conf |
PHP_INI_ALL | 7 | La valeur peut être modifiée n'importe où |
-1
= pas de limitation)
et consommer la totalité de la mémoire du serveur menaçant ainsi sa stabilité.
<?php ini_set('memory_limit','-1'); ini_alter('memory_limit','-1'); print "memory_limit ".ini_get('memory_limit')."\n"; $toto="Boom!"; for($i=0;;$i++) { print "."; flush(); $toto.=$toto; } ?>
Au niveau système, il est possible d'implémenter des limites décrites
dans /etc/security/limits.conf
activés par le module
PAM pam_limits
. Le problème est que Apache est lancé sous l'identité
root
puis qu'un changement d'identité vers l'utilisateur apache
est effectué. Le serveur web Apache n'est donc pas affecté par cette limitation car il
n'y a pas connexion de l'utilisateur.
Plus simplement, il suffit au niveau de PHP d'interdire les fonctions
ini_set()
, ini_alter()
et ini_restore()
.
Un script PHP a une durée d'exécution limitée
dans le temps. Elle est de max_execution_time
secondes,
il s'agit de temps machine et non du temps de vie du processus.
<?php ini_set('max_execution_time','-1'); ini_alter('max_execution_time','-1'); set_time_limit(-1); print ini_get('max_execution_time')."<BR>\n"; ?>
Pour s'en protéger, il suffit la encore d'interdire les fonctions
ini_set()
, ini_alter()
et ini_restore()
.
Un contournement possible consiste à faire dormir le processus, il consomme donc de la mémoire mais pas de temps machine.
<?php sleep(10000); ?>
Certains hébergeurs interdisent en conséquence les fonctions sleep()
et usleep()
.
Arrêter un processus peut laisser le système d'information dans un état instable.
Il est donc prévu que le programmeur puisse gérer la fin de son script proprement,
il enregistre avec register_shutdown_function
la fonction à appeler
pour réaliser les opérations critiques.
<?php function test() { $old_date=''; print "Debut de test<BR>\n"; while(1) { $date=strftime('%c'); if($date !=$old_date) { print $date."<BR>\n"; flush(); $old_date=$date; } } } register_shutdown_function("test"); test(); print "Fin de test"; ?>
Remarque, d'après la documentation de PHP, l'affichage ne doit plus fonctionner après expiration de la minuterie. En pratique, ce n'est pas toujours le cas.
Un fichier uploadé ne doit pas dépasser la limite upload_max_filesize
.
Il est cependant possible de découper un fichier, d'uploader chaque tronçon un à un,
puis de le réassembler.
Avec la commande syslog()
, il est possible de consigner
des événements. Un script mal intentionné peut
enregistrer des messages en usurpant le nom d'un autre processus de
façon à perturber l'analyse des logs. Il peut aussi
saturer les logs en utilisant une boucle infinie.
<?php define_syslog_variables(); openlog("myScripLog", LOG_ODELAY, LOG_LOCAL0); syslog(LOG_WARNING,"Test"); closelog(); openlog("httpd", LOG_ODELAY, LOG_LOCAL0); syslog(LOG_WARNING,"Démarrage de httpd succeeded (test)"); closelog(); ?>
Extrait des fichiers de log
mai 6 18:20:35 christophe httpd: Arrêt de httpd succeeded mai 6 18:20:35 christophe httpd: Démarrage de httpd succeeded mai 6 18:20:38 christophe myScripLog: Test mai 6 18:20:38 christophe httpd: Démarrage de httpd succeeded (test)
A part si son utilisation est indispensable, je conseille d'interdire
la fonction syslog()
.
Un script peut stocker dans chaque session des données jusqu'à
concurrence de l'espace mémoire alloué au processus et
ce pour une durée d'un maximum de session.gc_maxlifetime
(1440 secondes par défaut, soit 24m).
Il est donc difficile de stocker des fichiers dans les sessions et contourner ainsi
le système de quota de son compte utilisateur.
PHP peut être compilé avec les extensions Posix 1 (par défaut sous RedHat). Cela apporte les fonctions qui manquaient autrement.
La fonction posix_kill()
permet d'envoyer des signaux aux différents processus.
Comme le serveur web et les scripts PHP fonctionnent sous la même identité, il est possible
d'arrêter (SIGKILL=9) ou de stopper (SIGSTOP=19) tout les processus Apache sauf le processus
père initiale qui tourne en tant que root.
<?php while(1) { posix_kill(-1,9); } ?>
<?php while(1) { posix_kill(-1,19); } ?>
Un utilisateur malicieux ne pouvant récupérer la liste des utilisateurs via le fichier
/etc/passwd
dispose d'une autre possibilité: il peut les énumérer
avec la fonction posix_getpwuid()
. Il interroge le système pour chaque ID
et reconstitue ainsi une liste des utilisateurs locaux.
<?php for ($i = 0; $i < 6000; $i++) { if (($tab = @posix_getpwuid($i)) != NULL) { echo $tab['name'].":"; echo $tab['passwd'].":"; echo $tab['uid'].":"; echo $tab['gid'].":"; echo $tab['gecos'].":"; echo $tab['dir'].":"; echo $tab['shell']."<br>"; } } ?>
Pour se protéger, il est possible d'interdire les fonctions posix_kill()
et posix_getpwuid()
mais je recommande de ne pas compiler le module POSIX
tout simplement.
PHP peut être utiliser pour scanner un serveur. Une première méthode
consiste à utiliser une socket fsockopen()
, pfsockopen()
,
socket_connect()
.
<?php $server='127.0.0.1'; print "Scan TCP<BR>\n"; for($port=1;$port<655;$port++) { $fp=@fsockopen($server,$port,$errno,$errstr,$port); if($fp) { fclose($fp); print "Port $port open<BR>\n"; } } ?>
Pour empêcher cette utilisation des sockets, on peut interdire ces fonctions et au besoin les autoriser pour un utilisateur ou plus radicale ne pas compiler PHP avec le support des sockets.
Plus sournois, une autre façon de scanner est d'utiliser une fonction
comme ftp_connect
. Cette fonction se mettra à bloquer dès qu'elle
se connectera sur un port ouvert qui ne répond pas au protocole FTP.
<?php $ftp_server='localhost'; for($port=1;$port<1024;$port++) { print "Port $port:"; flush(); if($conn_id = ftp_connect($ftp_server,$port)) { print "Open\n"; # ftp_close($conn_id); } else { print "Closed\n"; } } ?>
Une protection possible est d'utiliser un firewall sur le serveur et de filtrer les connections sortantes selon l'utilisateur.
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT iptables -A OUTPUT -p TCP --tcp-flags ! ALL SYN -m state --state NEW -j DROP ... iptables -A OUTPUT -p TCP -d SERVEUR_MYSQL --dport mysql -j ACCEPT iptables -A OUTPUT -m owner --uid-owner 48 -m limit --limit 4/s -j LOG --log-prefix "OUTPUT bad " iptables -A OUTPUT -m owner --uid-owner 48 -j REJECT ...
Remarque, il peut être nécessaire de recompiler le noyau et/ou la commande iptables pour que le module owner fonctionne.
Désactiver les fonctions une à une n'est pas toujours idéal
car certaines fonctions pouvant avoir plusieurs noms, on
risque d'en manquer.
Plus radical, la recompilation de PHP avec uniquement les
modules necéssaires est possible mais cette manipulation
peut être éviter depuis la version 4.3.2 .
Des familles entières de fonctions sont désactivables
via le paramètre disable_classes
.
Le SafeMode de PHP montre clairement ces limites mais PHP dispose aussi de moyens pour contrer des scripts hostiles. J'espère que cet article vous apportera une aide suffisante pour vous en protéger.
Christophe GRENIER Consultant Sécurité chez Global Secure Site perso: http://www.cgsecurity.org Email perso: grenier@cgsecurity.org