Directory Traversal kurz erklärt

Directory Traversal ist eine der offensichtlichsten Sicherheitslücken, erschreckend einfach in der Anwendung, aber genauso effizient. Anders als andere Probleme, insbesondere (D)DoS, ist hier aber immer der Programmierer der Applikation schuld. Das heißt, die einfachste und effizienteste Form der Abwehr besteht einfach darin, keine derartigen Lücken zu erzeugen. Hier gibt es nämlich durchaus absolute Sicherheit: indem man das Problem einfach behebt, anders als zum Beispiel bei SYN-Flood, wo man nur Gegenmaßnahmen treffen kann, das Problem aber nicht elimieren.

Das heißt jedoch noch lange nicht, dass es dieses Problem nicht mehr gibt. Grund genug, sich ein wenig damit zu befassen. Auch weil der nebenstehende Auszug der Full Disclosure Mailingliste (noch immer) einen erheblichen Bedarf daran zeigt.

Grundlagen

Directory Traversal zählt zu den einfachen Sicherheitslücken, die es ohne größere Schwierigkeiten erlauben, Informationen freizugeben, die nicht für den Besucher vorgesehen sind, beispielsweise Konfigurationsdateien, Passwörter, Logindaten oder in extremen Fällen sogar Einschleusung von beliebigem Code erlauben.

Die Lücke kommt dadurch zustande, dass Programmierer ein einfaches Templatekonzept umsetzen möchten, da Frames in den letzten Jahren verpönt und obsolet wurden. Directory Traversal ist also eine Sicherheitslücke, die (beinahe) ausschließlich Web-Anwendungen betrifft.
Es ist ja auch so eine Qual, mit diesen modernen Webseiten. Früher, als die Welt noch besser und gute Dinge aus Holz waren, navigierte man noch über die Browsertasten (Vorwärts/Zurück) über Links auf Webseiten hinweg. Doch heute ist damit natürlich niemand mehr zufrieden, jede Webseite braucht ein Corporate Layout und eine Navigationsleiste. Nun, eine einfache Möglichkeit dies zu realisieren sind eben genau eine jene Programmierunarten, die direkt zum Problem führen: serverseitige Inkludierungen.

Natürlich könnte man so eine Navigationsleiste und zugehöriges Layout statisch für jede Inhaltsseite manuell anlegen. Das ergibt jedoch in der Praxis das Problem, dass man die entsprechenden Teile der Webseite auf jeder Seite einzeln ändern muss, anstelle einer zentralen Stelle. Der Vorteil liegt also auf der Hand: ein Programmierer dann auf die Idee kommen, Dateien wie im folgenden beschrieben anzulegen.

Mein Beispiel basiert auf PHP, identische Probleme gibt es aber in jeder beliebigen Skriptsprache, einschließlich Perl, Python und Ruby. Das Problem liegt nicht an der Programmiersprache (auch wenn PHP hier unter Umständen noch viel katastrophalere Lücken öffnet, dazu später), sondern am Programmierer. Das Szenario funktioniert so: Der Programmierer soll eine gewöhnliche Webseite mit Header, Footer und Navigationsleiste erstellen und tut dies vermutlich, indem er folgende Dateien anlegt, um damit eine - sehr einfache und spartanisch, weil layoutfreie - Webseite umzusetzen. Wie das dann aussieht, zeigt der Screenshot.

home:

Hallo, Herzlich Willkommen bei Foo Corp!

produkte:

Unsere Produkte

index.php:

<html>
<h1>Foo Corp</h1>
< ?php 
if ($dh = opendir('.')) 
{
  while (($file = readdir($dh)) !== false)
    if (!preg_match("/\./",$file))
      echo "&raquo; <a href=\"" . $_SERVER['PHP_SELF'] . "?p=$file\">$file ";
  closedir($dh);
}
?>
<br /> 
<br />
<div>
< ?php
$page = ($_GET['p']) ? $_GET['p'] : 'home';
include($page);
?>
</div>
<center>2009 Foo Corp</center>
</html>

