Les problèmes des CGI Perl

----[  Phrack Magazine --- Vol. 9 | ÉDITION 55 --- 09.09.99 --- 07 sur 19  ]


-------------------------[ Les problèmes des CGI Perl. ]


--------[  rain.forest.puppy / [ADM/Wiretrip] <rfp@wiretrip.net>  ]


--------[ 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&mdash;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&mdash;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.

<tirade> 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. <fin de la tirade>

	$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. <fin de la tirade>

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= <FILE>;
	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 (<FILE>) {
			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 (<AM>)
	     {
	      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&mdash;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