[LinuxFocus-icon]
Home  |  Plan  |  Index  |  Suchen

Nachrichten | Archiv | Links | Über uns
Dieses Dokument ist verfübar auf: English  Castellano  Deutsch  Francais  Nederlands  Portugues  Russian  Turkce  

convert to palmConvert to GutenPalm
or to PalmDoc

[image of the authors]
von Frédéric Raynal, Christophe Blaess, Christophe Grenier
<pappy(at)users.sourceforge.net, ccb(at)club-internet.fr, grenier(at)nef.esiea.fr>

Über den Autor:

Christophe Blaess ist selbständiger Aeronautik-Ingenieur. Er ist Linuxfan und erledigt einen großen Teil seiner Arbeit auf diesem System. Er koordiniert die Übersetzung der Man-Seiten, die vom Linux Documentation Project herausgegeben werden.

Christophe Grenier studiert im 5. Jahr an der ESIEA, wo er auch als Systemadministrator arbeitet. Er interessiert sich besonders für Computersicherheit.

Frédéric Raynal benutzt Linux seit vielen Jahren, weil es nicht mit Fetten verseucht ist, frei von künstlichen Hormonen und ohne BSE ist, es enthält nur den Schweiß ehrlicher Leute und einige Tricks.



Übersetzt ins Deutsche von:
Hermann J. Beckers <beckerst(at)lst-oneline.de>

Inhalt:

 

Vermeidung von Sicherheitslücken bei der Anwendungsentwicklung - Teil 6: CGI-Skripte

[article illustration]

Zusammenfassung:

Abruf einer Datei, Start eines Programmes durch ein schlecht programmiertes Perl-Skript. ... "Viele Wege führen zum Ziel!"

Bisherige Artikel in dieser Serie :



 

Web-Server, URIs und Konfigurationsprobleme

 

Eine (zu kurze) Einführung in die Arbeitsweise eines Webservers und zum Aufbau einer URL

Wenn ein Klient eine HTML-Datei anfordert, sendet der Server die gewünschte Seite (oder eine Fehlermeldung). Der Browser interpretiert die HTML-Anweisungen, um die Datei zu formatieren und anzuzeigen. Wenn zum Beispiel der http://www.linuxdoc.org/ HOWTO/HOWTO-INDEX/howtos.html
URL (Uniform Request Locator) eingegeben wird, nimmt der Klient Verbindung mit dem www.linuxdoc.org-Server auf und fordert die /HOWTO/HOWTO-INDEX/howtos.html-Seite mittels des HTTP-Protokolls an. Wenn die Seite existiert, sendet der Server die angeforderte Datei. In diesem statischen Modell wird die Datei, wenn sie auf dem Server vorhanden ist, "unverändert" an den Klient übertragen, andernfalls wird eine Fehlernachricht gesendet (das allzu gut bekannte 404 - Not Found).

Leider erlaubt dies keine Interaktivität mit der Benutzerin, daher sind Eigenschaften wie elektronische Geschäfte, ResErvierungen oder E-irgendwas sonst noch nicht möglich.

Glücklicherweise gibt es Lösungen, um HTML-Seiten dynamisch zu generieren. CGI (Common Gateway Interface)-Skripte gehören dazu. In diesem Fall ist der URL zum Zugriff auf diese Seite etwas anders aufgebaut:

http://<server><pathToScript>[?[param_1=val_1][...] [&param_n=val_n]]
Die Argumentliste wird in der Umgebungs-Variablen QUERY_STRING gespeichert. In diesem Zusammenhang ist ein CGI-Skript nichts anderes als eine ausführbare Datei. Es benutzt stdin (Standard-Eingabe) oder die Umgebungs-Variable QUERY_STRING zur Übernahme der Argumente. Nach Ausführung des Programmtextes wird das Ergebnis über stdout (Standard-Ausgabe) an den Web-Klient weitergeleitet. Fast jede Programmiersprache kann zur Erstellung eines CGI-Skriptes benutzt werden (kompilierte C-Programme, Perl, Shell-Skripte...).

Lassen Sie uns z. B. einmal herausfinden, was die HOWTOs von www.linuxdoc.org über ssh wissen:

