Sicherheitslücke in unserem PHP-Setup

Wir legen in unserer Entwicklung großen Wert auf Sicherheit. Jedes Stück Code läuft durch ein Code-Review, Designs werden (meistens) erst durchgesprochen und dann umgesetzt. Normalerweise kommen wir damit auch gut weg und haben uns in der Firmenhistorie nur wenige relevante Lücken erlaubt. Keine davon wurde unseres Wissens nach aktiv ausgentutzt, sondern durch User reported. Eine solche Report-Mail erreichte uns gestern.

tldr: Ein User hat eine Lücke in unserem PHP-Setup gefunden, die es Usern ermöglicht über den Webserver Code im Kontext anderer User auf dem selben Host auszuführen. Wir haben diese Lücke gestern kurz nach Eingang der Meldung gestopft; sie wurde unseres Wissens nach nie aktiv ausgenutzt. Es besteht keine Gefahr für die Zivilbevölkerung.

12:03 Report

Am 10. Jänner, pünktlich um 12 Uhr Mittags klingelte es im Ticketsystem und wir hatten Post von Julian Brost:

ich habe in euren U7-Setup (ältere Versionen habe ich mir nicht angeschaut) ein Problem gefunden, das das sich dazu ausnutzen lässt, PHP-Code als beliebiger anderer Benutzer (im Sinne von Kundennutzer, also alle Nicht-System-Nutzer) auszuführen.

Das ist erstmal.. sehr schlecht. Neben einer klassischen Privielge Escalation in Richtung root ist das Ausführen von beliebigen Code im Kontext anderer User so der 2nd-worst-case.

12:27 in die Entwicklung

Nachdem sich unser Support-Team durch die morgentliche Ticket-Flut gekämpft hat, sind sie auch bei diesem Ticket angekommen. Die Kollegen haben das Problem direkt richtig eingestuft, ein High-Prio-Issue in unserem Bug-Tracker draus gemacht und noch zusätzlich im Chat drauf hingewiesen.

Jonas und ich haben uns direkt auf Fehlersuche begeben. Julian führt in seiner ersten Mail weiter aus:

Das Problem: Die PHP-FPM-Sockets aller Nutzer sind für den Nutzer "apache" zugreifbar und man bekommt den Webserver über die .htaccess dazu, den Socket eines anderen Nutzers für eigene Dateien zu nutzen:

SetHandler "
  proxy:unix:/run/php-fpm-victim.sock|
  fcgi://localhost/localhost/var/www/virtual/attacker/payload.php
"

