Original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier
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.
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); }