Es passiert also nicht viel, außer, dass bei Aufruf der Seite im Browser die Datei index.php aufgerufen wird, die ein einfaches Header/Footer Template generiert und mitten drin die einzelnen navigierbaren Inhalte (home, produkte, …) einbindet. So weit nicht ungewöhnlich.

Das Problem ist die Art und Weise, wie eben diese navigierbaren Inhalte inkludiert werden. Es passiert einfach durch die PHP Funktion include(”filename”). Diese liest schlicht die angegebene Datei (hier: filename) ein, und gibt den Inhalt an den Browser aus. Welche Seite inkludiert eingelesen werden soll bestimmt eben der Besucher, indem er über die generierten Links navigiert.

Die Angreifersicht

Der fatale Teil ist folgender Code:

$page = ($_GET['p']) ? $_GET['p'] : 'home';
include($page);

Solange man die Intention des Programmierers verfolgt, funktioniert diese Codestelle wunderbar. Standardmäßig wird die Seite “home” ausgegeben, ansonsten eben die, die der Besucher anfordert. Ruft man zum Beispiel die Seite /index.php?p=produkte im Browser auf, passiert eigentlich folgendes: Der Code sucht nach einer Datei “produkte” und gibt diese, falls sie existiert, im Browser aus.

Nun werden auf der Homepage zwar nur valide Hyperlinks generiert, nichts und niemand hindert den Besucher jedoch daran, eine eigene Seite anzugeben, die anstelle der vorhergesehenen Seiten inkludiert werden soll. Mit das wichtigste Paradigma in der Applikationsprogrammierung ist: vertraue niemals dem Benutzer. So können zum Beispiel Konfigurationsdateien, Passwortdateien und dergleichen, die innerhalb oder außerhalb des Webverzeichnisses angelegt sind. Das Problem liegt nun darin, dass tatsächlich jede beliebige Datei aus dem Dateisystem abgerufen werden kann, zum Beispiel auch mit vollständigen Pfadangaben. Nebenstehendes Bild zeigt eben dies - die Ausgabe der Unix-Passwortdatei /etc/passwd, die ohne nennenswerte Schwierigkeit für den Angreifer zugänglich ist.
Das ist für sich nicht besonders schlimm, darin stehen auf modernen Linux-Systemen schließlich nicht wirklich Passwörter. Problematisch ist das aber, weil dies eine bedeutende “Information Disclosure” Lücke ist, ein Angreifer darüber als bedeutende Informationen über das Zielsystem erhalten kann, insbesondere welche Benutzer und Shells es gibt.

Eine besonders unangenehme Eigenart von PHP ist hier nicht die Tatsache, dass diese Skriptsprache auch noch “URL-Includes” erlaubt. Das bedeutet, dass nicht nur lokale Dateien auf dem ausgeführten Server eingebunden werden können, sondern noch schlimmer - entfernte. Man kann also schlicht und ergreifend URLs eingeben, die dann inkludiert und evaluiert (das heißt: ausgeführt) werden. Das macht aus der Code Traversal Lücke eine Code Injection Lücke - um Längen dramatischer.

Das bereits oben gelinkte PHP-Handbuch sagt hierzu:

If “URL fopen wrappers” are enabled in PHP (which they are in the default configuration), you can specify the file to be included using a URL (via HTTP or other supported wrapper - see List of Supported Protocols/Wrappers for a list of protocols) instead of a local pathname.

Das ermöglicht dem Angreifer PHP-Code einzuschleusen, der auf dem Server ausgeführt wird. Die Möglichkeiten sind damit unbegrenzt, von da an ist es für den Angreifer eine triviale Sache sich eine Shell auf dem Server zu öffnen oder beliebigen anderen Schadcode (Löschroutinen, Spamming, Spoofing, was auch immer) auszuführen. Dies funktioniert genauso recht und billig, wie lokale Inkludierungen, das “Hello World” im Bild wurde zum Beispiel so eingeschleust. Dagegen gibt es jedoch eine einfache Abhilfe, wie weiter unten beschrieben ist.

