----[ Phrack Magazine --- Vol. 9 | ÉDITION 55 --- 09.09.99 --- 07 sur 19 ] -------------------------[ Les problèmes des CGI Perl. ] --------[ rain.forest.puppy / [ADM/Wiretrip] ] --------[ Version française pas Deny ----------------[ Introduction Je suppose que je devrais faire une introduction au sujet de ce qui suit. La plupart du temps, j'ai programmé et vérifié de nombreux CGIs, et j'ai essayé de comprendre comment tirer profit de quelques problèmes dont j'ai pensé qu'ils avaient des failles. Quoi qu'il en soit, je n'en dirai pas plus, rentrons dans le vif du sujet. ----------------[ Le morceau de choix ----[ Poison NULL byte : La faille du caractère nul Le terme Poison NULL byte a été, à l'origine, employé par Olaf Kirch dans un article de Bugtraq. Je l'ai apprécié, et il convient ici ... Aussi, je m'en suis inspiré. Mes remerciements à Olaf. Dans quelle occasion root est diffèrent de root, mais en même temps, root est égal à root (Confu, quand même) ? Quand vous fusionnez des langages de programmation. Une nuit, je me suis demandé ce que Perl autorisait exactement, et si je pouvais obtenir de quoi franchir des obstacles de manière inattendue. Ainsi, j'ai commencé à transférer des données très étranges vers des appels et fonctions système divers. Rien de spectaculaire, à l'exception d'un seul qui était tout à fait remarquable ... Vous voyez, j'ai voulu ouvrir un fichier particulier, rfp.db. J'ai forgé un scénario web pour obtenir une valeur en entrée rfp, j'ai ajouté une extension .db, et ensuite j'ai ouvert le fichier. En Perl, la partie fonctionnelle du script ressemblait à : # parse $user_input $database="$user_input.db"; open(FILE "<$database"); Très bien. Je saisis user_input=rfp, et le script essaie d'ouvrir rfp.db. Plutôt simple (oublions la séquence évidente /../ pour le moment). Ensuite, c'est devenu intéressant quand j'ai saisi user_input=rfp%00. Perl a créé $database="rfp\0.db", et ensuite a essayé d'ouvrir $database. Résultats ? Il a ouvert rfp (où l'aurait ouvert, s'il avait existé). Qu'est-il arrivé à .db ? C'est la partie intéressante. On voit que Perl autorise les caractères nuls dans ses variables en tant que données. Contrairement à C, le caractère nul n'est pas un délimiteur de chaîne. Ainsi, root est différent de root\0. Mais, les appels système/noyau sous-jacents sont programmés en C, qui RECONNAÎT le caractère nul en tant que délimiteur. Ainsi quel est le mot de la fin ? Perl transmet rfp\0.db, mais la bibliothèque sous-jacente arrête le processus quand elle atteint le premier caractère nul (notre caractère). Que se passe-t'il dans le cas d'un script qui permet à un jeune administrateur de confiance de changer les mots de passe de n'importe quel compte À L'EXCEPTION de root ? Ce code pourrait être : $user=$ARGV[1] # utilisateur que l'administrateur veut modifier if ($user ne "root"){ # faîtes ce qui doit être fait pour cet utilisateur } (**NOTE: C'est dans un sens une manière ou une théorie simpliste juste pour illustrer ce point) Ainsi, si le jeune administrateur essaie de saisir root comme nom, cela ne fera rien. Mais, si le jeune administrateur saisit root\0, Perl transformera l'essai et exécutera le bloc. À présent, quand des appels système sont transférés (à moins qu'ils le soient entièrement en Perl, ce qui est possible mais peu probable), ce caractère nul sera réellement effacé, et des activités se produiront sous l'égide de root. Tandis que ce n'est pas nécessairement un problème de sécurité en soi, c'est certainement une particularité intéressante à surveiller. J'ai vu nombre de CGIs qui ajoutent un .html à des données de formulaire soumises par un utilisateur dans la page résultante. Par exemple, page.cgi?page=1 affiche 1.html. À demi-sécurisé, parce qu'on ajoute une page .html, ainsi on pourrait penser, au pire, qu'on affiche seulement des pages HTML. Eh bien, si nous envoyons page.cgi?page=page.cgi%00 (%00 == '\0' escaped) Alors le script nous enverra une copie de son propre source ! Même un contrôle de Perl -e échouera : $file="/etc/passwd\0.txt.whatever.we.want"; die("hahaha! Caught you!) if($file eq "/etc/passwd"); if (-e $file){ open (FILE, ">$file");} Cela fonctionnera (s'il y a, en fait, un /etc/passwd), et il sera ouvert avec des droits d'accès en écriture. Une solution ? Simple ! Supprimez les caractères nuls. En Perl, c'est aussi simple que $insecure_data=~s/\0//g; Ne les échappez pas avec le reste des méta caractères du shell. Supprimez-les complètement. Barre oblique (inverse) et conséquences Si vous consultez la FAQ du W3C WWW Security, vous observerez que la liste recommandée des méta caractères shell comprend : &;`'\"|*?~<>^()[]{}$\n\r Ce que je trouve le plus intéressant est que tout le monde semble oublier la barre oblique inverse ('\'). Peut-être parce qu'il s'agit simplement de la manière d'écrire le caractère d'échappement en Perl : s/([\&;\`'\\\|"*?~<>^\(\)\[\]\{\}\$\n\r])/\\$1/g; Avec toutes ces barres obliques inversées échappant [](){}, etc., il devient difficile de savoir si la barre oblique inversée est aussi prise en compte (ici, il s'agit de \\). Peut-être que certains sont juste dyslexiques à propos d'expressions régulières, et pensent que rencontrer un exemple de barre oblique inversée suffit à la prendre en compte. Aussi, bien sûr, pourquoi est-ce que ce point est important ? Imaginons que vous avez soumis la ligne suivante à votre CGI : user data `rm -rf /` Vous l'exécutez avec votre code d'échappement Perl, ce qui devient : user data \`rm -rf /\` Ce qui rend à présent vos opérations shell sûres à l'emploi, etc. À présent, disons que vous avez omis d'échapper les barres obliques inversées. L'utilisateur soumet la ligne suivante : user data \`rm -rf / \` Votre programme devient : user data \\`rm -rf / \\` La double barre oblique inversée se transformera en simple barre oblique de donnée, n'échappant pas les guillemets obliques. Cela exécutera efficacement rm -rf / \`. Naturellement, avec cette méthode, vous devrez toujours prendre en charge de fausses barres obliques inversées. Laisser la barre oblique inversée comme dernier caractère de la ligne conduira Perl à commettre une erreur lors des appels système et des guillemets obliques (du moins, pendant mon essai). Vous devrez ruser pour venir à bout de ce problème. ;) (C'est possible ...) Un autre effet secondaire intéressant de la barre oblique inversée provient du code suivant visant à empêcher une attaque transversale de répertoire : s/\.\.//g; Son travail consiste à supprimer les doubles points, empêchant efficacement une attaque transversale d'un fichier. Ainsi, /usr/tmp/../../etc/passwd deviendra /usr/tmp///etc/passwd Ce qui ne fonctionnera pas (Note: de multiples barres obliques sont autorisées. Essayons ls -l /etc////passwd') À présent, saisissons notre amie la barre oblique. Saisissons la ligne /usr/tmp/.\./.\./etc/passwd L'expression régulière ne s'accordera pas à cause de la barre oblique inversée. À présent, utilisons ce nom de fichier en Perl $file="/usr/tmp/.\\./.\\./etc/passwd"; $file=s/\.\.//g; system("ls -l $file"); nous devons utiliser les doubles barres obliques inversées pour amener Perl à insérer seulement une barre oblique de donnée—autrement Perl suppose que vous échappez simplement la séquence. La chaîne est toujours /usr/tmp/.\./.\./etc/passwd. Cependant, le passage ci-dessus fonctionne seulement avec des appels système et de guillemets obliques. Les fonctions de Perl -e et open (sans le pipe) ne fonctionneront PAS. Donc : $file="/usr/tmp/.\\./.\\./etc/passwd"; open(FILE, "<$file") or die("No such file"); échouera en affichant No such file. Je pense que c'est parce qu'on a besoin du shell pour transformer le \. en . (puisque une période échappée est toujours une période). Une solution ? Assurez vous que vous échappez la barre oblique inversée. Assez simple. ----[ Ce pipe agaçant En Perl, ajouter un | (pipe) à la fin d'un nom de fichier dans une commande open conduit Perl à exécuter le fichier spécifié, plutôt que de l'ouvrir. Ainsi avec, open(FILE, "/bin/ls") vous obtiendrez pas mal de code binaire, mais open(FILE, "/bin/ls|") exécutera réellement /bin/ls. Notez que l'expression régulière suivante s/(\|)/\\$1/g empêchera ceci (Perl échouera en affichant unexpected end of file, à cause d'une nouvelle ligne indiquée par le \ final. Si vous connaissez une façon de faire ceci, contactez-moi). À présent, on peut compliquer la situation plus avant avec les autres techniques que nous venons d'apprendre. Supposons que $FORM est une entrée brute soumise par un utilisateur via un CGI. D'abord, nous avons : open(FILE, "$FORM") où nous pouvons donner à $FORM la valeur ls| pour obtenir un listage de répertoire. À présent, supposons que nous avons : $filename="/safe/dir/to/read/$FORM" open(FILE, $filename) alors on doit indiquer spécifiquement où se situe ls,aussi nous attribuons à $FORM la valeur ../../../../bin/ls|, ce qui nous donne un listage de répertoire. Puisque c'est une commande open avec un pipe, il est possible d'utiliser notre technique de barre oblique inversée pour venir à bout des expressions régulières contre les attaques transversales, si c'est approprié. À ce stade, on peut utiliser les options en ligne de commande avec une commande. Par exemple, en utilisant l'extrait de code ci-dessus, on peut attribuer à $FORM la valeur touch /myself| pour créer le fichier /myself (désolé, je n'ai pas pu résister au nom du fichier. :) À présent, on rencontre une situation plus délicate : $filename="/safe/dir/to/read/$FORM" if(!(-e $filename)) die("I don't think so!") open(FILE, $filename) À présent, nous devons duper le -e. Le problème est que -e retournera que le fichier n'existe pas si on essaie de trouver ls|, parce qu'il cherche le nom du fichier avec le pipe réel à la fin. Ainsi, nous devons supprimer le pipe lors du contrôle effectué par le paramètre -e, mais Perl doit toujours le voir. À quoi pensez-vous ? C'est ici que Poison NULL vient à la rescousse ! Il nous suffit de définir $FORM à ls\0| (ou, pour échapper avec la méthode GET d'un formulaire web, ls%00|). Ceci conduit -e à contrôler ls (il s'arrête de contrôler à notre caractère nul, ignorant le pipe). Cependant, Perl voit toujours le pipe à la fin quand il s'agit d'ouvrir notre fichier, ainsi il exécutera notre commande. Il y a un hic, toutefois ...quand Perl exécute notre commande, il s'arrête à notre caractère nul—cela signifie que nous ne pouvons indiquer d'options en ligne de commande. Peut-être que des exemples seront plus explicites : $filename="/bin/ls /etc|" open(FILE, $filename) Cela affiche un listage du répertoire /etc. $filename="/bin/ls /etc\0|" if(!(-e $filename)) exit; open(FILE, $filename) Cela échouera parce que -e voit que /bin/ls /etc n'existe pas. $filename="/bin/ls\0 /etc|" if(!(-e $filename)) exit; open(FILE, $filename) Cela fonctionnera, hormis que nous obtiendrons seulement le listage de notre répertoire actuel (un simple ls) ...cela ne fournira pas à ls le répertoire /etc en argument. Je veux aussi faire une note pour vous les accros du code : si vous, paresseux programmeurs Perl (je ne parle pas de TOUS les programmeurs Perl ; seulement les paresseux) prenaient le temps nécessaire de chercher et de spécifier un mode de fichier particulier, cela rendrait ce bogue inopérant. $bug="ls|" open(FILE, $bug) open(FILE, "$bug") fonctionne. Mais open(FILE, "<$bug") open(FILE, ">$bug") open(FILE, ">>$bug") etc..etc.. ne fonctionne pas. Ainsi, si vous voulez lire dans un fichier, ouvrez donc votre fichier avec <$file, et non juste avec $file. Insérer ce signe moins-que (un caractère négligeable !) peut vous épargner à vous et à votre serveur pas mal de problèmes. Bien, à présent que nous avons quelques arguments, croisons le fer. ----------------[ Scripts Perl (dangereux) en production Notre premier CGI, je l'ai trouvé sur freecode.com. C'est un script de petites annonces. Voici le fichier CGI : # First version 1.1 # Dan Bloomquist dan@lakeweb.net Voici le premier exemple ...Dan analyse toutes les variables du formulaire en entrée dans %DATA. Il ne supprime pas les caractères .., ni les caractères nuls. Aussi, observons cet extrait de code : #Ceci définit le chemin réel vers les fichiers html et lock. #L'action se situe ici, après que les données de la page de #petites annonces transmises par POST aient été lues. $pageurl= $realpath . $DATA{ 'adPath' } . ".html"; $lockfile= $realpath . $DATA{ 'adPath' } . ".lock"; En employant adPath=/../../../../../etc/passwd%00, on peut indiquer à $pageurl de pointer sur le fichier /etc/passwd. Même chose pour $lockfile. Nous ne pouvons pas employer le pipe à la fin, parce qu'il se situe après le .html"/".lock (enfin, vous POUVEZ l'employer, mais cela ne fonctionnera pas. ;) #Lire la page de petites annonces open( FILE,"$pageurl" ) || die "can't open to read $pageurl: $!\n"; @lines= ; close( FILE ); Ici, Dan lit dans $pageurl, qui est le fichier qu'on a indiqué. Heureusement pour Dan, il ouvre immédiatement $pageurl en écriture. Ainsi, quelque soit le fichier qu'on demande à lire, on doit aussi avoir des droits pour y écrire. Cela limite le risque d'exploitation. Mais cela sert d'exemple grandeur nature de ce type de problème. De manière intéressante, Dan poursuit : #Envoyer votre courrier électronique. # open( MAIL, "|$mailprog $DATA{ 'adEmail' }" ) || die "can't open sendmail: $adEmail: $!\n"; Hmmmmm ...ceci est notre standard d'interdiction. Et Dan n'examine pas les méta caractères du shell, ainsi cet adEmail devient plutôt effrayant. En furetant autour de freecode.com, j'ai ensuite obtenu ce formulaire d'authentification simple : # flexform.cgi # Written by Leif M. Wright # leif@conservatives.net Leif analyse l'entrée du formulaire dans %contents, et n'échappe pas les méta caractères du shell. Ensuite, il poursuit $output = $basedir . $contents{'file'}; open(RESULTS, ">>$output"); En utilisant notre attaque transversale de répertoire standard, nous n'avons même pas à placer un caractère nul à notre extension. N'importe quel fichier indiqué s'ouvre en écriture avec ajout, et ici encore, nous devons avoir un peu de chance avec nos permissions. De nouveau, notre bogue relatif au pipe ne fonctionnera pas parce qu'on a défini un mode spécifique d'ajout (via le >>). Ensuite, voici LWGate, qui est une interface web de nombre de paquetages de listes de diffusion populaires. # lwgate by David W. Baker, dwb@netspace.org # # Version 1.16 # Dave place des variables de formulaire analysées dans %CGI. Ensuite, nous avons # Le programme de courrier dans lequel on transfère des données $temp = $CGI{'email'}; $temp =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g; $MAILER = "/usr/sbin/sendmail -t -f$temp" open(MAIL,"| $MAILER") || &ERROR('Error Mailing Data') Hmmmm ...Dave semble avoir oublié la barre oblique inversée lors de son remplacement à l'aide des expressions régulières. Pas bien. Bien, passons à une des nombreuses applications de panier d'achat. Celle-ci,Perlshop, une fois encore, vient de freecode.com, Perlshop. $PerlShop_version = 3.1; # A product of ARPAnet Corp. - perlshop@arpanet.com, www.arpanet.com/perlshop Voici la partie intéressante : open (MAIL, "|$blat_loc - -t $to -s $subject") || &err_trap("Can't open $blat_loc!\n") $to est évidemment l'utilisateur défini pour le courrier. Blat est un programme de courrier pour NT. Souvenez-vous que les méta caractères du shell dans NT sont <>&|% (peut-être plus ?). Rappelez-vous l'embêtant problème de pipe que j'ai noté ? (J'espère que vous vous en souvenez ... C'était seulement il y a quelques paragraphes !). Je l'admet, c'est un bogue très improbable, mais je l'ai trouvé. Poursuivons avec Matt's Script Archive. # File Download Version 1.0 # Copyright 1996 Matthew M. Wright mattw@worldwidemart.com En premier, il analyse les données en entrée de l'utilisateur dans $Form (en n'échappant rien). Ensuite, il exécute ce qui suit : $Request_File = $BASE_DIR . $Form{'s'} . '/' . $Form{'f'}; if (!(-e $filename)) { &error('File Does Not Exist'); } elsif (!(-r $filename)) { &error('File Permissions Deny Access'); } open(FILE,"$Request_File"); while () { print; } Cela remplit les critères à propos du problème de pipe agaçant. Nous avons un contrôle avec -e, ainsi nous n'avons pas besoin d'user d'arguments en ligne de commande. Puisqu'il place $BASE_DIR au début, nous devrons employer une attaque de répertoire transversal. Je suis sûr qu'en regardant ce qui précède, vous (devriez) voir un problème beaucoup plus simple. Que faire si f=../../../../../../etc/passwd ? Eh bien, si le fichier existe et s'il est lisible, il s'affichera. Réellement. Autre chose : tous les accès à download.cgi sont journalisés grâce au code suivant : open(LOG,">>$LOG_FILE"); print LOG "$Date|$Form{'s'}|$Form{'c'}|$Form{'f'}\n"; close(LOG); Ainsi, vos faits et gestes seront épiés. Mais vous ne devriez pas commettre de mauvaises actions envers les utilisateurs des autres serveurs de toute façon. ;) Passons à BigNoseBird.com. Voici le script en question : bnbform.cgi #(c)1997 BigNoseBird.Com # Version 2.2 Dec. 26, 1998 La partie intéressante du code se situe après que le script ouvre un pipe vers sendmail en tant que MAIL : if ($fields{'automessage'} ne "") { open (AM,"< $fields{'automessage'}"); while () { chop $_; print MAIL "$_\n"; } C'est encore une simple faille. BNB ne fait aucune analyse des variables saisies par l'utilisateur (dans $fields), ainsi nous pouvons indiquer le fichier de notre choix pour automessage. En supposant qu'il est lisible dans un contexte de serveur web, il sera envoyé à l'adresse de votre choix (en théorie). ----------------[ Conclusion Il était certain que depuis le temps, j'étais un peu fatigué de patauger dans le code Perl. Je vous le conseille comme un exercice pour chacun de vous désirant aller plus loin. Et si vous le faites, envoyez-moi des nouvelles—particulièrement si vous pouvez trouver quelques scripts vulnérables à l'agaçant problème de pipe. De toute façon, j'en ai fini à ce sujet, jusqu'à la prochaine fois. Des remerciements sont disponibles chez http://www.el8.org/~rfp/greets.html ----[ EOF