GuppY ***** Informations : °°°°°°°°°°°°°° Langage : PHP Website : http://www.freeguppy.org Version : 2.4 Problèmes : - XSS Permanent - Lecture de fichiers - Ecriture de fichiers Developpement : °°°°°°°°°°°°°°° GuppY, l'ancien miniPortail qui s'est "rénové", est un portail (CMS). L'équipe GuppY s'est lancé le défi de faire un portail de qualité (et c'est réussi) et surtout sans base de données ! Tout ce passe via des fichiers. Bien sûr des problèmes de sécurité en découlent. Mais le premier dont nous allons parler n'en découle justement pas, lui. Il s'agit de divers failles XSS exploitables sur les forums, dans les réactions à des articles,.. via le BBCode puis la signature. dAs a écrit récemment un tutoriel sur une autre faille dans le BBCode. Disponible : - en français http://www.echu.org/modules/news/article.php?storyid=369 - in english http://www.echu.org/articles/alertes/echu-alert4.txt Tous les problèmes qui concernent le BBCode se trouvent dans la balise [l], qui permet de reproduire la balise HTML , le lien hypertexte. Ainsi si on tape dans le forum un message contenant : [l]hop[/l], on obtiendra à sa lecture : hop. Le code php traduisant cette balise [l] se trouve dans le fichier postguest.php : -------------------------------------------------------------------------------------------------------------------- [...] $ptxt = eregi_replace("\\[l\\]www.([^\\[]*)\\[/l\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[l\\]www.([^\\[]*)\\[/L\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]www.([^\\[]*)\\[/l\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]www.([^\\[]*)\\[/L\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[l\\]([^\\[]*)\\[/l\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[l\\]([^\\[]*)\\[/L\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]([^\\[]*)\\[/l\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]([^\\[]*)\\[/L\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[l=([^\\[]*)\\]([^\\[]*)\\[/l\\]","\\2",$ptxt); $ptxt = eregi_replace("\\[l=([^\\[]*)\\]([^\\[]*)\\[/L\\]","\\2",$ptxt); $ptxt = eregi_replace("\\[L=([^\\[]*)\\]([^\\[]*)\\[/l\\]","\\2",$ptxt); $ptxt = eregi_replace("\\[L=([^\\[]*)\\]([^\\[]*)\\[/L\\]","\\2",$ptxt); [...] -------------------------------------------------------------------------------------------------------------------- J'ai pris la balise [l] pour plus de facilité, mais les mêmes failles peuvent être utilisées avec [L], [l=URL] ou [L=URL]. Le premier problème vient du fait qu'il n'y a aucun filtre ou presque appliqué sur la valeur donnée comme URL du lien. Ainsi, si on tape par exemple comme balise BBCode : [l]" style="background:url('javascript:alert()');visibility:hidden;[/l] on obtiendra comme code HTML (sans les passages à la ligne bien sûr) : ------------------------------------------------------------------------------------------ " style="background:url('javascript:[SCRIPT]');visibility:hidden; ------------------------------------------------------------------------------------------ et le javascript [SCRIPT] sera executé et caché (grâce à visibility:hidden). Plus de détails sur l'attribut style dans mon texte sur le XSS Permanent dans PHP-Nuke, et dans un texte à venir sur le XSS. Le deuxième XSS possible avec la balise [l] est dû à la possibilité d'imbriquer deux de ces balises. Cela peut permettre d'injecter un script même sans utiliser de caractère ' ou ". Imaginons par exemple que nous tapons les balises imbriquées suivantes : [l][l] {MACHIN} [/l][/l] On obtiendra alors le HTML suivant : {MACHIN} " target=_blank> {MACHIN} ce qui ne donne pas un code fort clair, pas idéal comme exemple :p Pour être plus clair et plus logique dans le résultat HTML, tapons la balise : [l][l] blob=[/l][/l] Cela donnera le HTML suivante (avec des passages à la ligne à chaque changement d'attribut ou de balise) : ------------------------------------- 1 6 10 blob= 11 12 ------------------------------------- Détaillons pour bien comprendre ce qui se passe. Une première balise est ouverte (1,5), avec les attributs suivants : - href (2) Qui est normalement l'url du lien vers lequel une balise doit diriger, mais qui a ici comme valeur : test2= - target (4) Qui a comme valeur _blank Ensuite une deuxième balise est ouverte (6,9) avec les attributs : - href (7) qui a comme valeur : blob= - target (8) qui a comme valeur : _blank Ensuite est affiché " blob=" en dehors de toute balise (10), puis les deux balises sont refermées (11,12). On a donc réussi à insérer dans une balise autre chose qu'un argument d'attribut, et sans utiliser ni ' ni ". Pour exploiter ceci, il suffirait de taper les balises imbriquées : [l][l] style=list-style:url(javascript:[SCRIPT]) truc=[/l][/l] ce qui donnera comme code HTML (avec la structure de ci-dessus) : --------------------------------------------------------------------------- style=list-style:url(javascript:[SCRIPT]) truc= --------------------------------------------------------------------------- [SCRIPT] est exécuté en tant qu'argument du deuxième attribut (style) de la première balise . Attaquons nous maintenant à la signature. Partout sauf à un endroit, il est impossible de placer une balise HTML dans la signature qui est, comme presque toutes les variables modifiables de GuppY, purgée de toutes balises HTML et PHP par la fonction strip_tags(). Cette balise autorisée une fois dans la signature est la balise
, et le code où elle est autorisée se trouve dans les fichiers inc/includes.inc et inc/includes_IIS.inc : ------------------------------------------------------------------------------- [...] $usercookie = "GuppYUser"; $userprefs = array(); if (!empty($HTTP_COOKIE_VARS[$usercookie])) { $userprefs = explode("||",$HTTP_COOKIE_VARS[$usercookie]); $userprefs[0] = strip_tags($userprefs[0]); $userprefs[1] = strip_tags($userprefs[1]); $userprefs[2] = strip_tags($userprefs[2]); $userprefs[3] = strip_tags($userprefs[3]); $userprefs[4] = strip_tags($userprefs[4]); $userprefs[5] = strip_tags($userprefs[5]); $userprefs[6] = strip_tags($userprefs[6],"
"); if (($userprefs[0] == $lang[0] || $userprefs[0] == $lang[1]) & empty($lng)) { $lng = $userprefs[0]; } } [...] ------------------------------------------------------------------------------- On voit que l'élément 6 qui du cookie nommé "GuppYUser" qui est la signature n'a aucun tag autorisé sauf
. Les éléments sont séparés par les caractères ||. Encore une fois on peut utiliser l'attribut style pour injecter un script, par exemple :
Pratiquement, il suffit d'envoyer un cookie avec comme nom GuppYUser et comme valeur : -------------------------------------------------------------------------------- fr||[NICK]||[MAIL]||LR||||on||
-------------------------------------------------------------------------------- sur le site de la victime puis d'y envoyer un message pour que son premier lecteur execute le script. [NICK] et [MAIL] sont au choix. Voyons maintenant les failles liées aux fichiers. Dans le fichier inc/functions.php se trouvent deux fonctions : -------------------------------------------------------------- [...] function ReadDBFields($fic) { global $connector; $DataDB = Array(); if (FileDBExist($fic)) { $DataDB = file($fic); for ($i = 0; $i < count($DataDB); $i++) { $Fields[$i] = explode($connector,trim($DataDB[$i])); } } return $Fields; } function WriteDBFields($fic,$Fields) { global $connector; $fhandle = fopen($fic, "w"); $DataDB = ""; for ($i = 0; $i < count($Fields); $i++) { for ($j = 0 ; $j < (count($Fields[$i])-1); $j++) { $DataDB .= trim($Fields[$i][$j]).$connector; } $DataDB .= trim($Fields[$i][count($Fields[$i])-1])."\n"; } fputs($fhandle, $DataDB); fclose($fhandle); } [...] -------------------------------------------------------------- La fonction ReadDBFiels, elle, lis le fichier $fic donné en argument, et renvois son contenu sous forme de tableau donc chaque élément a été séparé par les caractères ||. La fonction WriteDBFiels() écrit dans le fichier $fic donné en argument chaque élément de $Fields séparé par les caractères ||. Ces deux fonctions sont utilisées dans le fichier tinymsg.php, permettant de lire et d'envoyer des messages privés entre membres : ----------------------------------------------------------------------------------------------------------------------------- [...] elseif ($action == 2) { [...] $dbmsg[0][0] = 0; $dbmsg[1][0] = $from; $dbmsg[1][1] = GetCurrentDateTime(); $dbmsg[1][2] = PutBR(RemoveConnector(stripslashes($msg))); WriteDBFields($userep.$to.$dbext,$dbmsg); } [...] elseif ($action == 3) { ?> [...] $dbmsg = Array(); if (FileDBExist($userep.$userprefs[1].$dbext)) { $dbmsg = ReadDBFields($userep.$userprefs[1].$dbext); for ($i = 1; $i < count($dbmsg); $i++) { ?>

[ ]


[...] ----------------------------------------------------------------------------------------------------------------------------- Si la variable $action vaut 2, on utilise la première fonction, WriteDBFields(). Comme premier argument, le chemin complet du fichier dans lequel on va écrire le deuxième argument, on a $userep.$to.$dbext . Les variables $userep et $dbext sont définies dans le code PHP, il est donc impossible de les modifier. $userep vaut "/data/usermsg/" (le repertoire du fichier) et $dbext vaut ".dtb" (l'extension du fichier). Par contre on peut modifier $to, la variable du milieu, le nom du fichier. C'est là que vient la faille. Mais voyons d'abord ce qui est écrit dans le fichier, $dbmsg. Cette variable est un double tableau. L'élément [0][0] contient "0". L'élément [1][0] contient la variable $from que l'on peut modifier nous-mêmes. L'élément [1][1] contient le résultat de GetCurrentDateTime(), qui renvois la date et l'heure. L'élément [1][2] contient la variable $msg, que l'on peut également modifier. Toutes les variables, dont $msg et $from sont soumises à une fonction strip_tags(), empêchant d'envoeyr toute balise php ou HTML. Voyons maintenant où se trouve la faille. Vu qu'on peut modifier le nom du fichier, on peut déjà assez simplement modifier le dossier où va se trouver le fichier dans lequel on veut écrire $dbmsg. Par exemple avec l'url http://[target]/tinymsg.php?action=2&to=../../test&from=hop1&msg=hop2, la fonction WriteDBFields() écrira dans le fichier http://[target]/test.dtb la phrase : 0\nhop1||[DATE+HEURE]||hop2 alors qu'elle aurait dû l'écrire dans le fichier http://[target]/data/usermsg/test.dtb. Les fichiers .dtb et .inc sont illisibles à cause du .htaccess. Mais on peut déjà avec ça modifier quelques petites choses, comme par exemple ajouter une réponse au sondage en cours. En effet si on tape l'url : http://[target]/tinymsg.php?action=2&from=Vive la fete!||Great !||rose||10000&msg=1&to=../poll on écrira la ligne : Vive la fete!||Great !||rose||10000 dans le fichier http://[target]/data/poll.dtb, ce qui aura comme effet d'ajouter la réponse "Vive la fete!" au sondage en cours, avec une couleur rose à l'affichage du résultat, et déjà un dix milles votants pour cette réponse. La deuxième posibilité, qui compléte la gravité de la faille, est qu'on peut changer l'extension du fichier, grâce au caractère %00 (%00 dans l'url mais representé dans php par \0), qui fait ignorer la partie de l'url du fichier qui se trouve après lui. Ainsi, avec l'url : http://[target]//tinymsg.php?action=2&to=../../tadaam.html%00&from=youpi1&msg=youpi2 la fonction WriteDBFields(), qui aura comme premier argument "/data/usermsg/../../tadaam.html\0.dtb", écrira la ligne : 0\nyoupi1||[DATE+HEURE]||youpi2 dans le fichier http://[target]/tadaam.html, ce qui donnera à la lecture : ---------------------------- 0 youpi1||[DATE+HEURE]||youpi2 ---------------------------- \n étant le caractère de saut de ligne. La faille de lecture, elle aussi, peut utiliser le caractère %00 (ou \0) mais on peut imaginer d'autres possibilités. Comme par exemple lire les messages encore non-lus des autres utilisateurs. L'argument donné à ReadDBFields(), le fichier qui va être lu, est $userep.$userprefs[1].$dbext .On l'a vu, $userprefs est le deuxième élément du cookie GuppYUser. Donc pour changer sa valeur, c'est : fr||->ICI<-||[MAIL]||LR||||on||1 , dans la valeur du cookie GuppYUser. Pour lire les message de John, il suffit de taper à la place indiquée ci-dessus le pseudo 'John' et d'envoyer le cookie : GuppYUser=fr||John||[MAIL]||LR||||on||1 sur la page http://[target]/index.php pour se voir avertit de la reception des messages. Il est bien sûr plus interessant d'utiliser %00. Le principe est exactement le même que pour l'écriture. Par exemple pour lire le fichier http://[target]/admin/mdp.php, contenant le mot de passe crypté en md5 de la partie admin, il suffit d'envoyer un cookie avec comme nom "GuppYUser" et comme valeur : fr||../../admin/mdp.php%00||[MAIL]||LR||||on||1 à l'url http://[target]/tinymsg.php?action=3 . Patchs : °°°°°°°° Un patch est disponible sur http://www.phpsecure.info. L'équipe a été avertie, a réagit directement et a collaboré à la rédaction du patch. Dans inc/includes.inc et inc/includes_IIS.inc, remplacer la ligne : --------------------------------------------------- $userprefs[6] = strip_tags($userprefs[6],"
"); --------------------------------------------------- par : ---------------------------------------------------------------------------------------------- $userprefs[6] = str_replace("\n","
",strip_tags(str_replace("
","\n",$userprefs[6]))); ---------------------------------------------------------------------------------------------- Dans postguest.php, remplacer les lignes : -------------------------------------------------------------------------------------------------------------------- $ptxt = eregi_replace("\\[l\\]www.([^\\[]*)\\[/l\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[l\\]www.([^\\[]*)\\[/L\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]www.([^\\[]*)\\[/l\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]www.([^\\[]*)\\[/L\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[l\\]([^\\[]*)\\[/l\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[l\\]([^\\[]*)\\[/L\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]([^\\[]*)\\[/l\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]([^\\[]*)\\[/L\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[l=([^\\[]*)\\]([^\\[]*)\\[/l\\]","\\2",$ptxt); $ptxt = eregi_replace("\\[l=([^\\[]*)\\]([^\\[]*)\\[/L\\]","\\2",$ptxt); $ptxt = eregi_replace("\\[L=([^\\[]*)\\]([^\\[]*)\\[/l\\]","\\2",$ptxt); $ptxt = eregi_replace("\\[L=([^\\[]*)\\]([^\\[]*)\\[/L\\]","\\2",$ptxt); -------------------------------------------------------------------------------------------------------------------- par : ------------------------------------------------------------------------------------------------------------------------ $ptxt = eregi_replace("\\[l\\]www.([^\[\(\"']*)\\[/l\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[l\\]www.([^\[\(\"']*)\\[/L\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]www.([^\[\(\"']*)\\[/l\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]www.([^\[\(\"']*)\\[/L\\]", "\\1",$ptxt); $ptxt = eregi_replace("\\[l\\]([^\[\(\"']*)\\[/l\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[l\\]([^\[\(\"']*)\\[/L\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]([^\[\(\"']*)\\[/l\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[L\\]([^\[\(\"']*)\\[/L\\]","\\1",$ptxt); $ptxt = eregi_replace("\\[l=([^\[\(\"']*)\\]([^\[]*)\\[/l\\]","\\2",$ptxt); $ptxt = eregi_replace("\\[l=([^\[\(\"']*)\\]([^\[]*)\\[/L\\]","\\2",$ptxt); $ptxt = eregi_replace("\\[L=([^\[\(\"']*)\\]([^\[]*)\\[/l\\]","\\2",$ptxt); $ptxt = eregi_replace("\\[L=([^\[\(\"']*)\\]([^\[]*)\\[/L\\]","\\2",$ptxt); ------------------------------------------------------------------------------------------------------------------------ La GuppY Team s'est occupé de la faille de lecture/ecriture dans des fichiers. Pour cela, dans inc/includes.inc et inc/includes_IIS.inc, elle a ajouté après la ligne : -------------------------------------------- $userprefs[1] = strip_tags($userprefs[1]); -------------------------------------------- les lignes : ---------------------------------------------------------------- if (ereg("\\0",$userprefs[1]) or ereg("\.\.",$userprefs[1])) { die("GuppY thanks frog-m@n"); } ---------------------------------------------------------------- et dans tinymsg.php, après la ligne : ------------------------ $msg = strip_tags($msg); ------------------------ les lignes : ------------------------------------------------------------------------------------- if (ereg("\\0",$from) or ereg("\.\.",$from) or ereg("\\0",$to) or ereg("\.\.",$to)) { die("GuppY thanks frog-m@n"); } ------------------------------------------------------------------------------------- Credits : °°°°°°°°° Auteur : frog-m@n E-mail : leseulfrog@hotmail.com Website : http://www.phpsecure.info Date : 02/10/2003