Eine einfache Unvorsichtigkeit kann also dramatische Folgen haben, Zeit sich mit der Abwehr solcher Angriffe zu beschäftigen.

Directory Traversal abwehren

Eigentlich gibt es keine “Abwehr” von Directory Traversal Problemen. Es gibt nur eine Lösung und damit kann man dieses Problem gänzlich aus der Welt schaffen: sauber programmieren. Nun programmiert aber beileibe nicht jeder seine Anwendungen selbst, oder hat die technische Kompetenz dazu. Deshalb verwendet man in der Regel viel Software, die von externen Personen geschrieben wurde und denen man bei solchen Sachen leider vertrauen muss. Egal ob Forum, Weblog, Gästebuch oder CMS - Unmengen von Code landet auf einem Webserver, der vermutlich von anderen Autoren stammt - man kann hier aber trotzdem vorbeugende Maßnahmen schaffen, um Code Traversals zumindest zu erschweren oder deren Auswirkungen zumindest zu minimieren.

Zumindest Code Injections, wie sie PHP leider fatalerweise ermöglicht (für die mir im übrigen nicht eine einzige positive Anwendung einfällt, die URL-Handler für include() also sinn- und nutzlos macht) kann man aber einfach Abhilfe schaffen. Dazu einfach in der php.ini-Konfigurationsdatei auf dem Webserver folgende Änderung vornehmen:

allow_url_include = Off

Und schon funktioniert ein und der selbe Trick plötzlich nicht mehr und der Angreifer scheitert mit seiner Code-Injection, wie man nebenstehend sieht. Auf Debian und Ubuntu Linux-Distributionen befindet sich diese Konfigurationsdatei in /etc/php5/apache2/php.ini. Ansonsten offenbart den Pfad dazu auch die PHP-Funktion phpinfo().

Als nächsten Schritt könnte man vielleicht auf die Idee kommen, die gefährlichen Zeilen Code ein wenig zu entschärfen, indem man nur lokale Dateien zum einbinden erlaubt. Zum Beispiel in dieser Form, die vor die angeforderte Seite einfach ein Pfadpräfix “./” setzt:

include("./" . $page);

Sieht doch gleich besser aus, der bisherige Exploit funktioniert zu unserer Zufriedenheit nicht mehr. Doch diese Sicherheit ist trügerisch. Das Problem ist wieder die Art und Weise, wie hier Pfadangaben im Dateisystem landen - nämlich weiter ungefiltert. Dies ist also keine Lösung, sie macht die Ausnutzung der Unachtsamkeit nur unbedeutend schwieriger.

Anstatt absoluter Referenzierung (”/etc/passwd”) kann nämlich relative verwendet werden und daran ändert auch das “./”-Präfix nichts. Es ist nämlich völlig korrekt und legitim einen Pfad wie folgt zu referenzieren: ./../../../etc/passwd. Entsprechend ist die Lücke auch weiter auszunutzen - wie das Bild zeigt.

Die schlechte Nachricht ist, dagegen kann man nichts im besonderen tun. Es ist ein architektonischer Fehler eine Problemlösung dazu in PHP zu suchen. Trotzdem existiert in PHP im speziellen dafür der sogenannte Safe Mode - auch wenn der demnächst entfernt wird. Auf PHP-Ebene wird mit dieser Methode verboten auf Dateien unterhalb eines bestimmten Pfades zuzugreifen. Wieder ist hierfür eine Änderung dafür in PHP an der zugehörigen php.ini nötig:

safe_mode=On
open_basedir="/var/www"

Insbesondere ältere Applikationen haben aber mitunter Probleme mit Skripten, die im Safe Mode laufen. Dies ist einerseits eine spezifische Sache für PHP, andererseits wie gesagt architektonisch an der falschen Stelle umgesetzt. Wesentlich klüger ist übergeordnet im Webserver oder dem Betriebssystem darunter.

