Взлом LiteLLM 1.107.0 в PyPI и Docker: как стилер в cook.pth крал ключи без импорта пакета
litellm 1.107.0 оставалась доступной на PyPI менее двух часов и ставила стилер, который срабатывал при запуске интерпретатора в заражённом окружении. Разбираем механику cook.pth, вывод расследования Upwind и план зачистки.
21 октября 2026 года на PyPI попала заражённая версия litellm 1.107.0 — популярного шлюза к API языковых моделей. Стилер учётных данных и загрузчик удалённого кода находились во всех семи wheel-сборках, версия оставалась доступной в индексе 1 час 43 минуты. Параллельно теги latest, main-latest и main-stable Docker-образов litellm указывали на скомпрометированную сборку v1.107.0. В исходном посте исследователи Upwind отметили, что сами образы не инспектировали; по обновлению от 17:13 UTC теги к этому моменту были отвязаны.
Опасность в механизме запуска: при обычном запуске интерпретатора Python в заражённом окружении модуль site — он загружается при каждом стандартном старте без флага -S — автоматически выполняет строку из cook.pth и импортирует sitecustomize.py, поэтому вредоносный код срабатывает даже тогда, когда приложение ни разу не импортирует сам пакет litellm. Срабатывает даже запуск pip из командной строки. Ниже — хронология, механика внедрения, полный список добычи стилера, вывод расследования Upwind о сборке из репозитория-двойника и план зачистки.
- Заражены оба канала поставки: все 7 wheel-сборок 1.107.0 на PyPI и теги
latest,main-latest,main-stableв ghcr.io и docker.io, указывавшие на заражённый образv1.107.0. - Запуск без импорта:
cook.pthсо строкойimport cookиsitecustomize.pyисполняются при каждом обычном запуске интерпретатора в заражённом окружении, включая сам pip. - Добыча шире API-ключей: SSH, AWS, Azure, GCP, Kubernetes, Docker, пароли и базы
Web Dataбраузеров Chromium, сетевая конфигурация хоста, учётные данные задач ECS, секреты CI/CD, токены Claude, Cursor и Codex, а при заданномDATABASE_URL— таблицы ключей провайдеров из базы прокси. - Вторая стадия спит: фоновый процесс раз в час проверяет файл
domain.txtна GitHub и при появлении адреса скачает и исполнит новый код; по данным issue, файл пуст с 09:15 UTC 20 октября.
Хронология: меньше двух часов на PyPI, до 17:13 UTC в Docker
Wheel-сборки собрал maturin 1.9.4 около 11:21 UTC 21 октября, на PyPI они попали в 11:39–11:40 UTC. В 12:06 UTC исследователи Upwind опубликовали issue 47826: вредоносный код есть во всех семи wheel-сборках, а в sdist лежит изменённый pyproject.toml. К 13:23 UTC PyPI убрал версию — она находилась в индексе 1 час 43 минуты. К 17:13 UTC от заражённых образов отвязали теги latest, main-latest и main-stable в реестрах ghcr.io/berriai/litellm, ghcr.io/berriai/litellm-database, ghcr.io/berriai/litellm-non_root и docker.io/litellm/litellm.
| Время, UTC | Событие |
| 11:21 | maturin 1.9.4 собирает wheel-сборки 1.107.0 с внедрёнными файлами |
| 11:39–11:40 | wheel-сборки загружены на PyPI |
| 12:06 | Upwind публикует разбор в issue 47826 |
| до 13:23 | PyPI убирает версию 1.107.0 |
| до 17:13 | теги latest, main-latest и main-stable отвязаны от заражённых образов |
Почему Python исполняет вредоносный код без импорта litellm
Внедрение держится на двух стандартных крючках интерпретатора. Первый — файлы .pth в site-packages: при старте модуль site читает их и выполняет строки, начинающиеся с import. cook.pth весит 12 байт и содержит единственную строку import cook — этого достаточно, чтобы cook.py стартовал вместе с каждым запуском виртуального окружения или контейнера. Второй крючок — sitecustomize.py: если файл с таким именем лежит на sys.path, модуль site импортирует его при каждом стандартном старте интерпретатора — то есть во всех случаях, кроме запуска с флагом -S.
Пакет доставляет оба файла в корень site-packages через механизм data-файлов wheel: внутри архива они лежат как litellm-1.107.0.data/purelib/, и pip распаковывает их мимо каталога litellm. В итоге любой запуск интерпретатора — приложение, тесты, миграции или вызов pip — исполняет вредоносный код. Поэтому правильная зачистка начинается с ручного удаления файлов и остановки фонового процесса, и только после этого можно запускать pip.
# Содержимое крючка автозапуска: весь файл, 12 байт (чтение файла безопасно) cat "$VIRTUAL_ENV/lib/python3.12/site-packages/cook.pth" # import cook # Безопасный поиск следов компрометации средствами ОС (без запуска Python!) # Никогда не запускайте затронутый интерпретатор до проверки файловой системы. find / -name "cook.pth" -o -name "cook.py" 2>/dev/null grep -rn "litellm.dev" / --include="sitecustomize.py" --include="*.pth" 2>/dev/null grep -rn "1.107.0" / --include="requirements*.txt" --include="uv.lock" --include="poetry.lock" 2>/dev/null ls -la /tmp/.update_state /tmp/.update.lock 2>/dev/null
Что именно собирал стилер privcollect
По статическому разбору Upwind, cook.py — обёртка из zlib и base64 вокруг стилера, который называет себя privcollect. Он стартует один раз в фоновом потоке и не зависит от управляющего сервера: собирает всё локально, шифрует и отправляет. Список добычи:
- SSH-ключи из
~/.ssh, истории shell, баз данных и REPL; - расшифрованные пароли, cookies и базы
Web Dataбраузеров Chromium, файлыlogins.jsonиkey4.dbFirefox; - сетевую конфигурацию хоста: вывод
ss -tulpn, таблицы маршрутов и arp; - учётные данные AWS, Azure, gcloud, Kubernetes, docker, gh, rclone и
.netrc, включая учётные данные задач AWS ECS с проверкой в STSGetCallerIdentity; - секреты CI/CD: токен сервис-аккаунта Kubernetes, содержимое
/run/secrets, конфигурации gitlab-runner, Jenkins, Vault, Terraform и Packer; .git-credentials,.npmrc,.pypirc, учётные данные maven, gradle, cargo, gem и composer, файлы.env*,*.pem,*.keyи*.pfx;- токены и конфигурации локальных ИИ-инструментов: Claude, Codex, Gemini, Qwen, Cursor, Windsurf, Kiro, VS Code MCP и Copilot, токены Hugging Face;
- переменные окружения всех читаемых процессов через
/proc/*/environ; - метаданные облаков: учётные данные роли AWS через IMDSv2, токены сервис-аккаунтов GCP и Azure IMDS;
- GPG-ключи и приватные TLS-ключи из
/etc/ssl/privateи letsencrypt.
Отдельная ветка срабатывает при заданной переменной DATABASE_URL: стилер поднимает prisma и выгружает таблицы LiteLLM_CredentialsTable и LiteLLM_ProxyModelTable — все ключи провайдеров, которые хранит прокси LiteLLM.
Эксфильтрация построена расчётливо: данные шифруются AES-256-CBC, ключ оборачивается RSA-4096-OAEP, результат уходит tar.gz-архивом POST-запросом на www.litellm.dev/healthcheck. Домен litellm.dev зарегистрирован 13 октября 2026 года, за восемь дней до атаки; настоящий домен проекта — litellm.ai. По оценке Upwind, расшифровать перехваченный трафик по сетевому дампу невозможно.
sitecustomize.py добавляет вторую стадию: запускает отсоединённый фоновый процесс с flock на /tmp/remote_code_loader.lock, и тот раз в час читает файл domain.txt из репозитория letters9813/pythonclient-test на GitHub. Если в файле появился адрес, процесс повторяет сбор и исполняет ответ с /codepy через exec(). По данным issue, файл пуст с 09:15 UTC 20 октября — вторая стадия спит, но оператор может включить её одним коммитом. На Windows фоновый процесс не стартует и падает молча, зато cook.py исполняется штатно.
Как заражённый релиз прошёл официальный конвейер сборки
Вредоносного кода нет в git-теге v1.107.0: по данным расследования в треде issue 47826, пакеты собирали из репозитория-двойника Berri-AI/litellm — с дефисом в названии организации вместо оригинального BerriAI/litellm. В pyproject.toml злоумышленники добавили всего одну строку data = "cook" в секцию [tool.maturin]. Одинаковые временные метки у файлов внутри zip-архива wheel указывают, что всё содержимое упаковали единовременно при сборке, а архив после сборки вручную не перепаковывали.
Вредоносного кода нет в git-теге v1.107.0 — его внедрили в дерево сборки до того, как собрали wheel-сборки. Пользователям: закрепите litellm==1.106.2 и не запускайте pip install в затронутом окружении.
Как именно скомпрометировали среду сборки или токен публикации, в issue не утверждается: workflow GitHub Actions, публикующего пакет в PyPI, найти не удалось.
Проверка и зачистка окружения
Сначала диагностика: временные каталоги стилера, маркер эксфильтрации, lock-файл фонового процесса и сами файлы внедрения. Поиск ведите средствами ОС из доверенной среды, не запуская затронутый интерпретатор.
# Признаки работы стилера: временные каталоги и маркер успешной эксфильтрации
ls -d /tmp/.pc_sq_* /tmp/.pc_e_* 2>/dev/null
ls -l /tmp/.pc_once_72129118c59f5636c0bb 2>/dev/null
env | grep -F PRIVCOLLECT_ACTIVE
# Процесс, держащий lock-файл второй стадии
fuser -v /tmp/remote_code_loader.lock 2>/dev/null
# Файлы внедрения во всех site-packages системы
find / -type d -name site-packages -exec sh -c \
'ls "$1"/cook.pth "$1"/cook.py "$1"/sitecustomize.py 2>/dev/null' _ {} \;
Порядок лечения критичен: сперва остановка фонового процесса и ручное удаление файлов внедрения, и лишь потом pip — любой, включая установку «чистой» версии. Каждый запуск интерпретатора в затронутом окружении заново исполняет cook.pth и sitecustomize.py, поэтому установка поверх не вылечит систему. По рекомендации Upwind ручное удаление cook.pth, cook.py и sitecustomize.py с последующим pip install --force-reinstall "litellm==1.106.2" — лишь экстренная мера: во второй стадии присутствовал загрузчик удалённого кода. Предпочтительный и безопасный путь — полностью пересоздать виртуальное окружение или контейнерный образ из доверенных источников с фиксацией litellm==1.106.2, изолировать затронутый хост и провести полную ротацию всех секретов, к которым процесс имел доступ.
# 1. Остановить фоновый процесс до любого запуска pip fuser -k /tmp/remote_code_loader.lock # 2. Удалить файлы внедрения и их байткод в каждом затронутом окружении cd "$VIRTUAL_ENV/lib/python3.12/site-packages" rm -f cook.pth cook.py sitecustomize.py rm -f __pycache__/cook*.pyc __pycache__/sitecustomize*.pyc # 3. Экстренная мера; надёжный путь — пересоздать venv или образ из доверенных источников pip install --force-reinstall "litellm==1.106.2" # 4. Совпадение с таблицей хешей в issue 47826 подтверждает компрометацию sha256sum litellm-1.107.0-*.whl
Таблица SHA-256 в issue 47826 содержит хеши семи заражённых архивов .whl версии 1.107.0: совпадение с ними подтверждает факт компрометации, в том числе кэша CI/CD. Маркер /tmp/.pc_once_72129118c59f5636c0bb пишется только после успешной отправки данных, и его отсутствие ничего не доказывает: контейнеры и очистка tmp стирают улики. Если 1.107.0 хотя бы раз оказалась на хосте с исходящим интернетом, считайте эксфильтрацию состоявшейся.
Ротация обязательна для: роли инстанса или задачи в облаке; всех ключей провайдеров из базы прокси LiteLLM плюс мастер- и виртуальных ключей; пароля от DATABASE_URL; каждого секрета, лежавшего в окружении любого процесса на хосте. В CloudTrail ищите вызов GetCallerIdentity от роли инстанса около времени установки, а затем использование её учётных данных за пределами вашей инфраструктуры.
| Индикатор | Значение |
| Адрес эксфильтрации | POST на www.litellm.dev/healthcheck |
| Домен атаки | litellm.dev, зарегистрирован 13.10.2026 (домен проекта — litellm.ai) |
| Внешнее хранилище второй стадии | GitHub: letters9813/pythonclient-test, файл domain.txt |
| Файлы внедрения | cook.pth, cook.py, sitecustomize.py в корне site-packages |
| Маркеры на хосте | /tmp/.pc_once_72129118c59f5636c0bb, /tmp/remote_code_loader.lock, env PRIVCOLLECT_ACTIVE=1, каталоги /tmp/.pc_sq_* и /tmp/.pc_e_* |
| Аномальный процесс | осиротевший python с рабочим каталогом / |
Исследователи советуют мейнтейнерам перейти на PyPI Trusted Publishing с аттестациями: litellm никогда их не публиковала, и пользователь не может отличить подменённую сборку от легитимной. Пока это не сделано, сверяйте sha256 wheel-сборок с таблицей в issue и закрепляйте образы по digest вместо тега latest. Октябрь выдал сразу несколько инцидентов цепочки поставки: в PHP закрыли две критические дыры, позволявшие исполнить код на сервере ещё до выхода патчей, клиент OpenAI скачивал закрытые данные и молчал об этом, а The Next Web 22 октября 2026 года рассказала о поддельном npm-пакете openai-node, собиравшем API-ключи разработчиков.