Sicherheit für Websites

Aus eigener Erfahrung weiß ich, dass selbst in hochwertigen Anwendungen immer wieder sehr "billige" Sicherheitslücken eingebaut sind, da die Programmierer nicht die Zeit haben, sich mit den Sicherheitsaspekten von PHP zu beschäftigen. Dieser Artikel soll helfen, das Sicherheitsbewusstsein zu stärken.

Dieser Artikel ist schon einige Jahre alt und daher nicht mehr aktuell. Er behandelt auch nur Grundlagen - viele Details zur sicheren Programmierung (mit PHP) werden hier nicht erläutert. Das OWASP PHP Security Cheat Sheet und das OWASP PHP Configuration Cheat Sheet (beide englisch) behandeln viele weitere wichtige Grundlagen.

Vorbereitung

Worum gehts?

Der Untertitel dieses Artikels könnte lauten: "PHP/MySQL-Anwendungen hacken", da im Folgenden typische Angriffe auf eben solche Applikationen beschrieben werden. Mein Hauptaugenmerk möchte ich aber auf deren Verhinderung legen. Aus eigener Erfahrung weiß ich, dass selbst in Business-Anwendungen immer wieder sehr "billige" Sicherheitslücken eingebaut sind, da die Programmierer nicht die Zeit haben, sich mit den Sicherheitsaspekten von PHP zu beschäftigen. Aber auch in dem Code erfahrener Entwickler tauchen zwischen schönen Templatesystem und übersichtlichen aber funktionalen Klassenstrukturen immer wieder böse Zeilen auf, die einem Angreifer Tür und Tor öffnen.

Einführung

Die beschriebenen Attacken lassen sich - bei einer fehlerhaften Implementierung - auf diversen Systemkonfigurationen ausführen. In den Ausführungen wird von PHP in Verbindung mit MySQL ausgegangen, eine häufig anzutreffende Konfiguration.

Generell ein guter Anlaufspunkt bei PHP-Fragen und auch bei Sicherheitsfragen im Speziellen ist die PHP-FAQ.

GET und POST

Diese beiden sog. Methoden stellen Wege zur Verfügung, auf denen der Browser diverse Daten an eine Website übermitteln kann. Vom Programmierer werden hier meist wenige Werte vorgesehen, in Wahrheit steht es dem Browser und damit dem Benutzer, der ihn kontrolliert, aber völlig frei, welche Daten er übermittelt. Zur Manipulation der gesendeten Daten reicht es bei der GET-Methode, die URL zu verändern; aus http://www.example.com/index.php?name=Peter&nummer=15 kann ein Benutzer ohne besondere Fertigkeiten innerhalb von Sekunden http://www.example.com/index.php?name=&nummer=TEXT machen und damit evtl. schon eine Sicherheitslücke ausnutzen.

Es ist wichtig zu wissen, dass in einer URL nicht alle Zeichen erlaubt sind. Wir gehen daher davon aus, dass der verwendete Browser Sonderzeichen - sofern nötig - automatisch in ihren Zeichencode umwandelt. Um die Lesbarkeit zu verbessern werde ich daher im Folgenden auf diese Zeichencodes verzichten. Bei Bedarf kann man unter Stichworten wie "URL encoding" bei Google entsprechende Informationen finden.

JavaScript-Witze

Bilderschutz

Eigentlich gehört das, was jetzt kommt, nicht wirklich hier hin. Es soll mehr als warnendes Beispiel dienen, wie wenig der Webseitenersteller den Browser eines Benutzers wirklich unter Kontrolle hat und verdeutlichen, dass JavaScript sich nicht für Sicherheitsrelevante Dinge einsetzen lässt.

Es gibt Seiten im Netz, auf denen darf man keinen Rechtsklick einsetzen. Entweder der Browser reagiert einfach nicht auf den Klick oder es kommt ein Fenster was soviel sagt wie "Rechtsklick verboten!", "Alle Bilder (c) 2022 by ME!" oder ähnliches. Damit will der Ersteller der Seite verhindern, dass man Bilder mit Rechtsklick -> "Speichern unter" auf der lokalen Festplatte abspeichert.