Selbst dann - und das kann man auf diese Art und Weise nicht verhindern - sind aber noch Konfigurationsdateien, zum Beispiel Apache .htaccess-Dateien oder Einstellungen von Webapplikationen zugänglich. Es ist dies also auch keine Lösung. Die allerbeste Methode besteht also darin, die Anfrage zu filtern und nur zulässige Zeichen zuzulassen. Eine ganz gute Methode ist die folgende, die einfach alle Pfadseperatoren aus der Anfrage filtert und den damit beträchtlich sicherer macht:

$page = preg_replace("/\//", "", $page);

Das macht es schwieriger, aber noch immer nicht unmöglich, dass Informationen durch einen Angreifer aufgedeckt werden, die für diesen nicht bestimmt sind. Der Filter verlässt sich nämlich komplett auf eine “Blacklist”, eine Stoppliste von Zeichen, die im Pfad nicht vorkommen dürfen, nämlich das Pfadtrennzeichen “/“. Das bringt jedoch eine ganze Menge Seiteneffekte, die der Autor vermutlich nicht bedacht hat. Auf unixoiden Betriebssystem dürfte dieser Filter ausreichend sein. Aber ideal ist das noch immer nicht, landet dieses Skript beispielsweise auf einem Windows, ist der Filter plötzlich obsolet.

Viel besser und die einzige garantiert sichere Methode ist eine Whitelist, zum Beispiel eine Liste mit erlaubten URIs. Diese ist viel einfacher als vermutet - und dieser Code ist im Gegensatz zum anderen sicher:

    if (!preg_match("/\./",$file))
    {
      $goodlink[$file] = $file;
      echo "&raquo; <a href=\"" . $_SERVER['PHP_SELF'] . "?p=$file\">$file</a> ";
    }
 
// weiter unten ...
 
$page = (isset($goodlink[$_GET['p']])) ? $goodlink[$_GET['p']] : $goodlink['home'];

Beim einlesen wird also eine Hashmap (ein assoziatives Array) von erlaubten Links erzeugt, die gleichnamig mit dem Schlüssel ist, visuell dargestellt so:

home => home
produkte => produkte

Beim abrufen der Seite wird das Name-Wert-Paar also gegen sich selbst verifiziert. Nur wenn diese Übereinstimmung existiert (sprich: der Benutzer einen validen, nicht manipulierten Link anfordert) wird die Seite ausgeliefert. Das ist die einzige sichere, und verlässliche Methode eine Datei-Inklusion sicher zu programmieren, wenn man schon unbedingt sowas programmieren muss und will.

Leider gibt es wie gesagt Fälle, wo man nicht eigenen Quellcode auf Servern benutzt, also nicht den Überblick haben kann, ob Inklusionen überall sauber gemacht werden. Man kann sich also nur darauf verlassen, dass es keine derartigen Probleme gibt. Aber wenn doch, hat man die selben Probleme - man sollte also versuchen, den Schaden zu minimieren, den ein Directory Traversal anrichten kann. Das erreicht man, indem man Schutzschichten zwischen dem Browser im Klienten und der root-Shell auf dem Server errichtet. Das bedeutet, dass man die Integrität der Webapplikation unter Umständen nicht sichern kann, wohl aber die Systemebene darunter. Man kann also im Extremfall, wenn die Applikation fehlerhaft ist, damit leben, dass diese “gehackt” wird - unter der Voraussetzung, dass aber ansonsten kein Schaden am System angerichtet wird.

Das erreicht man durch Privilegienseperation und Verstecken von Ressourcen, die der Webserver nicht benötigt. Natürlich, der Webserver kann und muss sein Webverzeichnis lesen (und womöglich schreiben) können. Nicht aber Systemdateien. Der PHP Safe Mode ist eine (dumme) Idee der Umsetzung dieses Paradigmas. Eine sichere, aber schwierig umzusetzende (weil schwer wartbare) Lösung ist ein “chroot Jail” für den Webserver. Der Webserver (Apache, Lighttpd) sieht dann nur noch jene Daten die ihn direkt betreffen, ohne den kompletten Systemoverhead.