http://www.linuxdoc.org/cgi-bin/ldpsrch.cgi? svr=http%3A%2F%2Fwww.linuxdoc.org& srch=ssh&db=1&scope=0&rpt=20
Tatsächlich ist es einfacher als es aussieht. Wir analysieren den URL:

Oft sind Argumentnamen und Werte eindeutig genug, um ihre Bedeutung zu verstehen. Außerdem ist der Inhalt der Seite mit den Antworten von größerer Bedeutung.

Nun wissen Sie, dass das schöne an CGI-Skripten die Möglichkeit für den Benutzer ist, Argumente zu übergeben ... der Nachteil ist aber, das ein schlecht geschriebenes Skript eine Sicherheitslücke öffnen kann.

Sie haben wahrscheinlich die seltsamen Zeichen bemerkt, die Ihr bevorzugter Browser benutzt oder die im Beispiel benutzt wurden. Diese Zeichen sind im URI-encoded-Format. Die Tabelle 1 stellt einige Zeichen mit ihrer Bedeutung dar. Einige IIS4.0- und IIS5.0-Server weisen Sicherheitslücken auf, die auf diesen Zeichen basieren.

 

Apache-Konfiguration mit "SSI Server Side Include"

Server Side Include ist eine Funktion des Webservers. Diese erlaubt es, Anweisungen in Webseiten zu integrieren, entweder zum Einfügen einer Datei oder zur Ausführung eines Befehls (Shell- oder CGI-Skript).

In der Apache-Konfigurations-Datei httpd.conf aktiviert die "AddHandler server-parsed .shtml" -Anweisung diesen Mechanismus. Um eine Unterscheidung zwischen .html und .shtml zu vermeiden, wird oftmals die .html-Erweiterung hinzugefügt. Das verlangsamt natürlich den Server ... Auf Verzeichnisebene kann dies mit folgenden Instruktionen kontrolliert werden:



Im angehängten guestbook.cgi -Skript wird der von der Benutzerin eingegebene Text in eine HTML-Datei eingebunden, ohne die Zeichen '<' und ' >' in die &lt;- und &gt;-HTML-Kodierungen zu wandeln. Das kann jemand dazu anregen, folgende Instruktionen einzugeben:

Im ersten Beispiel
guestbook.cgi?email=pappy&texte=%3c%21--%23printenv%20--%3e
erhalten Sie einige Informationszeilen über das System:
DOCUMENT_ROOT=/home/web/sites/www8080
HTTP_ACCEPT=image/gif, 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/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 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



Die exec-Anweisung stellt Ihnen fast eine Shell-Umgebung bereit:



guestbook.cgi?email=ppy&texte=%3c%21--%23exec%20cmd="cat%20/etc/passwd"%20--%3e

Versuchen Sie nicht "<!--#include file="/etc/passwd"-->", die Pfadangabe ist relativ zum Verzeichnis der HTML-Datei und darf kein ".." enthalten. Die Apache-error_log-Datei enthält dann eine Nachricht, die einen Zugriffsversuch auf eine gesperrte Datei anzeigt. Der Benutzer sieht die Nachricht [an error occurred while processing this directive] in der HTML-Seite.

SSI werden oft gar nicht benötigt und dann ist es besser, sie auf dem Webserver zu deaktivieren. Die Ursache des Problems ist jedoch die Kombination der fehlerhaften Gästebuch-Anwendung und dem SSI .

 

Perl-Skripte

In diesem Abschnitt präsentieren wir Sicherheitslücken von in Perl geschriebenen CGI-Skripten. Um die Übersicht zu wahren, zeigen wir nicht den vollständigen Programmtext, sondern nur die zum Problemverständnis erforderlichen Teile.

Alle Skripte sind nach folgendem Muster aufgebaut:

#!/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);
# now use $input e.g like this:
# print "<p>$input{filename}</p>\n";
# #################################### #
# Start of problem description         #
# #################################### #



# ################################## #
# End of problem description         #
# ################################## #

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);

# first arg must be a reference to a hash.
# The hash will be filled with data.
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
    # \0 is the multiple separator
    $$in{$key} .= "\0" if (defined($$in{$key}));
    $$in{$key} .= $val;

  }
  return length($#in_second);
}

