|
|
Cet article est disponible en: English Castellano Deutsch Francais Nederlands Portugues Turkce |
par Frédéric Raynal, Christophe Blaess, Christophe Grenier <pappy(at)users.sourceforge.net,ccb(at)club-internet.fr,grenier(at)nef.esiea.fr> L´auteur: Christophe Blaess est un ingénieur indépendant dans le domaine de l'aéronautique Passionné par Linux, il effectue l'essentiel de son travail sur ce système, et assure la coordination des traductions des pages de manuel publiées par le Linux Documentation Project. Christophe Grenier est étudiant en 5ème année à l'ESIEA, où il est également administrateur système. La sécurité informatique est l'une de ses passions. Frédéric Raynal utilise Linux depuis des années parce qu'il ne pollue pas, qu'il est garanti sans hormones, OGM ou farines animales... il ne réclame que de la sueur et de l'astuce. Sommaire:
|
Résumé:
Obtenir un fichier, exécuter un programme via un script Perl mal programmé ... "There's More Than One Way To Do It!"
Les articles précédents de la série :
Lorsqu'un client envoie une requête pour obtenir un fichier HTML, le serveur lui retourne la page demandée (ou un message d'erreur). Le navigateur interprète alors le code HTML pour le mettre en forme et l'afficher. Par exemple, avec l'URL (Uniform Request Locator) www.linuxdoc.org/HOWTO/HOWTO-INDEX/howtos.html
, le client se connecte au serveur www.linuxdoc.org
et lui demande la page /HOWTO/HOWTO-INDEX/howtos.html
(appelée URI - Uniform Resource Identifiers), le dialogue ayant lieu en employant le protocole HTTP. Si cette page existe, le serveur renvoie le contenu du fichier demandé. Dans ce modèle, qualifié de statique, si le fichier est présent sur le serveur, il est transmis tel quel au client, sinon un message d'erreur (le fameux 404 - Not Found) est renvoyé.
Malheureusement, ce schéma ne permet pas d'interactions avec un utilisateur, ce qui rend impossible les opérations comme le e-commerce, la e-réservation de ses vacances ou encore le e-n'importe quoi.
Heureusement, il existe des solutions pour générer dynamiquement des pages HTML. Les scripts CGI (Common Gateway Interface) sont l'une d'elles. A la différence du cas précédent, les URLs pour y accéder sont construites légèrement différemment :
http://<serveur><chemin vers le script>[?[param_1=val_1][...][¶m_n=val_n]]La liste d'arguments est stockée dans la variable d'environnement
QUERY_STRING
. Un script CGI, dans ce contexte, n'est rien d'autre qu'un fichier exécutable. Il utilise l'entrée standard stdin
pour récupérer les arguments qui lui sont passés. Après exécution de son code, il affiche son résultat sur la sortie standard stdout
, qui est alors redirigée vers le client web. Pratiquement n'importe quel langage de programmation convient pour écrire un script CGI (programme C compilé, Perl, shell-scripts...).
Par exemple, recherchons ce que les HOWTOs sur www.linuxdoc.org
savent de ssh :
http://www.linuxdoc.org/cgi-bin/ldpsrch.cgi? svr=http%3A%2F%2Fwww.linuxdoc.org&srch=ssh&db=1&scope=0&rpt=20En fait, ceci est beaucoup plus simple qu'il n'y paraît. Décomposons cette URL :
www.linuxdoc.org
;/cgi-bin/ldpsrch.cgi
;?
signale le début d'une longue liste d'arguments :
srv=http%3A%2F%2Fwww.linuxdoc.org
est le serveur d'où est émise la requête ;srch=ssh
contient la requête à proprement parler ;db=1
signifie que la requête ne porte que sur les HOWTOs ;scope=0
stipule que la recherche concerne également le contenu du document, et pas seulement son titre ;rpt=20
limite à 20 le nombre de résultats affichés.Ici, avouons tout de suite, au risque de mettre fin à une légende, que nous ne sommes pas devins. Souvent, les noms des arguments et leurs valeurs sont suffisament explicites pour comprendre leurs significations. En outre, le contenu de la page affichant les résultats est pour le moins révélateur.
Vous l'aurez compris, ce qui fait l'intérêt des scripts CGI est la possibilité laissée à un utilisateur de passer des arguments... mais c'est également ce qui fait qu'un script mal écrit ouvre une faille de sécurité.
Vous avez également sans doute déjà remarqué les caractères étranges employés par votre navigateur favori ou présents dans la requête précédente. Ces caractères suivent le format iso_8859-1 (la commande man iso_8859_1
vous donnera tous les détails). Le tableau 1 donne la signification de certains codes. Signalons que certains serveurs IIS4.0 et IIS5.0 sont sensibles à une vulnérabilité fondée sur l'emploi de tels caractères (appelée bug unicode).
SSI Server Side Include
"Server Side Include
est une fonction intégrée aux serveurs web. Elle permet d'intégrer des directives dans les pages web, soit pour inclure un fichier tel quel, soit pour exécuter une commande (shell ou script CGI).
Dans le fichier de configuration httpd.conf
d'Apache, l'instruction "AddHandler server-parsed .shtml
" active ce mécanisme. Souvent, afin d'éviter le distinguo entre .html
et .shtml
, on y ajoute également l'extension .html
. Évidemment, cela ralentit un peu le serveur... Cette possibilité est contrôlée au niveau des répertoires par les directives :
Options Includes
active tous les SSI ;OptionsIncludesNoExec
interdit le exec cmd
et le exec cgi
.Dans le script guestbook.cgi
ci-joint, le texte fourni par l'utilisateur est directement inclus dans un fichier HTML, sans conversion des caractères '<' et ' >' en code html < et >. Il suffit alors à une personne curieuse de soumettre une des instructions suivantes :
<!--#printenv -->
(attention à l'espace après printenv
)<!--#exec cmd="cat /etc/passwd"-->
guestbook.cgi?email=pappy&texte=%3c%21--%23printenv%20--%3e
, quelques informations sur le système sont révélées :
DOCUMENT_ROOT=/home/web/sites/www8080 HTTP_ACCEPT=image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */* HTTP_ACCEPT_CHARSET=iso-8859-1,*,utf-8 HTTP_ACCEPT_ENCODING=gzip HTTP_ACCEPT_LANGUAGE=en, fr HTTP_CONNECTION=Keep-Alive HTTP_HOST=www.esiea.fr:8080 HTTP_PRAGMA=no-cache HTTP_REFERER=http://www.esiea.fr:8080/~grenier/cgi/guestbook.cgi? email=&texte=%3C%21--%23include+file%3D%22guestbook.cgi%22--%3E HTTP_USER_AGENT=Mozilla/4.76 [fr] (X11; U; Linux 2.2.16 i686) PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/X11R6/bin REMOTE_ADDR=194.57.201.103 REMOTE_HOST=nef.esiea.fr REMOTE_PORT=3672 SCRIPT_FILENAME=/mnt/c/nef/neftiles/grenier/public_html/cgi/guestbook.html SERVER_ADDR=194.57.201.103 SERVER_ADMIN=master8080@nef.esiea.fr SERVER_NAME=www.esiea.fr SERVER_PORT=8080 SERVER_SIGNATURE=<ADDRESS>Apache/1.3.14 Server at www.esiea.fr Port 8080</ADDRESS> SERVER_SOFTWARE=Apache/1.3.14 (Unix) (Red-Hat/Linux) PHP/3.0.18 GATEWAY_INTERFACE=CGI/1.1 SERVER_PROTOCOL=HTTP/1.0 REQUEST_METHOD=GET QUERY_STRING= REQUEST_URI=/~grenier/cgi/guestbook.html SCRIPT_NAME=/~grenier/cgi/guestbook.html DATE_LOCAL=Tuesday, 27-Feb-2001 15:33:56 CET DATE_GMT=Tuesday, 27-Feb-2001 14:33:56 GMT LAST_MODIFIED=Tuesday, 27-Feb-2001 15:28:05 CET DOCUMENT_URI=/~grenier/cgi/guestbook.shtml DOCUMENT_PATH_INFO= USER_NAME=grenier DOCUMENT_NAME=guestbook.shtml
Quant à la directive exec
, elle donne pratiquement l'équivalent d'un shell :
guestbook.cgi?email=ppy&texte=%3c%21--%23exec%20cmd="cat%20/etc/passwd"%20--%3e.
Pas la peine d'essayer "<!--#include file="/etc/passwd"-->
", le chemin est relatif au répertoire où se trouve le fichier HTML et il ne peut contenir de "..
". Le fichier error_log
d'Apache contient alors un message signalant une tentative d'accès à un fichier interdit. L'utilisateur voit dans la page html le message [an error occurred while processing this directive]
.
Il est assez rare d'avoir besoin des SSI. Le plus sage est de les désactiver de son serveur. Dans le cas présent, le problème vient de la combinaison de l'application guestbook boguée et de SSI.
Dans cette partie, nous présentons des failles de sécurité liées à l'emploi de Perl pour écrire des scripts CGI. Afin de tenter de conserver une certaine lisibilité, nous ne fournissons pas le code complet des exemples, seulement les extraits nécessaires pour comprendre où se situe le problème.
Tous nos scripts sont construits sur le modèle suivant :
#!/usr/bin/perl -wT BEGIN { $ENV{PATH} = '/usr/bin:/bin' } delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; # Make %ENV safer =:-) print "Content-type: text/html\n\n"; print "<HTML>\n<HEAD>"; print "<TITLE>Remote Command</TITLE></HEAD>\n"; &ReadParse(\%input); # utilisez plutôt $input de cette manière : # print "<p>$input{filename}</p>\n"; # #################################### # # Début du code descriptif du problème # # #################################### # # ################################## # # Fin du code descriptif du problème # # ################################## # form: print "<form action=\"$ENV{'SCRIPT_NAME'}\">\n"; print "<input type=texte name=filename>\n </form>\n"; print "</BODY>\n"; print "</HTML>\n"; exit(0); # le premier argument doit être une référence à un hachage. # Le hachage doit être rempli de données. sub ReadParse { local (*in) = @_ if @_; my ($i, $key, $val); my $in_first; my @in_second; # Read in text if ($ENV{'REQUEST_METHOD'} eq "GET") { $in_first = $ENV{'QUERY_STRING'}; } elsif ($ENV{'REQUEST_METHOD'} eq "POST") { read(STDIN,$in_first,$ENV{'CONTENT_LENGTH'}); }else{ die "ERROR: unknown request method\n"; } @in_second = split(/&/,$in_first); foreach $i (0 .. $#in_second) { # Convert plus's to spaces $in_second[$i] =~ s/\+/ /g; # Split into key and value. ($key, $val) = split(/=/,$in_second[$i],2); # Convert %XX from hex numbers to alphanumeric $key =~ s/%(..)/pack("c",hex($1))/ge; $val =~ s/%(..)/pack("c",hex($1))/ge; # Associate key and value $in{$key} .= "\0" if (defined($in{$key})); $in{$key} .= $val; } return length($#in_second); }
Nous reviendrons ultérieurement sur les arguments passés à Perl (-wT
). Nous commençons par nettoyer nos variables d'environnement $ENV
et $PATH
, puis nous envoyons l'entête du fichier HTML. La fonction ReadParse()
permet de récupérer les arguments passés au script. Des modules permettent de faire ceci bien plus souplement, mais pour vous éviter de devoir les installer si ce n'est déjà fait sur votre serveur web, cette fonction suffit amplement. Ensuite, les exemples présentés ci-dessous prennent place. Enfin, nous concluons notre fichier HTML.
Perl considère tous les caractères de manières identiques, ce qui n'est pas le cas des fonctions C par exemple. Le caractère nul de fin de chaîne est, pour Perl, un caractère comme les autres. Et alors ?
Ajoutons le code suivant à notre script pour obtenir showhtml.cgi
:
# showhtml.cgi my $filename= $input{filename}.".html"; print "<BODY>File : $filename<BR>"; if (-e $filename) { open(FILE,"$filename") || goto form; print <FILE>; }
La fonction ReadParse()
récupère l'unique argument transmis : le nom du fichier à afficher. Pour éviter qu'un petit comique essaye de lire autre chose que les fichiers HTML, nous ajoutons l'extension ".html
" à la fin du nom du fichier. Mais rappelez-vous que le caractère nul est un caractère comme les autres ...
Ainsi, lorsque notre requête est showhtml.cgi?filename=%2Fetc%2Fpasswd%00
, le fichier s'appelle alors my $filename = "/etc/passwd\0.html"
et nos yeux ébahis contemplent un fichier qui n'est pas du HTML.
Que se passe-t-il ? La commande strace
nous montre bien le cheminement suivi lors de l'ouverture d'un fichier en Perl :
/tmp >>cat >open.pl << EOF > #!/usr/bin/perl > open(FILE, "/etc/passwd\0.html"); > EOF /tmp >>chmod 0700 open.pl /tmp >>strace ./open.pl 2>&1 | grep open execve("./open.pl", ["./open.pl"], [/* 24 vars */]) = 0 ... open("./open.pl", O_RDONLY) = 3 read(3, "#!/usr/bin/perl\n\nopen(FILE, \"/et"..., 4096) = 51 open("/etc/passwd", O_RDONLY) = 3
open()
affiché par strace
correspond à l'appel-système, écrit en C. On constate que l'extension .html
a disparu, ce qui provoque bien l'ouverture du fichier protégé.
Ce problème se règle avec une simple expression régulière, chose assez simple en Perl :
s/\0//g
Voici maintenant le cas d'un script sans aucune protection qui affiche le fichier de votre choix sous une certaine arborescence :
#pipe1.cgi my $filename= "/home/httpd/".$input{filename}; print "<BODY>File : $filename<BR>"; open(FILE,"$filename") || goto form; print <FILE>;
Ne vous moquez pas de cet exemple ! Un fichier template est un modèle comportant des variables ($nom_utilisateur
, $nom_societe
...) qui sont définis dans le programme Perl. J'ai trouvé un bogue similaire dans un script où le fichier template était passé en argument.
La première erreur est évidente :
pipe1.cgi?filename=..%2F..%2F..%2Fetc%2Fpasswd"
open(FILE, "/bin/ls")
ouvre le fichier binaire "/bin/ls
"... mais open(FILE, "/bin/ls |")
exécute la commande spécifiée. L'ajout d'un simple tube '|
change le comportement du open()
.
Un autre problème vient de ce qu'ici, l'existence du fichier n'est pas testée, ce qui permet non seulement d'exécuter n'importe quelle commande, mais aussi de lui passer les arguments de son choix : pipe1.cgi?filename=..%2F..%2F..%2Fbin%2Fcat%20%2fetc%2fpasswd%20|
affiche encore le contenu du fichier de mots de passe.
Tester l'existence du fichier à ouvrir laisse déjà moins de liberté :
#pipe2.cgi my $filename= "/home/httpd/".$input{filename}; print "<BODY>File : $filename<BR>"; if (-e $filename) { open(FILE,"$filename") || goto form; print <FILE> } else { print "-e failed: no file\n"; }L'exemple précédent ne fonctionne plus. En effet, le test d'existence "
-e
" échoue car il ne trouve pas le fichier../../../bin/cat /etc/passwd |
".
Essayons maintenant la commande /bin/ls
. Le comportement sera le même qu'auparavant. En effet, si nous cherchons, par exemple, à lister le contenu du répertoire /etc
, le "-e
" teste alors l'existence du fichier "../../../bin/ls /etc |
, mais il n'existe pas plus. Tant que nous ne fournirons pas le nom d'un fichier "fantôme", nous ne pourrons rien tirer d'intéressant :(
Tout n'est cependant pas perdu, même si le résultat est moins intéressant. Le fichier /bin/ls
existe bien lui (enfin, sur la plupart des systèmes), mais si le open()
est appelé juste avec ce nom de fichier, la commande ne sera pas exécutée mais le binaire sera affiché. Il faut donc trouver une solution pour placer un tube '|
' à la fin du nom, sans que ce '|
' ne soit utilisé dans la vérification provoquée par le "-e
". Nous connaissons déjà la solution : l'octet nul. Si nous transmettons "../../../bin/ls\0|
" comme nom, le test d'existence réussit car il ne portera que sur "../../../bin/ls
" mais le open()
verra bien le pipe et exécutera la commande. Ainsi, l'URI fournit le contenu du répertoire courant :
pipe2.cgi?filename=..%2F..%2F..%2Fbin%2Fls%00|
Le script finger.cgi
exécute l'instruction finger
sur notre machine :
#finger.cgi print "<BODY>"; $login = $input{'login'}; $login =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g; print "Login $login<BR>\n"; print "Finger<BR>\n"; $CMD= "/usr/bin/finger $login|"; open(FILE,"$CMD") || goto form; print <FILE>
Ce script prend (enfin) une mesure utile : il protège certains caractères étranges pour éviter qu'ils ne soient interprétés par un shell. On dit alors que ces caractères sont échappés. Ceci consiste simplement à placer un '\
' devant afin d'en empêcher l'interprétation. Ainsi, le point-virgule est transformé en "\;
par l'expression régulière. Mais la liste ne contient pas tous les caractères importants. Il manque, entre autre, le retour à la ligne '\n
.
Sur la ligne de commande de votre shell favori, vous validez votre instruction en tapant sur les touches RETURN
ou ENTER
, ce qui provoque l'envoi d'un caractère '\n
'. En Perl, le même mécanisme est possible. Nous avons déjà vu que l'instruction open()
nous laissait exécuter une commande sous réserve de terminer la ligne par un tube '|
'.
Pour simuler ce comportement, il nous suffit d'ajouter, après le login transmis à la commande finger, un retour-chariot puis l'instruction de notre choix :
finger.cgi?login=kmaster%0Acat%20/etc/passwd
Dans le même ordre d'idées, d'autres caractères sont également intéressants pour exécuter successivement plusieurs instructions :
&&
: si la première instruction réussit (i.e. renvoie 0 en shell), alors la suivante est exécutée ;||
: si la première instruction échoue (i.e. renvoie une valeur non nulle en shell), alors la suivante est exécutée.Le script finger.cgi
précédent prend des mesures pour éviter de se voir passer des caractères étranges. Ainsi, l'URI <finger.cgi?login=kmaster;cat%20/etc/passwd
ne fonctionne pas car le point-virgule est échappé. Cependant, il est un caractère qui n'est pas très souvent protégé : le backslash '\
'.
Par exemple, prenons le cas d'un script qui veut interdire de remonter dans une arborescence en utilisant le répertoire "..
", l'expression régulière s/\.\.//g
efface les "..
", mais ça ne sert à rien car les shells supportent parfaitement l'amoncellement de plusieurs '/
' (essayez cat ///etc//////passwd
pour vous en convaincre).
Par exemple, dans le script pipe2.cgi
ci-dessus, la variable $filename
est initialisée à partir du préfixe "/home/httpd/
". L'utilisation de l'expression régulière précédente pourrait sembler efficace pour empêcher de remonter dans les répertoires. Certes, cette expression protège de "..
", mais que se passe-t-il si on ruse un peu en protégeant nous-mêmes le caractère '.
' ? En effet, l'expression régulière ne correspond pas si le nom du fichier est .\./.\./etc/passwd
. Signalons que ceci fonctionne très bien avec system()
(ou les guillemets inverses ` ... `
), mais le open()
ou le "-e
" échoue.
Revenons donc maintenant au script finger.cgi
. Si on prend le cas du point-virgule, l'URI finger.cgi?login=kmaster;cat%20/etc/passwd
ne nous donne pas le résultat escompté car le point-virgule est échappé par l'expression régulière. Ceci signifie que le shell reçoit l'instruction :
/usr/bin/finger kmaster\;cat /etc/passwdLes erreurs suivantes sont inscrites dans les logs du serveur web :
finger: kmaster;cat: no such user. finger: /etc/passwd: no such user.Ces messages sont identiques à ceux obtenus en tapant directement cette ligne dans un shell. Le problème vient de ce que la protection mise sur le '
;
' fait apparaître ce caractère comme appartenant au nom "kmaster;cat
".
Or, nous cherchons ici à séparer les deux instructions, celle du script et celle qui nous intéresse. Il faut donc protéger nous-mêmes le ';
' : <A HREF="finger.cgi?login=kmaster\;cat%20/etc/passwd"> finger.cgi?login=kmaster\;cat%20/etc/passwd</A>
. La chaîne "\;
sera alors transformée par le script en "\\;
", qui sera ensuite transmise au shell. Celui-ci voit donc :
/usr/bin/finger kmaster\\;cat /etc/passwdLe shell décompose ceci en deux instructions :
/usr/bin/finger kmaster\
qui a de forte chance d'échouer ... mais ça n'est pas notre problème ;-)cat /etc/passwd
qui affiche le fichier de mots de passe.\
' doit également être échappé.
Parfois, le paramètre est "protégé" à l'aide de guillemets. Nous avons légèrement modifié le script finger.cgi
précédent pour protéger ainsi la variable $login
.
Toutefois, si les guillemets ne sont pas échappés, c'est comme si rien n'était fait. En effet, il suffit simplement d'en ajouter dans sa propre requête. Ainsi, le premier guillemet transmis ferme celui (ouvrant) du script. Ensuite, on place la commande de notre choix, puis un second guillemet qui ouvre le dernier guillemet (fermant) du script.
Le script finger2.cgi
illustre ceci :
#finger2.cgi print "<BODY>"; $login = $input{'login'}; $login =~ s/\0//g; $login =~ s/([<>\*\|`&\$!#\(\)\[\]\{\}:'\n])/\\$1/g; print "Login $login<BR>\n"; print "Finger<BR>\n"; #Nouvelle super protection (in)efficace $CMD= "/usr/bin/finger \"$login\"|"; open(FILE,"$CMD") || goto form; while(<FILE>) { print; }
L'URI qui permet alors d'exécuter notre commande devient :
finger2.cgi?login=kmaster%22%3Bcat%20%2Fetc%2Fpasswd%3B%22
/usr/bin/finger "$login";cat /etc/passwd""
où les guillemets ne sont plus un problème.
Il est donc important, si vous voulez protéger vos paramètres avec des guillemets, de les échapper, tout comme le point-virgule ou le backslash que nous avons déjà abordés.
Lorsque vous programmez en Perl, utilisez l'option w
ou "use warnings;
" (Perl 5.6.0 et ultérieurs), elle vous prévient de certains problèmes potentiels, comme des variables non initialisées ou des expressions/fonctions obsolètes.
L'option T
( taint mode de to taint, corrompre en Anglais) offre un niveau de sécurité supérieur. Ce mode déclenche plusieurs tests. Le plus intéressant concerne une éventuelle corruption des variables. Celles-ci sont soit propres, soit corrompues. Toutes les données qui proviennent de l'extérieur du programme sont considérées comme corrompues tant qu'elles n'ont pas été nettoyées. De même, toutes les variables issues de variables corrompues sont, elles aussi, dans cet état. Une telle variable ne peut alors affecter quoi que ce soit en dehors du programme.
En mode corruption, les arguments de la ligne de commande, les variables d'environnement, le résultat de certains appels-système (readdir()
, readlink()
, readdir()
, ...), les données provenant de fichiers sont considérés comme a priori suspects, et donc corrompus.
Pour nettoyer une variable, il suffit de la filtrer au travers d'une expression régulière. Bien évidemment, utiliser .*
ne sert à rien. Le but de cette opération est de vous forcer à prendre garde aux arguments qui vont sont fournis.
Cependant, ce mode ne protège pas de tout : la corruption des arguments passés sous forme de liste à system()
ou exec()
n'est pas vérifiée. Vous devez donc être particulièrement méticuleux si un de vos scripts utilise ces fonctions. L'instruction exec "sh", '-c', $arg;
est considérée comme sécurisée, que $arg
soit corrompu ou pas :(
Il est aussi fortement conseillé d'ajouter un "use strict;" au début de vos programmes. Cela oblige à déclarer vos variables, certains vont trouver ça pénible, mais c'est obligatoire si vous faites du mod-perl
.
Ainsi vos scripts CGI en Perl doivent commencer par :
#!/usr/bin/perl -wT use strict; use CGI;ou depuis Perl 5.6.0 par :
#!/usr/bin/perl -T use warnings; use strict; use CGI;
open()
De nombreux programmeurs ouvre un fichier simplement avec open(FILE,"$filename") || ...
. Nous avons déjà vu les risques liés à ceci. Pour les réduire, il suffit de préciser le mode d'ouverture :
open(FILE,"<$filename") || ...
pour la lecture seule ;open(FILE,">$filename") || ...
pour l'écriture seule ;Avant d'accéder à un fichier, il est conseillé de vérifier aussi que le fichier en question existe bien. Ceci ne protège pas des problèmes de condition de concurrence (race conditions) que nous avons étudiés dans le dernier article, mais permet d'éviter certains pièges comme les commandes avec des arguments.
if ( -e $filename ) { ... }
Depuis Perl 5.6, il existe une syntaxe supplémentaire pour open()
: open(FILEHANDLE,MODE,LIST)
. Avec le mode '<', le fichier est ouvert en lecture ; avec le mode '>' le fichier est tronqué, ou créé au besoin, puis ouvert en écriture. Cela devient intéressant pour les modes où l'on communique avec d'autres processus. Si le mode est '|-' ou '-|', l'argument LIST est interprété comme une commande et se trouve respectivement après ou avant le pipe.
Avant l'apparition de Perl 5.6 et du open()
à trois arguments, certaines personnes avaient recours à la commande sysopen()
.
Il y a deux méthodes: soit on spécifie les caractères interdits, soit on définit explicitement les caractères autorisés à l'aide d'expressions régulières. Les programmes mis en exemple devraient vous avoir convaincus qu'il est facile d'oublier de filtrer des caractères potentiellement dangereux, c'est pourquoi la seconde méthode est conseillée.
En pratique, voici ce que cela donne : on commence par vérifier que la requête ne comporte que des caractères autorisés. Ensuite, on échappe ceux considérés comme dangereux parmi ces caractères autorisés.
#!/usr/bin/perl -wT # filtre.pl # Les variables $safe et $danger définissent respectivement l'ensemble des # caractères sans risque et celui des caractères dangereux. # Il suffit d'en ajouter/enlever pour changer le filtre. # Seules les $input comportant des caractères inclus dans l'union de # ces ensembles sont valides. use strict; my $input = shift; my $safe = '\w\d'; my $danger = '&`\'\\|"*?~<>^(){}\$\n\r\[\]'; #Note: # Le '/', l'espace et la tabulation ne sont volontairement # dans aucun des deux ensembles if ($input =~ m/^[$safe$danger]+$/g) { $input =~ s/([$danger]+)/\\$1/g; } else { die "Bad input chars in $input\n"; } print "input = [$input]\n";
Ce script définit deux ensembles de caractères :
$safe
contient ceux considérés comme sans risque (ici, uniquement des chiffres et des lettres) ;$danger
contient les caractères à échapper car potentiellement dangereux.Sans vouloir déclencher une polémique, il est à mon sens souvent préférable de choisir d'écrire ses scripts en PHP plutôt qu'en Perl. Plus exactement, en tant qu'administrateur-système, je préfère autoriser mes utilisateurs à utiliser le langage PHP plutôt que le langage Perl. Une personne programmant mal - de façon non-sécurisée - en Perl le fera aussi en PHP. Alors pourquoi préférer le PHP ? Même si on retrouve certains problèmes de programmation en PHP, on peut activer le Safe mode (safe_mode=on
) ou bien désactiver certaines fonctions (disable_functions=...
). Ce mode empêche d'accéder aux fichiers n'appartenant pas à l'utilisateur, de modifier les variables d'environnement sauf autorisation explicite, d'exécuter des commandes, etc.
Par défaut, la bannière d'Apache nous renseigne sur la présence éventuelle de PHP.
$ telnet localhost 80 Trying 127.0.0.1... Connected to localhost.localdomain. Escape character is '^]'. HEAD / HTTP/1.0 HTTP/1.1 200 OK Date: Tue, 03 Apr 2001 11:22:41 GMT Server: Apache/1.3.14 (Unix) (Red-Hat/Linux) mod_ssl/2.7.1 OpenSSL/0.9.5a PHP/4.0.4pl1 mod_perl/1.24 Connection: close Content-Type: text/html Connection closed by foreign host.Il suffit d'un
expose_PHP = Off
dans /etc/php.ini
pour la masquer :
Server: Apache/1.3.14 (Unix) (Red-Hat/Linux) mod_ssl/2.7.1 OpenSSL/0.9.5a mod_perl/1.24
Le fichier /etc/php.ini
(PHP4) ou /etc/httpd/php3.ini
recèle de nombreux paramètres permettant de durcir le système. Par exemple, l'option "magic_quotes_gpc
" ajoute des quotes sur les arguments reçus par les méthodes GET
, POST
et via les cookies, cela élimine déjà un bon nombre de problèmes présentés dans nos exemples en Perl.
De tous les articles présentés, celui-ci est certainement le plus facilement compréhensible. Il montre des failles exploitables tous les jours sur le web. Il en existe d'autres, souvent liées à une mauvaise programmation (par exemple, un script qui envoie un mail, prenant en argument le champ From:
, offre un sympathique moyen de spam). Les exemples sont (trop) nombreux. A partir du moment où un script se retrouve sur un site web, il y a fort à parier qu'au moins une personne cherchera à l'exploiter de manière malveillante.
Il termine notre série sur le thème de la programmation sécurisée. Nous espérons vous avoir fait découvrir les principales failles de sécurité présentes dans de trop nombreuses applications, et surtout que cela vous incitera à bien prendre en compte le paramètre "sécurité" lors de la conception et la programmation de vos applications. Les problèmes de sécurité sont trop souvent négligés en raison de la portée limitée du développement (usage interne, installation sur un réseau privé, maquette temporaire, etc.). Et pourtant, même un module à diffusion très restreinte peut servir un jour de base à une application plus conséquente, pour laquelle les modifications ultérieures se révéleront largement plus coûteuses.
ISO 8859-1 | Caractère |
%00 | \0 (fin de chaîne) |
%0a | \n (retour chariot) |
%20 | espace |
%21 | ! |
%22 | " |
%23 | # |
%26 | & (ampersand) |
%2f | / |
%3b | ; |
%3c | < |
%3e | > |
man perlsec
: la page man de Perl sur la sécurité ;guestbook.cgi
bogué#!/usr/bin/perl -w # guestbook.cgi BEGIN { $ENV{PATH} = '/usr/bin:/bin' } delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; # Make %ENV safer =:-) print "Content-type: text/html\n\n"; print "<HTML>\n<HEAD><TITLE>Buggy Guestbook</TITLE></HEAD>\n"; &ReadParse(\%input); my $email= $input{email}; my $texte= $input{texte}; $texte =~ s/\n/<BR>/g; print "<BODY><A HREF=\"guestbook.html\"> GuestBook </A><BR><form action=\"$ENV{'SCRIPT_NAME'}\">\n Email: <input type=texte name=email><BR>\n Texte:<BR>\n<textarea name=\"texte\" rows=15 cols=70> </textarea><BR><input type=submit value=\"Go!\"> </form>\n"; print "</BODY>\n"; print "</HTML>"; open (FILE,">>guestbook.html") || die ("Cannot write\n"); print FILE "Email: $email<BR>\n"; print FILE "Texte: $texte<BR>\n"; print FILE "<HR>\n"; close(FILE); exit(0); sub ReadParse { my $in =shift; my ($i, $key, $val); my $in_first; my @in_second; # Read in text if ($ENV{'REQUEST_METHOD'} eq "GET") { $in_first = $ENV{'QUERY_STRING'}; } elsif ($ENV{'REQUEST_METHOD'} eq "POST") { read(STDIN,$in_first,$ENV{'CONTENT_LENGTH'}); }else{ die "ERROR: unknown request method\n"; } @in_second = split(/&/,$in_first); foreach $i (0 .. $#in_second) { # Convert plus's to spaces $in_second[$i] =~ s/\+/ /g; # Split into key and value. ($key, $val) = split(/=/,$in_second[$i],2); # Convert %XX from hex numbers to alphanumeric $key =~ s/%(..)/pack("c",hex($1))/ge; $val =~ s/%(..)/pack("c",hex($1))/ge; # Associate key and value $$in{$key} .= "\0" if (defined($$in{$key})); $$in{$key} .= $val; } return length($#in_second); }
|
Site Web maintenu par l´équipe d´édition LinuxFocus © Frédéric Raynal, Christophe Blaess, Christophe Grenier, FDL LinuxFocus.org Cliquez ici pour signaler une erreur ou envoyer un commentaire à Linuxfocus |
Translation information:
|
2001-10-25, generated by lfparser version 2.18