Sesam schließe dich!

Sichere CGI-Programmierung in Perl

Immer mehr Webseiten benutzen öffentliche und eigene CGI-Skripte. Viele dieser Programme werden jedoch nur im Aspekt der Funktionalität und nicht mit Bedacht auf mögliche Sicherheitsprobleme programmiert. Somit werden immer mehr Seiten aufgrund ihrer Skripte Opfer von potentiellen Crackern, für die diese ein gefundes und beliebtes Fressen sind, da sie sich einige Stunden sinnloser Scans etc. sparen. Dieser Artikel soll dabei helfen, zunächst einmal eine Vorstellung davon zu kriegen, wodurch Sicherheitslücken entstehen und wie man sie vermeidet.

Das Problem bei unsicheren CGI-Skripten ist vor allem die Tatsache, dass kein Firewall vor einem Mißbrauch schützen kann, da ein externes Nutzen möglich sein muss. Deshalb werden sie bei sogenannten Crackern immer beliebter, da die Ausnutzung relativ simpel ist. Genutzt werden die Schwachstellen zum Einen zum Ausspionieren von Daten und zum Anderen zum sogenannten Defacen der Webseite, worunter man einen neu entdeckten Kindersport versteht, bei dem es darum geht die Webseite zu verunstalten und eine Art Gangartiges "Graffiti" zu hinterlassen.

Die einzige Möglichkeit des Schutzes besteht darin, die einzelnen Skripte auf mögliche Sicherheitsrisiken zu überprüfen und diese gegebenenfalls zu entfernen beziehungsweise zu limitieren. Hierfür muss man jedoch zunächst wissen, wo überhaupt potentielle Sicherheitsrisiken vorhanden sind. Die Gefahren, die durch ein unsicheres CGI-Skript entstehen, lassen sich in 2 Kategorien einteilen: zum Einen besteht die Gefahr, lokale Dateien auszulesen und anzeigen zu lassen und zum Anderen kann es möglich sein, Befehle auf dem Webserver auszuführen. Dieses erfolgt mit den Rechten des HTTP-Dämons (z.B. Apache), die meist mit Nobody- aber auch teilweise mit Rootrechten laufen. Somit wäre es schonmal ratsam den HTTP-Dämon nicht mit Rootrechten auszustatten.

Um eine kleine Vorstellung von den Schwachstellen in CGI-Skripten zu kriegen folgt nun ein kleines Beispiel anhand des Diskussionsforums Ikonboard (Version <= 2.1.7b) Dieses Forum hat eine gravierende Sicherheitslücke in seinem Hilfe-Skript, die dadurch verursacht wird, dass die Programmierer nicht daran gedacht haben, den Backslash bzw. den Punkt zu filtern. In den folgenden Zeilen wird eine Datei geöffnet, die zuvor durch den User indirekt bestimmt wurde. Zusätzlich wird ihr ein '.dat' angehängt. Das Problem ist hierbei, dass der User irgendeinen Namen angeben kann und durch die fehlende Filterung des Backslashs das Skript dazu bringen kann, die '.dat'-Erweiterung bei der Interpretation zu ignorieren.

$filetoopen = "$ikondir" . "help/$inhelpon.dat";
$filetoopen = &stripMETA($filetoopen);
open (FILE, "$filetoopen") or die "Cannot locate the required files";

Wenn der User jetzt einfach als Wert für $inhelpon zum Beispiel '../../etc/passwd\0' angibt, so zeigt das Hilfeskript des Ikonboard den Inhalt der Passwortdatei an. Erklärung: Die Datei kann frei gewählt werden und ihr wird lediglich eine '.dat'-Erweiterung angehängt, welche es eigentlich unmöglich machen sollte, beliebige Dateien zu öffnen. Hängt man nun aber ein '\0', einen sogenannten "Poison NULL Byte" an den Dateinamen, so bewirkt dieser dass die Passwortdatei geöffnet wird, da jegliche Zeichen nach dem NULL Byte bei der übergabe an Systemfunktionen ignoriert werden. Weitere Informationen zum Ikonboard Problem finden sie auf http://www.codito.de/text/ikonboard.html.