Die an Perl übergebenen Argumente (-wT) besprechen wir später. Wir beginnen mit der Bereinigung der $ENV und $PATH-Umgebungsvariablen und wir senden die HTML-Kopfzeilen (das ist Teil des html-Protokolls zwischen Browser und Server, Sie sehen dies nicht in der angezeigten Web-Seite). Die ReadParse()-Funktion liest die dem Skript übergebenen Argumente. Dies kann mit Modulen einfacher gestaltet werden, aber auf diese Weise sehen Sie die gesamten Anweisungen. Als nächstes präsentieren wir die Beispiele. Dann enden wir mit der HTML-Datei.

 

Das Null-Byte

Perl behandelt jedes Zeichen gleich, wodurch es sich z. B. von C-Funktionen unterscheidet. Für Perl ist das Null-Zeichen als Ende einer Zeichenkette ein Zeichen wie jedes andere. Wo liegt das Problem?

Wir fügen die folgenden Anweisungen zu unserem Skript hinzu und erhalten showhtml.cgi  :

  # showhtml.cgi
  my $filename= $input{filename}.".html";
  print "<BODY>File : $filename<BR>";
  if (-e $filename) {
      open(FILE,"$filename") || goto form;
      print <FILE>;
  }


Die ReadParse()-Funktion erhält als einziges Argument den Namen der anzuzeigenden Datei. Um jemanden mit "schlechten Manieren" davon abzuhalten, etwas anderes als HTML-Dateien zu lesen, hängen wir die ".html"-Erweiterung an das Ende des Dateinamens. Aber denken Sie daran, dass das Null-Byte ein Zeichen wie jedes andere ist ...

Wenn unsere Anforderung showhtml.cgi?filename=%2Fetc%2Fpasswd%00 ist, heisst die Datei my $filename = "/etc/passwd\0.html" und unsere verwunderten Augen schauen auf etwas, was nicht HTML ist.

Was passiert? Der strace -Befehl zeigt, wie Perl eine Datei öffnet:

  /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


Der letzte open()-Befehl, der von strace angezeigt wird, korrespondiert mit dem entsprechenden Systemaufruf in C. Wir sehen, dass die .html-Erweiterung verschwindet und dies erlaubt es uns, die Datei /etc/passwd zu öffnen.

Dieses Problem lässt sich mit einem regulären Ausdruck lösen, der alle Null-Bytes entfernt: :

s/\0//g;


 

Benutzung von Pipes

Nun folgt ein Skript ohne jeden Schutz. Es zeigt eine Datei aus dem Verzeichnisbaum /home/httpd/ an:

#pipe1.cgi

my $filename= "/home/httpd/".$input{filename};
print "<BODY>File : $filename<BR>";
open(FILE,"$filename") || goto form;
print <FILE>;


Lachen Sie nicht über dieses Beispiel! Ich habe schon solche Skrispte gesehen.

Die erste Ausnutzung ist offensichtlich:

pipe1.cgi?filename=..%2F..%2F..%2Fetc%2Fpasswd
Dies ist ausreichend, um innerhalb des Verzeichnisbaumes auf jede Datei zuzugreifen. Es gibt aber eine noch viel interessantere Möglichkeit: Sie können einen beliebigen Befehl ausführen. In Perl öffnet der open(FILE, "/bin/ls")-Befehl die Binär-Datei "/bin/ls" ... aber open(FILE, "/bin/ls |") führt den angegebenen Befehl aus. Das Hinzufügen eines einzelnen Pipe-(|)-Symbols ändert das Verhalten von open().

Ein weiteres Problem rührt daher, dass nicht auf die Existenz der Datei getestet wird. Das erlaubt es, nicht nur jeden Befehl auszuführen, sondern auch beliebige Argumente zu übergeben: : pipe1.cgi?filename=..%2F..%2F..%2Fbin%2Fcat%20%2fetc%2fpasswd%20| zeigt den Inhalt der Passwort-Datei an.

Die Überprüfung auf das Vorhandensein der Datei schränkt die Möglichkeiten ein:

#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";
}
Das vorherige Beispiel klappt nicht mehr. Der "-e" Test versagt, weil die "../../../bin/cat /etc/passwd |"-Datei nicht gefunden wird.