Dies umzusetzen ist nicht ganz einfach. Grundsätzlich rekonstruiert man in einer Blase ein kleines minimales Linux-System für den Daemon (also die Binaries, Libraries, Konfigurationsdateien und dergleichen umfassend, die Apache und die Applikation benötigt) und sperrt diesen darin ein. Das ist der generische Ansatz, der immer funktioniert, aber einerseits nur sehr schwer bis gar nicht wartbar und andererseits ziemlich verwirrend ist. Manche Daemons unterstützen diese Einkapselung von Haus aus - zum Beispiel der Lighttpd Webserver, wo demnach ein einfaches

server.chroot = "/whatever"

in der Konfigurationsdatei reicht. Zusammen mit FastCGI (zur Unterstützung von PHP, Python oder was auch immer) kommt man allerdings nicht drum herum für diese Binaries, die im Chroot selbst laufen die nötigen Bibliotheken bereit zu halten.

Für den populärsten der Webserver: Apache geht das nicht direkt von Haus aus, zusammen mit dem von mir dringend empfohlenen mod_security aber ebenso problemlos mit folgender Direktive:

SecChrootDir /whatever

Sogar mit dem Vorteil, dass hier kein umständliches Bibliothekgefrickel nötig wird, wenn man das richtig macht. Das ganze wird aber etwas (ok, viel) komplizierter, wenn manche zusätzlichen Module geladen werden, die Dokumentation gibt dazu weitere Tipps (oder hier).

Die weitaus modernste und sauberste Lösung wären aber komplexe und sehr flexible Sicherheitsprofile, wie sie SELinux und AppArmor bieten. Manche Distributionen wie RHEL tun genau dies bereits und verbieten Skripten den Zugriff außerhalb des /var/www-Verzeichnisses. Skripten laufen dort im Kontext “httpd_sys_script_t”, die eben diese Einschränkung hat. Die Umsetzung auf Distributionen, die (noch) nicht von vornherein eines dieser beiden Frameworks unterstützen und Profile mitliefern, müssen hier manuell Hand anlegen, was sehr komplex werden kann und daher hier nicht weiter darauf eingegangen wird.

Kategorien

Eingeordnet unter: , , ,

Verwandte Artikel

3 Antworten auf »Directory Traversal kurz erklärt«

  1. Wie immer - sehr gut erklärt.

    Allerdings möchte ich anmerken, dass ein deaktiviertes allow_url_include zwar RFIs unmöglich macht, Code-Injections über diese Schwachstelle in einigen Fällen jedoch nach wie vor möglich sind - dann, wenn der Angreifer Einfluss auf eine Datei nehmen kann, welche PHP auslesen darf. Hat der Angreifer z.B. die Möglichkeit, Bilder hochzuladen, dann kann er PHP-Code als Bild-Kommentar einschleusen und durch diese Schwachstelle zur Ausführung bringen.

    Weiterhin sei im Bezug auf die Beispiele am Rande bemerkt: `echo /* … */ $_SERVER['PHP_SELF'] /* … */` ist im Bezug auf XSS problematisch, da PHP_SELF (zumindest per Default) die vom Benutzer beeinflussbare PATH_INFO enthält. Das ist aber nicht Thema des Artikels…

    Kugelfisch - 31. März 2009 um 15:24

  2. Hier sollte noch gesagt sein, dass das Dateisystem mit den Benutzerrechten von Haus aus eine gute Möglichkeit zur Zugriffsbeschränkung bietet.
    Hierzu muss nur verhindert werden, dass Apache als wwwrun auf die Verzeichnisse zugreift.

    michi7x7 - 3. Juni 2009 um 15:54

  3. Das ist allerdings auf manche Dateien nicht möglich. Gerade auf manche Systemdateien benötigt jeder Benutzer Leserechte, /etc/passwd ist ein Beispiel hierfür. Das ist im übrigen auch der Grund, dass halbwegs moderne Linux-Distributionen das Passwort selbst nicht mehr da ablegen, sondern in /etc/shadow.

    Arno - 3. Juni 2009 um 23:46

Einen Kommentar schreiben