Fallstudie: Python-RCE-Schwachstelle in Celery
Calum Hutton
15. Februar 2022
0 Min. LesezeitÜberblick
Auf Grundlage bestehender Python-Schwachstellen untersuchte ich ein gemeinsames Softwaremuster. Mithilfe unserer hauseigenen Engine für statische Analysen, die auch Snyk Code antreibt, unser Produkt für statische Anwendungssicherheitstests (SAST), konnte ich benutzerdefinierte Regeln erstellen und einen großen Datensatz mit Open-Source-Code durchsuchen, um weitere Projekte zu finden, die dasselbe Muster verwenden. So entdeckte ich eine Stored-Command-Injection-Schwachstelle in Celery. SAST-Tools wie Snyk Code helfen Entwicklern, Fehler in ihrer Software zu erkennen, indem sie ausschließlich den statischen Quellcode analysieren und Muster für ineffizienten oder gefährlichen Code identifizieren.
Motivation
Zu diesem Forschungsprojekt motivierten mich persönliche Erfahrungen und Untersuchungen zu Schwachstellen in Open-Source-Python-Projekten (CVE-2017-11610 und CVE-2021-32807). Meine Hypothese war, dass das Durchlaufen von Objekten (die Möglichkeit, über ein anderes Objekt auf ein beliebiges Objektattribut zuzugreifen) in Python weit verbreitet ist. Ich wollte diese Hypothese untersuchen und belegen, um herauszufinden, wie häufig dieses Muster im gesamten Python-Ökosystem auftritt und insbesondere Fälle eines beliebigen (oder nahezu beliebigen) Durchlaufens von Objekten zu identifizieren, die zu Sicherheitslücken führen könnten.
Hintergrund
In Python ist fast jedes Sprachelement ein Objekt mit eigenen expliziten und geerbten Attributen und Methoden (einschließlich Klasseninstanzen und Modulen). Daher können Python-Anwendungen eine Methode zum Durchlaufen eines Objektnamensraums bereitstellen, über die sich Verweise auf Attribute oder Unterattribute eines Objekts abrufen lassen. Dazu kann eine rekursive Attributsuche durchgeführt werden.
Das folgende vereinfachte Beispiel zeigt, wie sich Objektnamensräume in Python 3 durchlaufen lassen: Durch den Import eines harmlosen Moduls (random) lässt sich mithilfe von getattr ein Verweis auf das importierte Modul os (mit dem Alias _os) und so auf eine gefährliche Funktion (os.system) abrufen:
Wenn eine Python-Anwendung Benutzern Funktionen zum Durchlaufen von Objekten bereitstellt, mit denen sie den angeforderten Pfad oder das Attribut des Objektnamensraums manipulieren können, kann dies ein beliebiges (oder nahezu beliebiges) Durchlaufen von Objekten und damit mehrere potenzielle Probleme ermöglichen:
Die wahrscheinlichsten Folgen offengelegter Funktionen zum Durchlaufen von Objekten sind fehlerhafte Zugriffskontrollen oder die Offenlegung von Informationen, wenn Benutzer auf eine private Methode oder ein privates Attribut wie
obj._private_method() or obj._secret. zugreifen können.Auch Remote Code Execution (RCE) ist möglich, wenn eine beliebige Methode wie
os.system()abgerufen und mit von Benutzern bereitgestellten Argumenten aufgerufen wird. Dies ist weniger wahrscheinlich, da Benutzer einen Verweis auf die Methode abrufen und ihr Argumente übergeben können müssen.
Zusammengefasst könnte ein Angreifer, der das Durchlaufen von Objekten in einer Python-Anwendung steuern oder manipulieren kann, möglicherweise auf Attribute und Unterattribute eines bestimmten Moduls oder einer Klasseninstanz zugreifen und deren Funktionen uneingeschränkt oder auf unerwartete Weise nutzen.
Das anfällige Muster
Das Muster, das im Zusammenhang mit den oben genannten CVEs als relevant erkannt wurde, ist eine rekursive Attributsuche, die normalerweise auf einem durch Punkte getrennten Python-Pfad basiert. Der Pfad wird aufgeteilt und in einer Schleife durchlaufen. Dabei wird das aktuelle Pfadelement mithilfe von getattr() verwendet, um einen Verweis im jeweiligen Kontext abzurufen. Der Verweis aus dem vorherigen Aufruf von getattr() dient dann als Kontext für den nächsten Aufruf. Dieser Vorgang wiederholt sich, bis keine Pfadelemente mehr übrig sind. Das folgende Skript veranschaulicht dies:
Der durch Punkte getrennte Pfad wird in zwei Zeichenfolgen aufgeteilt (_os und system). Bei der ersten Iteration der Schleife verweist die Variable cls auf das Modul random. Daher sucht getattr() im Modul random nach dem Attribut _os, also dem Modul os. Das Modul os wird anschließend zum Kontext von getattr(). Bei der nächsten Iteration wird das Attribut system des Moduls os abgerufen, also os.system().
Fallstudie (Celery – CVE-2021-23727)
Mit der Snyk Code-Engine entwickelte ich Regeln, um dasselbe Muster in anderen Open-Source-Python-Projekten zu erkennen. Eine der Übereinstimmungen mit den Regeln bzw. Mustern fand sich in Celery, einer Open-Source-Warteschlange für asynchrone Aufgaben, die auf verteilter Nachrichtenübermittlung basiert. Die Regel fand die Funktion exception_to_python in der Klasse celery.backends.base.Backend:

Das rekursive getattr-Muster ist in der Funktion in Zeile 354 zu sehen. Eine genauere Prüfung der Funktion und ihrer Verwendung in Celery ergab, dass das gesamte exc-Dict-Objekt aus JSON-Daten stammte, die in einem Celery-Backend gespeichert waren. Daher konnte es potenziell von einem Benutzer mit Zugriff auf den Backend-Server kontrolliert oder manipuliert werden. Alle Felder des Dicts mussten somit als potenziell manipuliert betrachtet werden.
Bei einer weiteren Codeanalyse stellte sich heraus, dass anhand einer Eigenschaft des exc-Dict-Objekts ein beliebiger Modulverweis abgerufen wird (Zeile 352). Anschließend wird mithilfe des rekursiven Attributmusters ein beliebiges Attribut des Moduls gesucht. Der abgerufene Attributverweis wird mit einem einzelnen Zeichenfolgenargument aufgerufen, das ebenfalls aus dem exc-Dict-Objekt stammt (Zeile 364).
Da das gesamte exc-Dict-Objekt potenziell manipuliert werden konnte und keine Validierung den Zugriff auf beliebige Module und Attribute verhinderte, wurde dieser Code als wahrscheinlich anfällig für Stored Command Injection eingestuft.
Ich überprüfte diese Hypothese in der Python-Konsole, indem ich die betreffenden Klassen importierte und ein schädliches Dict erstellte. Damit simulierte ich, wie ein Angreifer ein schädliches JSON-Objekt in der Datenbank eines Celery-Backends ablegt:
Zunächst erstelle ich ein Python-Dict, das an die anfällige Funktion übergeben wird. Dieses Dict enthält Eigenschaften, mit denen sich das abzurufende Modul festlegen lässt (exc_module), das Attribut des Moduls, auf das verwiesen werden soll (exc_type), und schließlich das Argument, das an die abgerufene Methode übergeben wird (exc_message).
In den nächsten Zeilen importiere ich den anfälligen Code aus Celery in die Python-Konsole und initialisiere die Klasse Backend mit einem Celery-Objekt.
Schließlich wird die Schwachstelle ausgelöst, wenn das Dict an die anfällige Methode exception_to_python übergeben wird.
Die letzte Zeile des obigen Codeausschnitts zeigt die Ausgabe des Befehls id. Dies beweist, dass das präparierte Dict deserialisiert wurde und erfolgreich eine beliebige Befehlseinschleusung in der Klasse Backend ausgelöst hat.
Bei einem System, das Celery verwendet, könnte ein Angreifer die Schwachstelle ausnutzen, um Befehle im Producer einzuschleusen und möglicherweise das gesamte System zu übernehmen. Befindet sich das Celery-Backend auf einem Remoteserver, könnten Angreifer, die Zugriff darauf erlangt haben, sich außerdem im Netzwerk eines Unternehmens lateral bewegen und sich Zugang zu weiterer Netzwerkinfrastruktur verschaffen.
Behebung
Das Problem wurde Celery verantwortungsvoll offengelegt und in Version 5.2.2 der Software behoben. Dazu wurde eine Validierung des angesprochenen Moduls hinzugefügt. Außerdem werden die Typen aufgelöster Attribute geprüft, bevor beliebige Funktionen mit potenziell manipulierten Eingaben aufgerufen werden. Beim Deserialisieren von Objekten sollte generell sorgfältig mit allen potenziell manipulierten Daten umgegangen werden – auch dann, wenn sie aus einer Remote-Datenquelle oder Datenbank stammen. Wenn möglich, sollten die Objekttypen vor der Deserialisierung oder zumindest vor dem Aufruf einer Methode oder Eigenschaft des deserialisierten Objekts überprüft und validiert werden.
Die Entdeckung dieser Command-Injection-Schwachstelle in Celery und die erforderliche Analyse manipulierter Daten wurden durch Snyk Code vereinfacht. Unser revolutionäres SAST-Tool ist eine entwicklerfreundliche, schnelle und präzise Alternative zu herkömmlichen Sicherheitstools. IDE-Plugins integrieren Tests in Echtzeit in bestehende Workflows und ermöglichen es Entwicklern, Schwachstellen in nur fünf Minuten zu finden und zu beheben. Starten Sie noch heute Ihre kostenlose Testversion und erleben Sie, wie Snyk Code sichere Entwicklung einfach macht.
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.
Quellen
Celery (CVE-2021-23727): https://security.snyk.io/vuln/SNYK-PYTHON-CELERY-2314953