Nun versuchen wir den /bin/ls-Befehl. Das Verhalten wird gleich sein. Wenn wir z. B. versuchen, den Inhalt des /etc-Verzeichnisses anzuzeigen, testet der "-e"-Test auf das Vorhandensein von "../../../bin/ls /etc |" , aber das gibt es ja auch nicht. Solange wir nicht den Namen einer "Phantom"-Datei angeben, werden wir nichts Interessantes zu sehen bekommen :(

Es gibt jedoch immer noch eine "Flucht-Möglichkeit", auch wenn die Ergebnisse nicht so gut sind. Die /bin/ls-Datei existiert (jedenfalls in den meisten Systemen), aber wenn open() mit diesem Dateinamen aufgerufen wird, wird nicht der Befehl ausgeführt, sondern es wird das Programm angezeigt. Wir müssen eine Möglichkeit finden, um ein Pipe-(|)-Zeichen an das Ende des Namens zu setzen, ohne dass es der Prüfung durch "-e" unterzogen wird. Wir kennen die Lösung bereits: Das Null-Byte. Wenn wir "../../../bin/ls\0|" als Name übergeben, ist der Existenz-Test erfolgreich, weil nur "../../../bin/ls" getestet wird, aber open() erkennt die Pipe und führt dann den Befehl aus. Deshalb lautet der URL zum Anzeigen des aktuellen Verzeichnisinhalts:

pipe2.cgi?filename=../../../bin/ls%00|
 

Zeilenschaltungen

Das finger.cgi-Skript führt den finger-Befehl auf unserer Maschine aus:

#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>


Dieses Skript benutzt (endlich) eine nützliche Massnahme: Es kümmert sich um einige seltsame Zeichen, um deren Interpretation durch eine Shell zu verhinden, indem es ihnen ein '\' voransetzt. So wird das Semikolon durch den regulären Ausdruck in "\;" umgewandelt. Aber die Liste enthält nicht jedes wichtige Zeichen. Neben anderen fehlt die Zeilenschaltung '\n'.

In der Shell wird ein Befehl validiert, indem die RETURN oder ENTER-Taste gedrückt wird, wodurch das '\n'-Zeichen geschickt wird. In Perl können sie das gleiche tun. Wir haben bereits gesehen, dass es die open()-Anweisung erlaubt, einen Befehl auszuführen, wenn die Zeile mit dem Pipe-'|'-Zeichen endet.

Um dieses Verhalten zu simulieren, reicht es, eine Zeilenschaltung und eine Anweisung nach dem Login an den finger-Befehl zu senden:

finger.cgi?login=kmaster%0Acat%20/etc/passwd


Um mehrere Anweisungen nacheinander auszuführen, sind auch folgende Sonderzeichen von Bedeutung:



Sie haben hier keine Auswirkung, weil sie durch den regulären Ausdruck maskiert werden. Aber lassen Sie uns nach einem Ausweg dafür suchen.  

Backslash und Semikolon

Das finger.cgi-Skript vermeidet Probleme mit einigen seltsamen Zeichen. So funktioniert der URL <finger.cgi?login=kmaster;cat%20/etc/passwd nicht, weil das Semikolon maskiert ist. Ein Zeichen ist jedoch nicht geschützt: der backslash '\'.

Lassen Sie uns als Beispiel ein Skript nehmen, das uns durch den regulären Ausdruck s/\.\.//g daran hindert, den Verzeichnisbaum hinaufzusteigen, indem ".." gelöscht wird. Es bewirkt nichts! Shells können mehrere '/' auf einmal verarbeiten (versuchen Sie einfach cat ///etc//////passwd: überzeugt? ).

Als Beispiel wird im obigen pipe2.cgi-Skript die $filename-Variable durch den "/home/httpd/"-Prefix. initialisiert. Die Benutzung des vorherigen regulären Ausdrucks scheint ausreichend zu sein, um Verzeichniswechsel zu verhinden. Natürlich schützt dieser Ausdruck vor "..", aber was passiert, wenn wir das '.'-Zeichen schützen? Der reguläre Ausdruck trifft nicht auf den Dateinamen .\./.\./etc/passwd zu. Wie wollen erwähnen, das dies sehr gut mit system() oder ( ` ... `) klappt, aber open() oder "-e" versagen.

Wir gehen nun zurück zum finger.cgi-Skript. Unter Benutzung des Semikolons ergibt der finger.cgi?login=kmaster;cat%20/etc/passwd-URL nicht das erwartete Resultat, weil das Semikolon durch den regulären Ausdruck maskiert wird. Die Shell erhält die Anweisung:

/usr/bin/finger kmaster\;cat /etc/passwd
Folgende Fehlermeldungen finden sich in den Log-Dateien des Webservers:
finger: kmaster;cat: no such user.
finger: /etc/passwd: no such user.
Diese Meldungen gleichen denen, die erzeugt werden, wenn diese Zeile in einer Shell eingegeben wird. Das Problem ist, dass das geschützte ';' als Bestandteil der Zeichenkette "kmaster;cat" angesehen wird.

Wir wollen beide Anweisungen trennen, d. h. die aus dem Skript und diejenige, die wir benutzen wollen. Wir müssen daher das ';' schützen : <A HREF="finger.cgi?login=kmaster\;cat%20/etc/passwd"> finger.cgi?login=kmaster\;cat%20/etc/passwd</A>. Die "\;"-Zeichenkette wird dann durch das Skript in "\\;" gewandelt und an die Shell gesendet. Es ergibt sich:

/usr/bin/finger kmaster\\;cat /etc/passwd
Die Shell teilt das in 2 Anweisungen auf:
  1. /usr/bin/finger kmaster\ was wohl fehlschlägt ... aber uns nicht interessiert ;-)
  2. cat /etc/passwd wodurch die Passwort-Datei angezeigt wird.