Leider ein relativ einfacher Exploit. Zum besseren Verständis ein wenig Hintergrundinformationen zu unserem PHP-Setup:

              _
            (`  ).
           (     ).            .-------.        .-------.        .---------.
          _(       '`.  -----> | nginx | -----> | httpd | -----> | php-fpm |
      .=(`( Internet )         '-------'        '-------'  via   '---------'
      ((    (..__.:'-'                                    /run/php-fpm-$USER.sock
      `(       ) )                                        srw-rw---- apache:apache
        ` __.:'   )
               --'

PHP-Webseiten werden bei U7 über php-fpm abgewickelt. Dieser wird von einem zentralen Apache-Webserver pro Host direkt angesprochen. Den Apachen teilen sich alle User und bekommen jeweils einen <VirtualHost>; PHP-FPM gibt es aus verschiedenen Gründen für jeden User einzeln. Diese Instanzen laufen ausschließlich mit User-Rechten.

Genau diesen zentralen Apachen macht sich unser Angreifer Julian hier zu Nutze. In der angesprochenen Apache-Config findet sich folgender Block, um PHP und Apache zu verknuspern:

<FilesMatch "\.php$">
    SetHandler "
     proxy:unix:/run/php-fpm-{{ param_username }}.sock|
     fcgi://php-fpm-{{ param_username }}
    "
</FilesMatch>

<Proxy "fcgi://php-fpm-{{ param_username }}" max=10>
</Proxy>

Leider lässt sich, wie in Julians Mail illustriert, eine Variante davon auch problemlos in eine .htaccess-Datei packen. So verbindet sich dann der User attacker über den Apachen zum php-fpm vom User victim und führt dort eine für victim lesbare, aber von attacker kontrollierte Datei aus. Da php-fpm volle Zugriffsrechte auf den Uberspace hat, lässt sich diese Lücke trivial zu vollem SSH-Zugriff ausbauen.

Sidenote: Wir schrieben oben, dass php-fpm ausschließlich mit User-Rechten läuft, der Socket gehört aber apache:apache. Ein User kann keinen Socket mit diesem Permissions erstellen. Daher übernimmt das eine systemd .socket-Unit und gibt den Socket dann an php-fpm weiter. Alternativ lässt sich ein solches Setup auch mit Tools wie xinetd aufbauen.

13:24 der Fix

Unser Setup hat hier zwei Mängel:

  1. php-fpm liest Dateien, die es gar nicht lesen soll.
  2. php-fpms anderer User sind über eine Ecke für Dritte zu erreichen.

Das erste Problem haben wir mit der php-Konfiguration open_basedir gefixt. Konkret steht in der php-fpm.conf jeder Userin nun:

php_admin_value[open_basedir] = /opt/remi:/var/opt/remi:/var/www/virtual/$USER:/home/$USER

PHP darf also nur noch auf die eigenen Libraries, den Documentroot, sowie das Home-Verzeichnis zugreifen. Dabei sind die ersten drei zwingend notwendig, in letzterem wird gerne von Usern das Nextcloud-data-Verzeichnis abgelegt. Der Zugriff auf /var/www/virtual/attacker/payload.php schlägt allerdings fehl.

Diese Änderung ist nur im Space des Opfers, nicht jedoch in der des Angreifers notwendig. Somit lässt sie sich auch nicht durch Angreifer einfach mit einer eigenen php.ini wieder entfernen.

Das zweite Problem lässt sich mit unserem aktuellen Setup nicht über reine Config-Änderungen beheben. Wir prüfen hier gerade, ob wir SetHandler entsprechend einschränken können. Alternativ steht auch ein Apache-Server pro User im Raum. Dazu mehr in der Zukunft.

14:06 - 18:00 Rollout

Den Fix haben wir natürlich erst auf unseren Dev-Systemen, dann auf den echten Demo-Accounts von Julian getestet. Anschließend wurde er um 14:06 auf das erste Drittel unserer U7-Flotte ausgerollt. Um 16:00 und 18:00 folgten die weiteren Drittel.

Alle U7-Systeme sind damit seit gestern 18:00 mit dem entsprchenend Patch versehen und nicht mehr angreifbar.

Auswirkungen auf eure Apps

Da eure Applikationen ohnehin nur auf eure Daten zugreifen, sollte die Änderung wenig Einfluss auf eure Spaces haben. Eine Ausnahme gibt es jedoch: /tmp. Dort legen viele Apps standardmäßig temporäre Dateien wie Uploads ab. Wir können dieses Verzeichnis jedoch nicht freigeben, da Angreifer dort den Payload trivial ablegen könnten.

Es folgt eine zweite Änderung um upload_tmp_dir und sys_temp_dir entsprechend auf /home/$USER/tmp anzupassen. Alle Applikationen die brav php nach einem temporären Verzeichnis fragen, sollten dann wieder regulär funktionieren. Für Apps, die das nicht tun, muss in deren Config ein abweichendes Verzeichnis gesetzt werden.

die Zukunft

Der zentrale Apache pro Host ist eine Design-Schwachstelle in U7. Er ermöglicht es uns viele User-Anwendungen mit vergleichsweise geringem Ressourcenaufwand abzufackeln, versperrt uns aber auch viele Wege. So könnten wir mit einem Apache pro User bereits heute Websockets anbieten, User könnten eigene Apache-Module laden und allgemein tiefer in die Config eingreifen, als es im Moment möglich ist.

Neben möglichen, neue Einschränkungen für .htaccess-Dateien prüfen wir auch, ob ein Apache pro User bei uns möglich ist. Somit könnten wir diese Klasse von Problemen als ganzes aus der Welt schaffen.

Security-Probleme aller Art könnt ihr uns übrigens - so wie alle anderen Fragen - via hallo@uberspace.de verschlüsselt zukommen lassen. Wir bemühen uns diese schnellstmöglich zu beheben. In den meisten Fällen gelingt uns das auch, so wie hier.


Die ASCII-Wolke von asciiart.eu hat a:f gemalt. Danke!

Sicherheitslücke in unserem PHP-Setup
Share this