Was dabei jedoch übersehen wird: In dem Moment, in dem ich das Bild sehe, ist es bereits auf meiner Festplatte. Nämlich im Cache-Verzeichnis des Browsers. Außerdem muss das Bild ja nach wie vor abrufbar sein, es muss eine URL haben und damit ist es auch direkt erreichbar; Meist reicht es, das Bild einfach in die Adressleiste des Browsers zu ziehen und es erscheint komplett ungeschützt im Fenster. Auch das liesse sich verhindern, aber in allen Browser kann man auch JavaScript ausschalten.

Im Firefox gibt es eine ganz besonders einfache Möglichkeit, alle Bilder bequem abzuspeichern: Extras, Seiteninformationen, Medien. Schwupps sieht der Benutzer alle Bilder und kann sie bequem abspeichern.

Wer an ein Bild ran will, kriegt es auch!

Diese Art von Pseudo-Schutz ist nur ein Ärgernis für Benutzer, die mit dem Rechtsklick navigieren (entweder Rechtsklick -> zurück oder mit Mausgesten). Sie zeugt von Inkompetenz seitens des Erbauers der Page.

Formularschutz

Oftmals werden Formulare im Web von JavaScript auf deren Validität, also das korrekte Ausfüllen durch den Benutzer geprüft. Das ist sinnvoll, weil man so sofort nach Klick auf den Absenden-Button ein Feedback erhält, ob man nichts vergessen hat.

Niemals auf JavaScript vertrauen!

Aber diese Prüfung darf keinesfalls die einzige sein, bevor die Daten z.B. von PHP-Code verarbeitet werden. Es muss immer zusätzlich in PHP oder mit vergleichbaren Mitteln serverseitig geprüft werden! Denn wie oben schon gilt: JavaScript kann ausgeschaltet sein oder man kann auf Browser treffen, die gar kein JavaScript können.

JavaScript-Zugriffsschutz

Du willst alleine mit JavaScript eine Passwortabfrage für eine Seite erstellen? Vergiss es, keine Chance. Auch wenn das richtige Passwort noch so kompliziert "verschlüsselt" im Code auftaucht, es ist leicht rauszufinden. Nur .htaccess oder sichere Lösungen in serverseitigen Sprachen erfüllen hier ihren Zweck. Alles andere ist sicherheitstechnische Homöopathie.

Mailformulare missbrauchen

Die zweitbekannteste Anwendung des Internets ist nach dem World Wide Web wohl E-Mail. Und beide tauchen dort gerne zusammen auf, wo der Seitenersteller sich z.B. ein Feedback von seinen Benutzern erhofft oder Dienste wie "Seite weiterempfehlen" angeboten werden. (Meiner Meinung nach eine der überflüssigsten Funktionen überhaupt, aber egal.)

Im Grunde stellen diese Formulare damit meistens nur eine einfache Schnittstelle zum mail-Befehl von PHP dar. Um die dabei oft entstehende Sicherheitslücke zu kennen, muss man ein bisschen über Header bei E-Mails wissen:

E-Mail-Header bestehen, wie man leicht z.B. im Thunderbird E-Mail-Client mit Strg+U oder in Outlook-Express mit Strg+F3 nachschauen kann, aus mehreren Zeilen, die jeweils mit einem Schlüsselwort anfangen und durch Absatzzeichen getrennt sind, z.B. To: frosch@see.de
From: info@example.com
Subject: Ich bin eine Betreffzeile

Die Formulare bieten in der Regel eine Möglichkeit, dass der Absender seine eigene Adresse eintippt und die Formularinhalte unter seinem Namen irgendwohin geschickt werden.

In PHP sieht das dann z.B. so aus: mail($to, $subject, $text, "From: ".$from);

Dabei kommen $to, $subject, $text und $from direkt aus dem Formular. Im mail-Befehl kann man als viertes Argument beliebige Header angeben. Der Entwickler hat also an dieser Stelle einen From-Header, gefüllt mit der Eingabe des Benutzers, vorgesehen.

Ein Angreifer könnte sich dies folgendermaßen zunutze machen: Er sendet für $from den Wert (ohne Anführungszeichen) "test@example.com\n\rBcc: spam-empfaenger@example.com". Wobei \n und \r hier natürlich für die Zeichen mit den ASCII-Codes 13 und 10 stehen. Zusammen ergeben sie einen Zeilenumbruch (nach Windows-Art).