Ich werde im Laufe des Artikels die Einzelheiten dieser Problematik erläutern und somit wird spätestens zu Ende auch der Fehler des obigen Beispiels deutlich, sofern er es noch nicht ist. Die Gefahren entstehen sobald vom User angebene Variablen in das Skript eingebunden werden, seien dies nun Gästebucheinträge, Suchbegriffen, etc. Der sogenannte "Unexpected Input" (unerwartete Eingabe), kann bewirken, dass das Skript Dateien öffnet bzw. Befehle ausführt, für die es nicht gedacht war.

Beispiele von unsicheren Anwendungen von Perlfunktionen
Wie im obigen Beispiel bereits gezeigt wurde, entstehen die Sicherheitsprobleme sobald Eingaben vom User übernommen werden. Sei dies nun bei Dateien die ausgelesen werden, E-Mails die verschickt werden, etc. Bei den meisten Funktionen erfordert es nur minimalen Aufwand um sie zu sichern, so z.B. die open()-Funktion. Das Problem hierbei ist, dass open() die Eingabe als Befehl ausführt, sobald diesem eine Pipe voran oder hinterher gestellt ist (so würde zum Beispiel '| mail muench@gmc-online.de < /etc/passwd' eine Kopie der Passwortdatei an meine E-Mailadresse schicken). Die sichere Verwendung sollte anhand folgender Beispiele relativ schnell deutlich werden:

open(DATEI, "$eingabe"); # Unsicher
open(DATEI, ">$eingabe"); # Sicher
open(DATEI, ">>$eingabe"); # Sicher
open(DATEI, "<$eingabe"); # Sicher
...

Wie man oben sehen kann wird die Gefahr einer Penetration durch simples Hinzufügen von 1-2 Zeichen für diesen Teil eliminitert. Bei der system()-Funktion ist es fast genauso simpel die gefährlichen Eingaben unschädlich zu machen. Hierbei dürfte relativ klar sein, dass bei Eingabe der Pipe oder des Semikolons und eines weiteren Kommandos dieses auch ausgeführt werden würde (z.B. 'user | rm -rf /' ).

system("/bin/echo $eingabe"); # Unsicher
system("/bin/echo", $eingabe); # Sicher

Dadurch ist es relativ unsicher, bei einem CGI-Skript welches unter anderen für den Versand von E-Mails genutzt wird, die system()-Funktion zu benutzen. Zwar nicht so einfach, dafür aber sicherer wäre es, hierfür die open()-Funktion zum interaktiven Ausführen von Shellkommandos zu benutzen. Hierbei wäre eine Gefahr bei den Usereingaben nicht vorhanden, da diese nicht direkt an die Shell sondern erst innerhalb des Programms angewendet werden. Somit wäre folgende Anwendungen für den Mailversand relativ sicher und somit vorteilhaft:

open(MAIL, "|/usr/lib/sendmail -t");
print MAIL "To: $empfaenger\n";
...

Man sollte nach Möglichkeit die Benutzung der Shell vermeiden um kein unnötiges Sicherheitsrisiko einzugehen. Zwar sind die oben beschrieben Methoden relativ sicher, dennoch ist es weitaus einfacher einfach sämtliche Usereingaben zu filtern und somit unerwünschte Eingaben zu unterbinden.

Unexpected Input
Unter "Unexpected Input" versteht man Eingaben vom User, die nicht erwartet wurden, so wie im obigen Beispiel die Passwortdatei kombiniert mit dem Poison NULL Byte. Ziel der sicheren Programmierung ist es, nicht erwünschte beziehungsweise unerwartete Eingaben zu unterbinden und somit eine Ausnutzung der Skripte zur Penetration zu vermeiden. Hierfür ist es erforderlich, jegliche vom User bestimmten Eingaben zu überprüfen und nach bestimmten Kriterien zu filtern. So kann man zum Beispiel die unerwünschten Zeichen wie zum Beispiel Metacharaktere aus der Eingabe herauszulöschen. Bei dieser Variante werden die vom User eingegebenen Werte durch einen Filter geschickt, der aus diesen die unerwünschten Zeichen herauslöscht. Ein Beispiel für einen solchen Filter wäre folgender:

$eingabe =~ s/%(..)/pack("c", hex($1))/ge; # Umwandeln der Hex-Werte in Ascii-Zeichen
$eingabe =~ s/[([;<>\*\|&\$!#\(\)\[\]\{\}:'"\\]//g; # Entfernen der Metacharaktere

Hier wird zunächst die Eingabe auf Hex-Werte überprüft, die dann ggf. in Ascii-Zeichen umgewandelt werden. Danach werden sämtliche Metacharaktere, die in der obigen Liste stehen, aus der Eingabe entfernt. Natürlich ist diese Methode ziemlich sicher, jedoch nur wenn man wirklich alle Sonderzeichen entfernt, die einge mögliche Gefahr darstellen könnten. Im obigen Beispiel des Ikonboards wurde der Backslash bei den Filterregeln vergessen und somit war das gesamte Skript ein potentielles Sicherheitsrisiko. Diese Filtermethode ist zwar effektiv, dennoch ist sie gleichzeitig viel zu kompliziert, da es viel einfacher geht. Natürlich gibt es Leute, die ihren Code so kompliziert wie möglich gestalten wollen, jedoch sind es meist deren Skripte, die bestimmte Sonderzeichen vergessen zu filtern. Eine weitaus einfachere und übersichtlichere Methode wäre es, einfach bestimmte Zeichen zu bestimmen, die bei der Eingabe erlaubt sind. Bei diesem Weg werden einfach alle Zeichen, die nicht in der Liste der Erlaubten stehen entfernt und somit kann es nicht dazu kommen, dass bestimmte unerwünschte Zeichen beim Filtern vergessen werden. Die Anwendung dieses Filters sollte folgendes Beispiel relativ einfach zeigen:

$ZEICHEN='-a-zA-Z0-9';  # Festlegen der erlaubten Zeichen
$user_data=~s/[^$ZEICHEN]//go;   # Entfernen der unerwünschten Zeichen

In der ersten Zeile werden wie erlaubten Zeichen festgelegt. Wenn man nun z.B. eine Telefonnummer per CGI-Skript empfangen will, so reicht es wenn man lediglich die Zahlen 0-9 erlaubt. Bei E-Mail Adressen müsste man zudem noch Groß- und Kleinbuchstaben und das '@' erlauben. Grundsätzlich gilt es, alle Eingaben die vom User bestimmt werden genauestens zu filtern um mögliche Risiken zu vermeiden.

Alamieren - Ignorieren?
Jetzt wo die Eingaben effektiv gefiltert werden bleibt immernoch die Frage, ob man den User beim Verstoss gegen die Filterregeln alarmieren sollte und ihn darüber aufklären sollte, oder ob man die unerwünschten Zeichen still und leise löscht, so dass der User nichts davon erfährt. Beide Varianten haben gewisse Vor- und Nachteile und sollten deshalb gut überlegt sein. Wenn ein Skript zum Beispiel bei jedem Verstoss gegen die Filterregeln eine Warnmeldung anzeigt ist es relativ leicht möglich, das Skript auch ohne vorhandenen Quellcode zu analysieren, indem man einfach sämtliche brauchbaren Sonderzeichen ausprobiert. Des einen Vorteil ist des anderen Nachteil, so wäre es sehr unvorteilhaft wenn bei einem Skript über das man ein Passwort setzen/ändern kann keine Alarmierung stattfinden würde und einige Zeichen still und heimlich entfernt werden würden. Wieder einige Stunden Spaß für Support-Hotline. Generell kann man nicht genau sagen, wann es sich lohnt zu alamieren und wann nicht, da es Situationsabhängig ist. Man muss sich hier immer fragen ob es wirklich notwendig und sinnvoll ist, den User über seine unkorrekte Eingabe zu informieren. Auf jeden Fall sollte man jeden Verstoss gegen die Filterregeln loggen um sich hinterher einen Überblick verschaffen zu können. Somit kann man im nachhinein erkennen, ob jemand probiert hat das CGI-Skript zu exploiten, sich einfach nur vertippt hat oder sogar eigentlich benötigte Zeichen gefiltert werden. So kann man sein Skript nach und nach den Eingaben anpassen und auf die Eingaben in einer bestimmten Art und Weise reagieren.

Sicherheitsfunktion
Bei Perl gibt es eine integrierte Sicherheitsfunktion namens "Taint". Diese ist bei der Programmierung sehr hilfreich, da sie einen Abbruch des Skriptes bewirkt, sobald ungeprüfte und unsichere Eingaben an Systemkommandos wie z.B. bind, chown, syscall, system, etc.übergeben werden. Aktiviert wird diese Funktion durch Verwendung von '-T' (#!/usr/bin/perl -T). Ich werde diese Funktion nur kurz erläutern, da sie in der Manpage PERLSEC(1) (man perlsec) sehr gut beschrieben wird. Diese Funktion prüft generell ob die einzelnen Komponenten vor allem im Bereich der Shell richtig angewendet wurden. So z.B. auch ob Pfade festgelegt wurden, etc. wie man an folgendem Beispiel sehen kann:

system("echo $eingabe"); # Verboten
system("echo", $eingabe); # Verboten
system("/bin/echo", $eingabe); # Erlaubt

Vor allem für CGI-Skripte ist diese Funktion sehr empfehlenswert, da selbst bei fehlendem oder falschem Filter die Gefahr einer Penetration sehr gering ist. Gerade für unerfahrene CGI-Programmierern ist diese Funktion sehr nützlich, da eine unsichere Programmierung nahezu unmöglich ist.

Sourcescanner
Vor allem bei öffentlichen Skripten ist es relativ mühsam diese auf ihre Sicherheit zu überprüfen, da man sich erst in den fremden Code einarbeiten muss. Hierfür gibt es sogenannte Sourcescanner, die den Quellcode auf mögliche Gefahren untersuchen und diese dann auflisten. Diese Probleme sind meist nur gefährlich, wenn nicht korrekt gefiltert wird. So zeigt er z.B. eine unsichere Verwendung der open()-Funktion an:

open(DATEI, $eingabe) || die "Fehler: $_";
close(DATEI);

Die Problematik dieser Anwendung wurde bereits erläutert, somit sollte klar sein welche Gefahr sie darstellt. Solche unsicheren Funktionen fallen zwar nur bei fehlerhafter Anwendung beziehungsweise bei fehlen eines Filters ins Gewicht, jedoch sollte man sie steht's überprüfen und umgehen. Ein Programm was hierbei recht hilfreich ist, ist der sogenannte Sourcescanner, zu finden unter: http://www.codito.de/prog/sourcescanner.pl.txt. Er ist in der Lage C sowie Perl Quellcodes auf bestimmte Schwachstellen und häufig vorkommende Sicherheitsprobleme zu untersuchen.

Fazit
Werden sämtliche Usereingaben richtig und fehlerfrei gefiltert besteht keine große Gefahr einer Penetration des Skriptes, da es bei der Umsetzung der Angaben nicht zu unerwarteten Eingaben kommen kann. Die Sicherung der Skripte ist so einfach und schnell umgesetzt, dass es wirklich unverständlich ist, wieso manche Leute nicht einfach ihre Codes dicht machen. CGI-Programmierer die bei ihren Skripten mit Usereingaben arbeiten und auf Filter und den Taintmodus komplett verzichten gehören verbannt. Ich denke das wichtigste bei dieser Problematik ist es, zunächst erstmal zu realisieren, dass es eine gibt.

Martin J. Münch <mjm(-@-)codito.de>
http://www.codito.de


Links

1.The World Wide Web Security FAQ -
http://www.perl.com/pub/doc/FAQs/cgi/www-security-faq.html

2.CGI Security Tutorial
http://www.pi.infn.it/html/cgisecdef.html

3.CGI Security
http://www.lanl.gov/projects/ia/library/bits/bits0396.html

4.Safe CGI Programming
http://www-no.ucsd.edu/security/safe-cgi.txt

5.Sourcescanner
http://www.codito.de/prog.html

6.Liste einiger verwundbarer CGI-Skripte
http://www.codito.de/text.html

7.How To Remove Meta-characters From User-Supplied Data In CGI Skripts
http://www.cert.org/tech_tips/cgi_metacharacters.html

Literatur

[1] Hack Proofing Your Network
Syngress Media;
ISBN: 1928994156

Dieses Buch hat ein extra Kapitel zum Thema 'Unexpected Input', welches sehr empfehlenswert wert. Ansonsten wird die allgemeine Sicherheitsproblematik von Netzwerken, Betriebssystemen, etc. behandelt.