Die Lösung ist einfach: Der backslash '\' muss ebenfalls maskiert werden.

 

Benutzung eines ungeschützten "-Zeichens

Manchmal wird der Parameter durch Anführungszeichen "geschützt". Wir haben das vorherige finger.cgi-Skript leicht abgewandelt, um die $login -Variable auf diese Weise zu schützen.

Wenn die Anführungszeichen jedoch nicht maskiert sind, ist dies nutzlos. Sie müssen nur ein Anführungszeichen in Ihrer Anforderung hinzufügen. So schliesst das erste Anführungszeichen das öffnende aus dem Skript. Dann geben Sie die Anweisung ein und ein zweites Anführungszeichen, welches das letzte (schliessende) Anführungszeichen aus dem Skript öffnet.

Das finger2.cgi -Skript illustriert dies:

#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";
#New (in)efficient super protection :
$CMD= "/usr/bin/finger \"$login\"|";
open(FILE,"$CMD") || goto form;
while(<FILE>) {
  print;
}


Der auszuführende URL wird dann:

finger2.cgi?login=kmaster%22%3Bcat%20%2Fetc%2Fpasswd%3B%22
Die Shell erhält die Anweisung /usr/bin/finger "$login";cat /etc/passwd"" und die Anführungszeichen sind kein Problem mehr.

Wenn Sie die Parameter mittels Anführungszeichen schützen wollen, ist es daher wichtig, sie wie die bereits erwähnten Zeichen Semikolon und backslash zu maskieren.

 

Skripte in Perl schreiben

 

Warning- und tainting-Optionen

Wenn Sie in Perl programmieren, sollten Sie die w-Option oder "use warnings;" (Perl ab 5.6.0) benutzen. Dadurch erhalten Sie Informationen über mögliche Probleme wie uninitialiserte Variablen oder überholte Ausdrücke/Funktionen.

Die T-Option ( taint mode/Ma(e)kel-Modus) bietet zusätzliche Sicherheit. Dieser Modus aktiviert verschiedene Tests. Der wichtigste ist eine mögliche Markierung von Variablen als makelhaft. Variablen sind entweder sauber oder makelhaft. Daten von ausserhalb des Programms werden als makelhaft angesehen, solange sie nicht bereinigt wurden. Eine so befleckte Variable kann keine Werte an Sachen zuweisen, die ausserhalb des Programms benutzt werden (Aufrufe anderer Shell-Befehle).

Im taint-Modus werden die Befehlszeilenargumente, die Umgebungsvariablen, einige Systemaufruf-Ergebnisse (readdir(), readlink(), readdir(), ...) und die Daten aus Dateien als verdächtig und damit als makelhaft angesehen.

Um eine Variable zu bereinigen, muss sie durch einen regulären Ausdruck gefiltert werden. Die Benutzung von .* ist natürlich nutzlos. Das Ziel ist es, Sie dazu zu bringen, sich um die übergebenen Argumente zu kümmern. Spezifizieren Sie die regulären Ausdrücke so genau wie irgend möglich..