Damit sorgt er dafür, dass an der Stelle, an der eigentlich nur ein From-Header stehen sollte, nun auch ein Bcc-Header steht. Mit diesem kann er den Inhalt der Mail an viele, beliebig wählbare Empfänger schicken - und damit die Server-Resourcen missbrauchen und dem Inhaber evtl. auch Ärger einbringen.

Um das zu verhindern, genügt es im Grunde, aus sämtlichen Eingaben die Absatzzeichen auszufiltern, z.B. mit $checkedFrom = str_replace(array("\n", "\r"), '', $from);

register_globals und was es bewirkt

Es gibt in der PHP.INI die Einstellung register_globals. Sie bewirkt, wie es in älteren PHP-Versionen auch Standard war, den Import sämtlicher externer Daten in den globalen Namensraum des Skripts. Alle Daten aus Cookies, Get, Post, Umgebung (Environment) und Sessions werden also als Variablen der Form $name einfach so bereitgestellt. Üblicherweise sollte der Zugriff auf diese Variablen immer über $_COOKIE, $_GET, $_POST(, $_REQUEST) und $_SERVER erfolgen!

Abschalten - 1

Diese Einstellung sollte dringen auf "off" stehen. Warum? Weil sonst - bei unsauberer Programmierung - dein Skript beeinflußt werden könnte an Stellen wo dies nicht gewünscht oder sogar gefährlich ist. Beispiel:

< ?php
if($passwort =="okay"){
     $loggedIn= true;
}
if($loggedIn == true){
    ...
}
?>

In diesem Beispiel wird also eine Variable $loggedIn auf true gesetzt, sobald das Passwort stimmt. Stimmt es nicht, wird die Variable nicht gesetzt. Weiter unten wird dann geprüft ob die Variable auf true gesetzt ist - und dann davon ausgegangen, der User sei eingeloggt.

Bei ordentlicher Programmierung wurde am Anfang des Skriptes oder in der php.ini die Einstellung error_reporting auf E_ALL gesetzt - damit käme bei Ausführung des unteren Teils eine Notice-Meldung, wenn der Benutzer nicht eingeloggt ist. Denn es wird dann ja eine Variable geprüft, die nicht vorher gesetzt wurde. Und dies könnte sich ein Hacker zunutze machen, indem er die PHP-Datei so aufruft: index.php?loggedIn=1

- und schon wäre er drin. Also:

error_reporting(E_ALL) nervt, aber wirkt! (Fehler in Produktivumgebungen aber nicht ausgeben, sondern loggen!)

Stets Variablen vor der Benutzung einen definierten Wert zuweisen und vor allem

register_globals immer auf "off" stellen!

Gewöhnt man sich beides an, verbessert sich auch automatisch der Programmierstil :-)

Abschalten - 2

Aus einem ähnlichen Grund sollte man nie Session-Daten über eine mit register_globals registrierte Variable prüfen. Denn man weißt nicht sicher, ob die Variable wirklich Daten aus der Session enthält (denen man trauen kann) oder ob sie nicht doch eine verkappte GET- oder POST-Variable ist und damit evtl. manipulierte Inhalte hat.

Cookie-Daten-Integrität

Nur weil der normale Benutzer selten bis nie etwas von den Cookies einer Seite mitbekommt, heisst das für den Programmierer nicht, dass er den dort abgelegten Daten trauen darf.

Traue niemandem! Prüfe alle Eingaben!

(Diese Warnung ist zugegebenermaßen geklaut aus der PHP-FAQ, aber sehr eingängig!)

Es ist für den Benutzer ein leichtes (z.B. mit entsprechenden FireFox-Plugins) die Daten in den Cookies zu verändern. Daher sollte man dort nie z.B. den Login-Status, die eingeloggte User-ID oder Passwörter speichern! Dafür gibt es Sessions, die erledigen das auf alle Fälle für dich.

eval is evil!

Das folgende Code-Fragment hätte ich nie zu sehen bekommen, wenn es nicht genau dort gewesen wäre wo es war. Gleichzeitig ist es eines der fatalsten Code-Fragmente in Bezug auf PHP-Sicherheit:


foreach($HTTP_GET_VARS as $key => $value) {
    eval("$$key = \"$value\";");
}

