
Ein Proof-of-Concept-Exploit für PyInstaller CVE-2019-16783
Dies ist mein POC für die Windows PyInstaller Version < 3.6 Schwachstelle, die für die --onefile-Option existiert. Ein Angreifer könnte Codeausführung und möglicherweise LPE erreichen, indem er eine DLL kapert, die von der von der PyInstaller-Binärdatei verwendeten Python-Interpreter-DLL importiert wird. Im Folgenden wird eine kurze Erklärung der Schwachstelle und des Ausnutzungsprozesses gegeben. Mein Dank gilt Alter Solutions für das Auffinden der Schwachstelle und für die Veröffentlichung ihrer Erkenntnisse in PagedOut #3 (Seite 55 im PDF).
Die Schwachstelle wurde durch eine schwache Erstellung eines Verzeichnisses verursacht, das von PyInstaller zur Laufzeit der ausgeführten Binärdatei verwendet wird. PyInstaller erstellt ein _MEIPIDX-Verzeichnis im Temp-Verzeichnis des Benutzers und legt dort verschiedene Dinge ab, wie etwa die Python-Interpreter-DLL, die zum Ausführen des in eine PE-Datei verpackten Python-Codes verwendet wird.
Das Problem bei diesem Prozess war, dass das für NT AUTHORITY\SYSTEM erstellte Verzeichnis C:\Windows\Temp war, was es einem ermöglichte, es sowohl zu erraten als auch darin zu schreiben. So wäre ein DLL-Hijacking z. B. möglich, wenn der Python-Interpreter ausgeführt wurde. Hier ist der Commit, der die Schwachstelle behebt. Anstatt sich nur auf einige Standard-API-Funktionen zum Erstellen des Verzeichnisses zu verlassen, implementierten die Entwickler ihre eigene Funktion, um mehr Kontrolle über die Verzeichniserstellung zu haben.
Da dies mein erster POC war, werde ich auf einige Probleme eingehen, auf die ich unterwegs gestoßen bin.
Zunächst einmal war die Einrichtung einer Umgebung für diesen POC nicht besonders schwierig, da alles, was man brauchte, die richtige Paketversion war. Allerdings stürzte es ab, als ich es installierte:
Traceback (most recent call last):
File "c:\users\ckrielle\appdata\local\programs\python\python38\lib\runpy.py", line 194, in _run_module_as_main
return _run_code(code, main_globals, None,
...
File "c:\users\ckrielle\appdata\local\programs\python\python38\lib\site-packages\PyInstaller\building\utils.py", line 653, in <genexpr>
strip_paths_in_code(const_co, new_filename)
File "c:\users\ckrielle\appdata\local\programs\python\python38\lib\site-packages\PyInstaller\building\utils.py", line 660, in strip_paths_in_code
return code_func(co.co_argcount, co.co_kwonlyargcount, co.co_nlocals, co.co_stacksize,
TypeError: an integer is required (got type bytes)
Nachdem ich ein wenig gesucht hatte, sah ich, dass meine Python-Version schuld war (ich verwende 3.8.10). Meine Python-Version erschwerte die Installation einer anderen 3.8.x-Version, da der Installer bei der Installation meine Python38-Version fand und einen Fehler zurückgab. Ich versuchte auch, eine andere 3.8-Version zu bauen, aber das gelang nicht, da sie Visual Studio 2015 benötigte, während ich 2022 habe. Schließlich entschied ich mich, Python 3.7.5 herunterzuladen, das einwandfrei funktionierte. Also erstellte ich eine virtuelle Umgebung für die 3.7-Version.
Mir wurde klar, dass die Einrichtung der Umgebung von bereits eingerichtet bis hin zu potenziell sehr zeitaufwändig sein kann. Auch wenn es wichtig ist, könnte es möglicherweise den Spaß an der eigentlichen Ausnutzung unseres Ziels nehmen.
Es gibt zwei Schritte in unserem Ausnutzungsprozess: das Finden des Verzeichnisses des mit PyInstaller verpackten Prozesses und das Schreiben unserer Exploit-DLL dorthin. Im ersten Teil können wir die PID über die standardmäßigen WINAPI-Funktionen (CreateToolhelp32Snapshot, Process32First, Process32Next) finden. Das funktionierte reibungslos. Danach müssen wir die letzte Nummer des _MEI-Verzeichnisses finden. Der ursprüngliche Exploit schlägt vor, einfach die Funktion GetFileAttributesA zu verwenden und zu prüfen, ob der zurückgegebene Statuscode FILE_ATTRIBUTE_DIRECTORY ist. Das funktionierte bei mir jedoch nicht. Meine Lösung bestand darin, in jedem Verzeichniskandidaten eine Datei zu erstellen, und wenn die Datei erfolgreich erstellt wurde, existierte das Verzeichnis. Aus irgendeinem Grund war der von GetFileAttributesA für die Datei zurückgegebene Status FILE_ATTRIBUTE_ARCHIVE, also prüfe ich darauf. Bevor ich fortfahre: Die Schleife bei der Suche nach der PID dient dazu, dass wir unsere DLLs injizieren, wenn der Zielprozess ausgeführt wird, damit sie vor dem Laden bereit sind.
Für die DLL müssen wir eine DLL kapern, die der Python-Interpreter (python37.dll) importiert. Um die DLLs zu erhalten, die der Interpreter importiert, könnten wir die DLL mit PE-Bear öffnen. Eine der importierten System-DLLs ist version.dll. Also können wir unsere eigene DLL erstellen und die Suchreihenfolge des Windows-Loaders ausnutzen, indem wir sie in das Temp-Verzeichnis des mit PyInstaller verpackten Prozesses legen. Auf diese Weise importiert er, wenn er die DLL importieren möchte, unsere bösartige DLL anstelle der richtigen.

Das allein wird jedoch nicht funktionieren. Der Grund ist, dass der Import der Funktion, die der Python-Interpreter aus version.dll aufruft (insbesondere VerQueryValueW aus dem Bild), nicht aufgelöst wird. Das Programm stürzt ab. Um dieses Problem zu lösen, müssen wir DLL-Proxying betreiben. Kurz gesagt, konfigurieren wir unsere bösartige DLL so, dass sie die Funktionen der Original-DLL exportiert, und wir bringen die Original-DLL umbenannt mit, damit sie sie laden und die Funktion von dort aufrufen kann. Für unseren Exploit werden wir also eine bösartige DLL mit einem DllMain kompilieren, das uns die Codeausführung ermöglicht. Wir exportieren alle Funktionen von version.dll, kopieren die ursprüngliche System-version2.dll und legen sie in dasselbe Verzeichnis. Auf diese Weise kann version.dll die Funktionsaufrufe an version2.dll weiterleiten. Sobald wir diese DLLs haben, müssen wir sie nur noch in das Verzeichnis des Prozesses kopieren und auf die Codeausführung warten. Eine bessere Erklärung für DLL Proxying/Hijacking finden Sie im verlinkten Artikel.

Auch wenn diese Schritte im Nachhinein alle einfach und unkompliziert sind, waren sie es während der Entwicklung des POC nicht. Vor einiger Zeit dauerte es eine Weile, den Prozess des Exploits zu verstehen, und sobald ich mit dem Programmieren begann, verstand ich ihn besser. Was die DLL betrifft, so versuchte ich, sie selbst so zu kompilieren, wie es im Alter Solutions POC-Repo angedeutet wurde. Sie haben eine separate DLL für die Codeausführung und eine andere für das Proxying, die die payload.dll zur Laufzeit lädt (DllMain wird beim Laden aufgerufen). Ich konnte sie jedoch nicht richtig kompilieren. Am Ende, nachdem ich den obigen Artikel gelesen hatte, entschied ich mich für das Repository DLLProxyProject, das die gewünschte DLL kompilierte und allgemein zur Erstellung einer DLL für Proxying-Zwecke verwendet werden kann. Ich versuchte, die DLLMain.cpp-Datei zu kopieren und mit der Header-Datei exports.h zu kompilieren. Obwohl es lief, erzeugte es einen Fehler:
Fatal Python error: init_sys_streams: can't initialize sys standard streams
OSError: [WinError 6] The handle is invalid
Current thread 0x00002ea8 (most recent call first):
Dieser Fehler könnte mit der Datei Utils.cpp vermieden werden, also entschied ich mich, dieses Projekt für meine DLL-Kompilierung zu behalten (obwohl es schöner gewesen wäre, wenn ich es komplett selbst gemacht hätte).
Nach dem Einrichten der Umgebung (verwendete psexec, um eine NT AUTHORITY\SYSTEM-Shell zu erhalten), führte ich den Exploit aus, führte die Ziel-Binärdatei aus und erhielt Codeausführung als Admin.

Dies war eine sehr schöne Erfahrung, und ich bin froh, sie gemacht zu haben. Ich möchte wirklich weitere POCs für andere CVEs erstellen, und dies war ein perfektes erstes Ziel. Das Lesen der Beschreibung der Schwachstelle war interessant, da mir klar wurde, dass die Hauptschwierigkeit bei der Umsetzung eines eigenen POC darin besteht, die Lücken für Dinge zu füllen, die der Autor nicht erklärt hat (ob absichtlich oder nicht), und einfach zu verstehen, was man liest. Es war eine einfache Schwachstelle, also habe ich nicht viel Mühe in das Verständnis gesteckt. In naher Zukunft, nachdem ich ein paar weitere gemacht habe, werde ich versuchen, einen POC für eine Speicherkorruptions-Schwachstelle zu machen.