Trotzdem schützt dies nicht vor allen Fallen: Die Reinheit von Argumenten, die als Listen-Variablen an system() oder exec() übergeben werden, wird nicht geprüft. Sie müssen also besonders vorsichtig sein, wenn Ihre Skripte diese Funktionen benutzen. Die exec "sh", '-c', $arg; -Anweisungen werden als sicher angesehen unabhängig davon, ob $arg bereinigt ist oder nicht :(

Es wird ausserdem empfohlen, "use strict;" an den Beginn Ihrer Skripte zu setzen. Dies zwingt Sie dazu, Variablen zu deklarieren; einige werden es als lästig empfinden, aber es ist auf jeden Fall erforderlich, wenn Sie mod-perl benutzen.

Daher müssen Ihre Perl-CGI-Skripte wie folgt anfangen:

#!/usr/bin/perl -wT
use strict;
use CGI;
oder ab Perl 5.6.0 :
#!/usr/bin/perl -T
use warnings;
use strict;
use CGI;


 

Aufruf von open()

Viele Programmierer/innen öffnen Dateien einfach durch open(FILE,"$filename") || .... Wir haben die Risiken dieser Anweisungen bereits gesehen. Zur Reduzierung des Risikos reicht es, den Öffnungs-Modus anzugeben:

Öffnen Sie Ihre Dateien also nie in einer unspezifizierten Weise.

Vor dem Zugriff auf eine Datei sollten Sie deren Existenz überprüfen. Dies verhindert keine der im letzten Artikel besprochenen "race condiions", vermeidet aber einige Fallen wie Befehle mit Argumenten.

if ( -e $filename ) { ... }

Seit Perl 5.6 gibt es eine neue Syntax für open() : open(FILEHANDLE,MODE,LIST). Mit dem '<'-Modus wird die Datei zum Lesen geöffnet, mit dem '>';-Modus wird sie auf 0 Bytes gestutzt oder neu angelegt und zum Schreiben geöffnet. Dies ist interessant für Modi, die mit anderen Prozessen kommunizieren. Wenn der Modus '|-' oder '-|' ist, wird das LIST-Argument als Befehl interpretiert und entsprechend vor oder nach der Pipe eingesetzt.

Vor Perl 5.6 und open() mit drei Argumenten haben einige auch den sysopen()-Befehl benutzt.

 

Eingabe-Maskierung und -Filterung

Es gibt zwei Methoden: entweder spezifizieren Sie die verbotenen Zeichen oder Sie definieren ausdrücklich die zulässigen Zeichen durch reguläre Ausdrücke. Die Beispielprogramme sollten Sie davon überzeugt haben, dass es sehr leicht passiert, beim Filtern einige möglicherweise gefährlichen Zeichen zu vergessen; deshalb wird die zweite Methode empfohlen.

Was sie also tun müssen: zuerst überprüfen Sie, ob die Anforderung nur die erlaubten Zeichen enthält. Als nächstes werden von den zugelassenen Zeichen diejenigen maskiert, die als gefährlich angesehen werden.

#!/usr/bin/perl -wT

# filtre.pl

#  The $safe and $danger variables respectively define
#  the characters without risk and the risky ones.
#  Enough to add/remove some to change the filter.
#  Only $input containing characters included in the
#  definitions are valid.

use strict;

my $input = shift;

my $safe = '\w\d';
my $danger = '&`\'\\|"*?~<>^(){}\$\n\r\[\]';
#Note:
#  '/', space and tab are not part of the definitions on purpose


if ($input =~ m/^[$safe$danger]+$/g) {
    $input =~ s/([$danger]+)/\\$1/g;
} else {
    die "Bad input chars in $input\n";
}
print "input = [$input]\n";


Dieses Skript definiert zwei Zeichen-Sätze:

Jede Anforderung, die ein Zeichen enthält, welches nicht in einem der beiden Zeichensätze enthalten ist, wird sofort zurückgewiesen.

 

PHP-Skripte

Ich möchte keine Kontroverse auslösen, aber ich denke, es ist besser, Skripte in PHP als in Perl zu schreiben. Genauer, als Systemadministrator ziehe ich es vor, wenn meine Benutzer/innen ihre Skripte in PHP anstatt Perl schreiben. Wer in der falschen Art und Weise programmiert - nicht sicherheitsbewußt - ist in PHP genauso gefährlich wie in Perl. Warum bevorzuge ich dann PHP? Bei Programmierproblemen in PHP können Sie den Safe-Modus (safe_mode=on) aktivieren oder Funktionen deaktivieren (disable_functions=...). Dieser Modus verhindert Zugriffe auf Dateien, die dem Benutzer nicht gehören, verhindert das Ändern von Umgebungsvariablen (sofern nicht ausdrücklich erlaubt), das Ausführen von Programmen usw.

Standardmäßig informiert der Apache-Banner uns über die benutztte PHP-Version.

$ 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.
Es reicht, expose_PHP = Off in die /etc/php.ini zu schreiben, um diese Information zu unterdrücken:
Server: Apache/1.3.14 (Unix)  (Red-Hat/Linux) mod_ssl/2.7.1
OpenSSL/0.9.5a mod_perl/1.24


Die /etc/php.ini-Datei (PHP4) oder /etc/httpd/php3.ini kann viele Parameter enthalten, mit denen das System gesichert werden kann. Zum Beispiel fügt die "magic_quotes_gpc"-Option Anführungszeichen zu den Argumenten hinzu, die über die GET, POST-Methoden und per Cookie erhalten werden; dies vermeidet eine Anzahl der Probleme aus unseren Perl-Beispielen.

 

Schlussfolgerung

Von den Artikeln in dieser Serie ist dieser wahrscheinlich am leichtesten zu verstehen. Er zeigt Schwachstellen auf, die täglich im Web ausgenutzt werden können. Es gibt viele andere Möglichkeiten, die oft auf schlechte Programmierung zurückzuführen sind (z. B. ein Skript, welches Mail versendet und als Argument das From:-Feld akzeptiert, ist ein großer Anreiz für Massen-Mail-Versender). Es gibt (zu)viele Beispiele. Sobald sich ein Skript auf einer Webseite findet, können Sie darauf wetten, das irgendjemand versucht, es auf die falsche Art zu benutzen.

Dieser Artikel beendet die Serie über sichere Programmierurng. Wir hoffen, dass wir Ihnen geholfen haben, die größten Sicherheitslöcher in zu vielen Anwendungen zu entdecken und dass Sie den "Sicherheits"-Parameter berücksichtigen, wenn Sie Ihre Anwendungen gestalten und programmieren. Sicherheitsprobleme werden zu oft vernachlässigt wegen dem begrenzten Einsatzbereich (interne Benutzung, private Netzwerk-Nutzung, temporäres Modell usw.). Aber auch ein ursprünglich nur für einen sehr begrenzten Zweck vorgesehenes Modul kann die Basis für eine viel größere Anwendung werden und dann notwendig werdende Änderungen werden sehr viel teurer sein.


 

Einige Unicode-Zeichen

Unicode Zeichen
%00 \0 (end of string)
%0a \n (carriage return)
%20 space
%21 !
%22 "
%23 #
%26 & (ampersand)
%2f /
%3b ;
%3c <
%3e >
Tab 1 : Unicode und Zeichen-Beziehung

 

Links


 

Das fehlerhafte guestbook.cgi Program

#!/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);
}


 

Talkback für diesen Artikel

Jeder Artikel hat seine eigene Seite für Kommentare und Rückmeldungen. Auf dieser Seite kann jeder eigene Kommentare abgeben und die Kommentare anderer Leser sehen:
 Talkback Seite 

Der LinuxFocus Redaktion schreiben
© Frédéric Raynal, Christophe Blaess, Christophe Grenier, FDL
LinuxFocus.org

Einen Fehler melden oder einen Kommentar an LinuxFocus schicken
Autoren und Übersetzer:
fr --> -- : Frédéric Raynal, Christophe Blaess, Christophe Grenier <pappy(at)users.sourceforge.net, ccb(at)club-internet.fr, grenier(at)nef.esiea.fr>
fr --> en: Georges Tarbouriech <georges.t(at)linuxfocus.org>
en --> de: Hermann J. Beckers <beckerst(at)lst-oneline.de>

2001-10-24, generated by lfparser version 2.19