Was genau der Autor damit anstellen wollte, wird wohl ewig im Dunkeln bleiben. Fakt ist aber dass er mich damit dazu eingeladen hat, beliebige PHP-Befehle direkt aus der Adressleiste auszuführen. Als Beispiel stelle man sich vor, $value enthielte ";php_info();// womit man schon mal eine schöne Übersicht über die PHP-Installation bekommt. Das lässt sich dann mit ein wenig Kreativität und ein paar PHP-Funktionen ausbauen zu einer Dateiübersicht, der Anzeige von Dateien (z.B. der Datei, die eben diese Lücke enthält), dem Löschen von Dateien, Spam-Versand... etc.pp.. Von daher:

eval vermeiden wo immer möglich

(wie z.B. in dem gezeigten Fall, wo das völlig überflüssig eingesetzt wurde) und vor allem

NIEMALS! ungeprüfte Eingaben in eval packen!

denn

EVAL is EVIL!

und es geht eben meist auch ohne...

MySQL-Injections

...sind eigentlich ganz einfach, eine entsprechend nachlässige Programmierung vorausgesetzt.

Für Anfänger...

Ein Beispiel: Angnommen wir haben ein ganz normales Login-Formular, also so mit Benutzername und Passwort und so. Damit erhält der Benutzer, der eine richtige Kombination aus Passwort und Benutzername eintippt Zugang zu einem geschützten Bereich. Der Programmierer prüft das folgendermaßen: ($_REQUEST['passwort'] und $_REQUEST['benutzername'] enthalten die Daten aus dem Login-Formular) mysql_query('SELECT * FROM users WHERE passwort = "'.$_REQUEST['passwort'].'" AND username = "'.$_REQUEST['benutzername'].'"');

Hier wird geprüft, ob eine Kombination aus Benutzername und Passwort in der Datenbank sind. Auch wenn man mal davon absieht, dass Passwörter nie in Klartext in eine Datenbank gehören wird hier eine Einladung für Angreifer ausgesprochen. Als Beispiel für einen Hack sei die Standard-MySQL-Injection für Benutzername und Passwort genannt: " OR 1 OR " in beide Felder eintippen und man ist drin. Geht mit erstaunlich vielen Seiten! Alternativ natürlich noch einfache Anführungszeichen statt doppelten.

Netterweise ist der Account, der dabei SELECTet wird, meistens auch noch ein Administrator-Account...

...und Fortgeschrittene.

Etwas mehr Kreativität seitens des Eindringlings erfordert folgendes Beispiel, welches im Ernstfall aber auch wesentlich größere Konsequenzen hat.

Ausgegangen wird von einem Formular in dem ein Benutzer seine ICQ-Nummer ändern kann. $_REQUEST['icq'] enthalte daher seine Eingabe, für $username sei bereits sichergestellt dass der richtige Benutzername enthalten ist. mysql_query('UPDATE users SET icqNummer = "'.$_REQUEST['icq'].'" WHERE username = "'.$username.'"');

Damit hat der Programmierer einem Angreifer bereits alle Möglichkeiten in die Hand gegeben, die Daten in der Datenbank beliebig zu verändern. Ein Beispiel? Bitte: ", is_admin="1

eingeben und schon hat man Admin-Rechte, vorausgesetzt der Programmierer hat eine Spalte is_admin vorgesehen, die angibt ob ein Benutzer Administratorrechte hat. Da allerdings, wenn überhaupt solche Spalten vorhanden sind, diese meist nicht all zu kreative Namen haben, kann man hier mit ein wenig Herumraten schon oft etwas erreichen.

Will ein Eindringling nur etwas Schaden anrichten, wird er sich daran erinnern, dass man mit # in MySQL Kommentare anfängt. Daher eignet sich folgende Eingabe von ihm, um allen Benutzern Admin-Rechte zu geben: ", is_admin="1" #

Auch schön, oder?

Will er großen Schaden anrichten, macht er das Ganze in einem DELETE-Statement.

Vorbeugen!

Um solche Angriffe zu verhindern, muss man sich vor allem an

Traue niemandem! Prüfe alle Eingaben!

