
Un exploit Proof of Concept per il PyInstaller CVE-2019-16783
Questo è il mio POC per la vulnerabilità di Windows PyInstaller versione < 3.6 che esiste per l'opzione --onefile. Un attaccante potrebbe ottenere l'esecuzione di comandi e un possibile LPE dirottando una DLL importata dalla DLL dell'interprete Python utilizzata dal binario PyInstaller. Seguirà una breve spiegazione della vulnerabilità e del processo di sfruttamento. Un ringraziamento a Alter Solutions per aver trovato la vulnerabilità e aver scritto delle loro scoperte su PagedOut #3 (pagina 55 del pdf).
La vulnerabilità è stata causata da una debole creazione di una directory utilizzata da PyInstaller durante l'esecuzione del binario generato. PyInstaller costruisce una directory _MEIPIDX nella directory temp dell'utente e ci inserisce vari elementi, come la DLL dell'interprete Python utilizzata per eseguire il codice Python impacchettato in un eseguibile PE.
Il problema con questo processo era che la directory costruita per NT AUTHORITY\SYSTEM era C:\Windows\Temp, il che permetteva a qualcuno sia di indovinarla che di scrivere al suo interno. Quindi un dirottamento di DLL sarebbe stato possibile, ad esempio, quando veniva eseguito l'interprete Python. Qui c'è il commit che corregge la vulnerabilità. Invece di affidarsi solo ad alcune funzioni API standard per creare la directory, gli sviluppatori hanno implementato una propria funzione per avere maggior controllo sulla creazione della directory.
Dal momento che questo è stato il mio primo POC, discuterò un paio di problemi che ho incontrato lungo il percorso.
Innanzitutto, configurare un ambiente per questo POC non è stato particolarmente difficile, dato che tutto ciò di cui avevi bisogno era la versione corretta del pacchetto. Tuttavia, quando l'ho installato, crashava:
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)
Dopo aver cercato un po', ho visto che la mia versione di Python era la causa (sto usando la 3.8.10). La mia versione di Python ha reso difficile installare qualsiasi altra versione 3.8.x, poiché all'installazione il programma di installazione trovava la mia versione Python38 e restituiva un errore. Ho anche provato a compilare un'altra versione 3.8, ma non ci sono riuscito perché necessitava di Visual Studio 2015, mentre io ho il 2022. Alla fine, ho scelto di scaricare Python 3.7.5, che ha funzionato perfettamente. Quindi ho creato un ambiente virtuale per la versione 3.7.
Ho capito che configurare l'ambiente può variare dall'averlo già pronto, a richiedere potenzialmente molto tempo per essere impostato correttamente. Sebbene sia importante, potrebbe potenzialmente togliere il divertimento dello sfruttamento effettivo del nostro target.
Ci sono due passaggi nel nostro processo di sfruttamento: trovare la directory del processo impacchettato con PyInstaller e scrivere la nostra DLL exploit in quella directory. Per la prima parte, possiamo trovare il PID attraverso le funzioni WINAPI standard (CreateToolhelp32Snapshot, Process32First, Process32Next). Questo ha funzionato senza problemi. Successivamente, dobbiamo trovare l'ultimo numero della directory _MEI. L'exploit originale suggerisce di utilizzare semplicemente la funzione GetFileAttributesA e controllare se il codice di stato restituito è FILE_ATTRIBUTE_DIRECTORY. Tuttavia, questo non ha funzionato per me. La mia soluzione è stata creare un file in ogni directory candidata e, se il file veniva creato con successo, allora la directory esisteva. Per qualche motivo, lo stato restituito per il file da GetFileAttributesA era FILE_ATTRIBUTE_ARCHIVE, quindi controllo quello. Prima di proseguire, il ciclo durante la ricerca del PID è perché vogliamo iniettare le nostre DLL quando il processo target viene eseguito, per averle pronte prima del caricamento.
Per la DLL, dobbiamo dirottare una DLL che l'interprete Python (python37.dll) importa. Per ottenere le DLL che l'interprete importa, potremmo aprire la DLL con PE-Bear. Una delle DLL di sistema importate è version.dll. Quindi possiamo creare la nostra DLL e abusare dell'ordine di ricerca del loader di Windows posizionandola nella directory temp del processo impacchettato con PyInstaller. In questo modo, quando vuole importare la DLL, importerà la nostra DLL malevola, invece di quella corretta.

Tuttavia, fare questo non funzionerà. Il motivo è che l'import della funzione che l'interprete Python chiama da version.dll (nello specifico VerQueryValueW dall'immagine) non viene risolto. Quindi il programma crasha. Per risolvere questo problema, dobbiamo fare un proxy DLL. In breve, configuriamo la nostra DLL malevola per esportare le funzioni della DLL originale, e portiamo la DLL originale rinominata, in modo che possa caricarla e chiamare la funzione da lì. Quindi per il nostro exploit, compileremo una DLL malevola con un DllMain che ci permetterà di ottenere l'esecuzione del codice. Esporteremo tutte le funzioni di version.dll e copieremo la version2.dll originale di sistema e la posizioneremo nella stessa directory. In questo modo, version.dll può inoltrare le chiamate di funzione a version2.dll. E una volta che abbiamo queste DLL, tutto ciò che dobbiamo fare è copiarle nella directory del processo e attendere di ottenere l'esecuzione del codice. Per una spiegazione migliore del DLL Proxying/Hijacking, leggi l'articolo collegato.

Anche se a posteriori questi passaggi sono tutti facili e diretti, non lo sono stati durante lo sviluppo del POC. Qualche tempo fa, ci è voluto un po' per capire il processo dell'exploit, e una volta che ho iniziato a programmare, ho iniziato a capire di più. Per quanto riguarda la DLL, ho provato a compilarla da solo nel modo in cui era implicito nel repository POC di Alter Solutions. Loro hanno una DLL separata per l'esecuzione del codice e una diversa per il proxying, che caricava la payload.dll a runtime (DllMain viene chiamato al caricamento). Tuttavia non sono riuscito a compilarla correttamente. Alla fine, dopo aver letto l'articolo sopra, ho deciso di utilizzare il repository DLLProxyProject, che ha compilato la DLL che volevo e può essere utilizzato generalmente per produrre una DLL per scopi di proxying. Ho provato a copiare il suo file DLLMain.cpp e a compilarlo con il file header exports.h. E anche se è stato eseguito, ha prodotto un errore:
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):
Questo errore potrebbe essere evitato con il file Utils.cpp, quindi ho deciso di mantenere quel progetto per la compilazione della mia DLL (anche se sarebbe stato più bello se l'avessi fatto completamente da solo).
Dopo aver configurato l'ambiente (ho usato psexec per ottenere una shell NT AUTHORITY\SYSTEM), ho eseguito l'exploit, avviato il binario target e ho ottenuto l'esecuzione del codice come amministratore.

Questa è stata un'esperienza molto piacevole, e sono felice di averla affrontata. Voglio davvero produrre altri POC per altre CVE, e questo è stato un primo target perfetto. Leggere la descrizione della vulnerabilità è stato interessante, poiché ho capito che la difficoltà principale nell'implementare un POC da soli è quella di riempire gli spazi vuoti per le cose che l'autore non ha spiegato (sia intenzionalmente che no), e semplicemente capire ciò che si sta leggendo. Era una vulnerabilità semplice, quindi non ho dedicato molto sforzo per capirla. Nel prossimo futuro, dopo averne fatti un paio di altri, proverò a fare un POC per una vulnerabilità di corruzione della memoria.