
Эксплойт, подтверждающий концепцию, для PyInstaller CVE-2019-16783
Это мой POC для уязвимости Windows PyInstaller версии < 3.6, существующей для опции --onefile. Злоумышленник может выполнить команду и возможно LPE, перехватив DLL, импортируемую интерпретатором Python, используемым бинарным файлом PyInstaller. Краткое объяснение уязвимости и процесса эксплуатации будет приведено далее. Моя благодарность Alter Solutions за обнаружение уязвимости и описание своей находки в PagedOut #3 (страница 55 в PDF).
Уязвимость была вызвана слабым созданием каталога, используемого PyInstaller во время выполнения бинарного файла. PyInstaller создает каталог _MEIPIDX во временном каталоге пользователя и помещает туда различные вещи, например dll интерпретатора Python, используемую для выполнения кода Python, упакованного в PE-исполняемый файл.
Проблема этого процесса заключалась в том, что каталог, создаваемый для NT AUTHORITY\SYSTEM, был C:\Windows\Temp, что позволяло кому-либо как угадать его, так и записывать в него. Таким образом, возможен перехват DLL, например, при выполнении интерпретатора Python. Вот коммит, исправляющий уязвимость. Вместо использования только стандартных API-функций для создания каталога разработчики реализовали собственную функцию для большего контроля над созданием каталога.
Поскольку это был мой первый POC, я расскажу о паре проблем, с которыми столкнулся по пути.
Прежде всего, настройка среды для этого POC не была особенно сложной, так как всё, что было нужно — это правильная версия пакета. Однако когда я установил его, он вылетал с ошибкой
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)
После небольшого поиска я понял, что проблема в моей версии Python (я использую 3.8.10). Моя версия Python затруднила установку любой другой версии 3.8.x, так как при установке установщик находил мою версию Python38 и выдавал ошибку. Я также попытался собрать другую версию 3.8, но не смог, потому что требовалась Visual Studio 2015, а у меня 2022. В итоге я решил загрузить Python 3.7.5, который работал отлично. Поэтому я создал виртуальную среду для версии 3.7.
Я понял, что настройка среды может варьироваться от уже готовой до требующей много времени для правильной настройки. Хотя это важно, это может отнять часть удовольствия от непосредственной эксплуатации нашей цели.
В нашем процессе эксплуатации два шага: найти каталог процесса, упакованного PyInstaller, и записать туда нашу DLL-эксплойт. Для первой части мы можем найти PID через стандартные функции WINAPI (CreateToolhelp32Snapshot, Process32First, Process32Next). Это сработало гладко. Затем нам нужно найти последний номер каталога _MEI. Оригинальный эксплойт предлагает использовать только функцию GetFileAttributesA и проверять, возвращается ли статус FILE_ATTRIBUTE_DIRECTORY. Однако у меня это не сработало. Моим решением было создать файл в каждом кандидате в каталог, и если файл создавался успешно, значит, каталог существовал. По какой-то причине статус, возвращаемый для файла из GetFileAttributesA, был FILE_ATTRIBUTE_ARCHIVE, поэтому я проверяю его. Прежде чем продолжить, цикл при поиске PID нужен, потому что мы хотим внедрить наши DLL при выполнении целевого процесса, чтобы они были готовы до начала загрузки.
Для DLL нам нужно перехватить DLL, которую импортирует интерпретатор Python (python37.dll). Чтобы получить импортируемые интерпретатором DLL, мы можем открыть DLL с помощью PE-Bear. Одна из импортируемых системных DLL — version.dll. Таким образом, мы можем создать свою собственную DLL и злоупотребить порядком поиска загрузчика Windows, поместив её во временный каталог процесса, упакованного PyInstaller. Таким образом, когда он захочет импортировать DLL, он импортирует нашу вредоносную DLL, а не правильную.

Однако это не сработает. Причина в том, что импорт функции, которую вызывает интерпретатор Python из version.dll (конкретно VerQueryValueW из изображения), не разрешается. Поэтому программа вылетает. Чтобы решить эту проблему, нам нужно сделать проксирование DLL. Короче говоря, мы настраиваем нашу вредоносную DLL на экспорт функций оригинальной DLL и приносим оригинальную DLL переименованной, чтобы она могла загрузить её и вызывать функцию оттуда. Итак, для нашего эксплойта мы скомпилируем вредоносную DLL с DllMain, которая позволит нам выполнить код. Мы экспортируем все функции version.dll и скопируем оригинальную системную version2.dll и поместим её в тот же каталог. Таким образом, version.dll сможет перенаправлять вызовы функций на version2.dll. И как только у нас есть эти DLL, всё, что нам нужно сделать — скопировать их в каталог процесса и ждать выполнения кода. Для лучшего объяснения проксирования/перехвата DLL прочитайте связанную статью.

Хотя в ретроспективе эти шаги просты и понятны, при разработке POC это было не так. Некоторое время потребовалось, чтобы понять процесс эксплуатации, и как только я начал писать код, я стал лучше его понимать. Что касается DLL, я попытался скомпилировать её самостоятельно так, как подразумевалось в репозитории POC Alter Solutions. У них есть отдельная DLL для выполнения кода и отдельная для проксирования, которая загружает payload.dll во время выполнения (DllMain вызывается при загрузке). Однако я не смог правильно её скомпилировать. В конце концов, прочитав вышеупомянутую статью, я решил использовать репозиторий DLLProxyProject, который скомпилировал нужную мне DLL и может использоваться в целом для создания DLL для проксирования. Я попытался скопировать файл DLLMain.cpp и скомпилировать его с заголовочным файлом exports.h. И хотя он запускался, он выдавал ошибку:
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):
Этой ошибки можно избежать с помощью файла Utils.cpp, поэтому я решил оставить этот проект для компиляции моей DLL (хотя было бы лучше, если бы я сделал это полностью самостоятельно).
После настройки среды (использовал psexec для получения оболочки NT AUTHORITY\SYSTEM) я запустил эксплойт, запустил целевой бинарный файл и получил выполнение кода с правами администратора.

Это был очень хороший опыт, и я рад, что прошёл через него. Я действительно хочу сделать больше POC для других CVE, и это была идеальная первая цель для POC. Чтение описания уязвимости было интересным, так как я понял, что основная сложность реализации POC самостоятельно заключается в заполнении пробелов в вещах, которые автор не объяснил (намеренно или нет), и просто в понимании того, что вы читаете. Это была простая уязвимость, поэтому я не приложил много усилий для её понимания. В ближайшем будущем, после того как сделаю ещё пару, я попробую сделать POC для уязвимости повреждения памяти.