erinnern. Also: Numerische Werte z.B. mit (int) zu einem Integer casten, um sicherzustellen dass wirklich keine unerlaubten Zeichen enthalten sind. Z.B. so: $checkedValue = (int)$_REQUEST['value'];

Wie man beispielsweise String-Werte und die Rückgabewerte aus Select-Boxen prüft, steht in der PHP-FAQ.

Auf der sicheren Seite ist man insbesondere bei Eingabestrings, wenn man mysql_escape_string() bzw. das neuere mysql_real_escape_string() anwendet. Die oben gezeigten Abfragen in der sicheren Version lauten also: mysql_query('SELECT * FROM users WHERE passwort = "'.mysql_real_escape_string($_REQUEST['passwort']).'" AND username = "'.mysql_real_escape_string($_REQUEST['benutzername']).'"');
mysql_query('UPDATE users SET icqNummer = "'.mysql_real_escape_string($_REQUEST['icq']).'" WHERE username = "'.$username.'"');

(Wobei man das natürlich alles noch schöner schreiben kann.)

Zombie-Includes

Mein persönlicher Favorit unter den Sicherheitslücken sind aber amoklaufende Includes, die wieder mal nur funktionieren, weil jemand die Eingabedaten nicht richtig prüft.

Finden...

Auffällig werden Seiten mit solchen Schwachstellen in der URL: http://www.example.com/index.php?include=content/readme

Sieht "content/readme" nicht verdächtig nach einem Dateinamen aus? Nur ohne Dateiendung. Der Programmierer wird hier also eine Datei includen, die vom aktuellen Ordner aus gesehen im Ordner content liegt und mindestens mit readme anfängt. Es wird wohl eine feste Dateiendung geben, die er immer an den angegebenen Pfad dranhängt. Also könnte obiges Konstrukt z.B. auf /content/readme.txt verweisen.

Sein Code könnte z.B. so aussehen: include($_REQUEST['include'].'.txt');

Machen wir ein paar Versuche! http://www.example.com/index.php?include=content/readmeBLABLA

sollte eine Datei sein, die nicht existiert. Wenn wir Glück haben, wird eine entsprechende Fehlermeldung ausgegeben. http://www.example.com/index.php?include=index.php%00

Könnte, mit etwas Glück, die index.php in sich selber einbinden. Denn: Das %00 in einer URL sendet PHP das ASCII-Zeichen mit Code 0 - und das schneidet Strings ab. Daher wird die Dateiendung, die der Autor hinzufügt, auch abgeschnitten. Was wir nicht wissen ist, ob er vielleicht noch einen Text vorne an den angegebenen Namen anfügt, dann sähe sein Code so aus: include('inc/'.$_REQUEST['include'].'.txt');

Dann gehen oben beschrieben Attacken natürlich nicht mehr. Stattdessen müsste man sich in diesem Fall mit .. durch die Verzeichnisstruktur hangeln um so an andere Dateien zu kommen.

Noch viel gefährlicher ist die Tatsache, dass man unter Umständen bei der Version ohne Verzeichnisangabe Code von externen Quellen ausführen kann - damit stehen einem Angreifer endgültig Tür und Tor offen.

...und vermeiden!

Wieder bedeutet die Lösung für dieses Problem nicht viel Aufwand: mit str_replace werden sämtliche Slashes aus der Eingabe abgefangen, zur Sicherheit auch Backslashes und Punkte: $safeInclude = str_replace(array('/', '\/', '.'), $_REQUEST['include']);

Natürlich muss die Seite so gebaut sein, dass sie eben ohne diese Zeichen in der übergebenen Variablen auskommt!

Auf Nummer Sicher geht man aber, wenn man statt des Dateinamens nur ein Kürzel übergibt, das mittels eines Arrays den Dateinamen ermittelt.

File-Uploads

Eine weitere Möglichkeit für Hacker, eigenen Code auf fremden Systemen auszuführen bilden immer wieder Dateiuploads. Welche Gefahr fremde Daten auf dem eigenen Server darstellen scheint vielen Programmierern nicht bewusst zu sein. In der Regel werden die Uploads z.B. für eine Bilder-Galerie vorgesehen und der Autor rechnet nicht damit, dass man nicht nur Bilder hochladen kann.

Für einen erfolgreichen Upload eigener Skripte müssen einige Voraussetzungen erfüllt sein:

Dann kann der Hacker also eine Datei nach seinen Wünschen hochladen und, da er ihr ja eine individuelle Endung verpassen kann, diese auch ausführen. Denkbar ist auch, dass wenn Punkt 2 und die Einschränkung von Punkt 3 nicht zutreffen, er mittels eines "offen stehenden" includes (Siehe vorangegangenen Abschnitt) eine Datei ausführt, die nicht im document root liegt und die evtl. noch eine Endung wie .gif trägt. Daher ist bei Dateiuploads immer höchste Vorsicht geboten!

Abwehrstrategien

Man kann auch vorsorglich einige Maßnahmen treffen, um es gar nicht erst zum Versuch eines Angriffs kommen zu lassen. Bots erkennen dann nicht mehr, an welcher Stelle sie angreifen müssten und Menschen vergeht schnell die Lust am Herumexperimentieren wenn kein Erfolg zu sehen ist.

Verwirre deinen Gegner!

Das Apache-Modul mod_rewrite ermöglicht es, Aufrufe wie www.example.com/df_maoam auf eine URL wie www.example.com/index.php?f=maoam "zeigen zu lassen". (Vielleicht wirft der ein oder andere an diese Stelle zurecht einen Blick in die Adresszeile seines Browsers.) Bei jedem Aufruf einer Seite wird also der Webserver eine URL, die mit df_ beginnt in einen Aufruf der index.php umwandeln. Für den Browser des Besuchers und den Besucher selbst geschieht das transparent, d.h. sie bekommen nicht mit, dass hier in echt eine andere Seite aufgerufen wird.

Dieses Mapping wird z.B. in einer .htaccess-Datei festgelegt und sieht für das Beispiel so aus: RewriteEngine On
RewriteRule ^df_$ index.php
RewriteRule ^df_([^\.]+)$ index.php?f=$1

Der Kenner sieht natürlich, dass es sich bei dem Zeichenwirrwar in der zweiten und dritten Zeile um einen regulären Ausdruck handelt - und wie die gebildet werden findet man nebenan im Regex-Tutorial.

Mit mod_rewrite kann man lustige Sachen machen, aber man verliert auch genau so schnell mal gerne den Überblick, was man da eigentlich gemacht hat. Eine komplette Anleitung gibt es natürlich auch, und die Zitate am Anfang sind ernst zu nehmen!

Täuschen und Tarnen

Ein weiteres Mittel ist das Verstecken von PHP. Nur mit HTML gestaltete Sites sind sicherlich ein wesentlich unattraktiveres Angriffsziel als interaktive. Um PHP zu verstecken sind im Grunde nur zwei Schritte nötig:

Beide Maßnahmen sind allerdings nur als kleine zusätzliche Vorkehrungen zu verstehen und bieten nicht all zu viel Sicherheit. Vor allem endet ihre Wirksamkeit dort, wo GET-Parameter in der URL auftauchen, ganz offensichtlich dynamische Inhalte vorliegen oder PHP-Fehlermeldungen angzeigt werden.

Wichtige Einstellungen der PHP.INI

In der Konfigurationsdatei von PHP, der PHP.INI findest du einige Einstellungen, die wesentlich zur Sicherheit beitragen können:

Meldung erstatten!

An dieser Stelle wirds dann schon leicht paranoid. Hast du mit Angriffen zu rechnen, lasse dir fehlgeschlagene MySQL-Queries, Parameter die durch die Eingangsprüfung am Anfang des Skriptes durchgefallen sind und fehlgeschlagene Logins direkt per E-Mail zusammen mit allen Daten, die du vom Verursache zusammenkratzen kannst (IP-Adresse, Browser etc.pp.) zusenden.

Klingt gut, hat aber zwei Design-Schwächen: Erstens kann ein Angreifer seine Identität locker verstecken. Zweitens könnte er genau diese Form von "Error Reporting" für einen Denial-of-Service Angriff nutzen indem er dein Postfach mit Mails zumüllt und deinen Server durch das massenhafte Versenden von Mails lahmlegt.

Wenn man auf das Live-Feeling verzichten kann, ist es daher besser, solche Benachrichtigungen in einem Logfile abzuspeichern.

Error: No site found with the domain 'test.basti1012.bplaced.net' (Learn more)