Специалисты по информационной безопасности обнаружили один из самых сложных инцидентов в сфере атак на цепочки поставок программного обеспечения. Вредоносная версия официального пакета intercom-client для Node.js (версия 7.0.4) была загружена в реестр npm и за несколько часов успела поразить множество систем, прежде чем её удалили. Анализ показал, что злоумышленники создали не просто похитителя учётных данных, а полноценного червя с четырьмя механизмами распространения, двойной социальной инженерией и многоуровневым шифрованием.
Описание
Инцидент начался 30 апреля 2026 года, когда система мониторинга цепочек поставок зафиксировала подозрительные изменения в пакете intercom-client. Предыдущая версия была чистой, а в версии 7.0.4 появились три новых файла и одна строка в package.json, которая превращала каждую команду npm install в выполнение удалённого кода без какого-либо подтверждения со стороны пользователя. После обнаружения пакет был изъят из реестра, но последствия продолжали развиваться самостоятельно: заражённые ветки репозиториев, троянизированные пакеты, опубликованные через украденные токены, и автоматические точки входа в VS Code и Claude Code.
Сама атака начиналась с файла-загрузчика setup.mjs. При выполнении npm install запускался скрипт, который загружал официальный JavaScript-рантайм Bun с серверов GitHub и с его помощью исполнял обфусцированную полезную нагрузку. Использование Bun вместо стандартного Node.js позволяло обойти множество защитных механизмов: перехватчики NODE_OPTIONS, песочницы npm, системы обнаружения на основе сигнатур node.exe и среды анализа, которые обычно следят только за процессами Node. Для сетевого монитора загрузка Bun с официального релизного URL выглядела как обычное развёртывание инструмента разработчиком в среде непрерывной интеграции.
Загрузчик был спроектирован с запасом надёжности: он поддерживал Linux x64, Linux arm64, Alpine/musl, macOS x64, macOS arm64, Windows x64 и Windows arm64. Для извлечения zip-архива использовался собственный парсер на чистом Node.js, чтобы не зависеть от утилиты unzip, которая может отсутствовать в минимальных контейнерах. Если на машине уже был установлен Bun, загрузчик завершал работу, не запуская полезную нагрузку - атака была нацелена именно на контейнеры и агенты сборки, где Bun отсутствует.
Основной файл полезной нагрузки router_runtime.js весил 11,7 мегабайта и представлял собой строку из 65 536 символов. После деобфускации специалисты получили 221 771 строку читаемого JavaScript-кода. Все конфиденциальные строки в нём были зашифрованы с использованием собственного шифра: мастер-ключ выводился через PBKDF2 с 200 тысячами итераций и солью, а каждый байт заменялся по таблице перестановок, сгенерированной на основе SHA-256. Такое количество итераций нехарактерно для простого сокрытия строк - это была намеренная мера для замедления анализа.
Полезная нагрузка перед началом своей работы проводила четыре проверки. Одна из самых примечательных - геофильтр по русскоязычному окружению. Скрипт проверял локаль через Intl API, переменные окружения LC_ALL, LC_MESSAGES, LANGUAGE и LANG, а также Windows-специфичные параметры. Если обнаруживался префикс "ru", выполнение немедленно прекращалось. Такой шаблон характерен для операторов из Восточной Европы, которые стараются избежать внимания правоохранительных органов стран СНГ.
Если вредоносная программа обнаруживала, что работает не в среде непрерывной интеграции, она пыталась демонизироваться: порождала дочерний процесс и завершала родительский, так что npm install возвращался пользователю без заметной задержки. В средах CI демонизация не требовалась, поскольку управление жизненным циклом процессов берёт на себя платформа. Всего скрипт распознавал 30 различных платформ CI, включая GitHub Actions, GitLab CI, CircleCI, Travis, Jenkins и другие. Эта информация включалась в отправляемые злоумышленнику данные.
Основная задача червя - кража всех возможных токенов и ключей. Для этого было реализовано 11 коллекторов, работающих в три фазы. Сначала три "быстрых" коллектора проверяли локальное окружение: файловую систему (более 90 путей на Linux, 80 на macOS и 12 на Windows - включая AWS, Azure, GCP, SSH, npm, PyPI, конфигурации ИИ-ассистентов и даже криптовалютные кошелёк), выполняли команду gh auth token для извлечения токена GitHub CLI и считывали полное окружение process.env. Самым агрессивным оказался сборщик, который на GitHub Actions Runner запускал встроенный скрипт Python с sudo. Скрипт искал процесс Runner.Worker, читал всю его память через /proc и извлекал фрагменты, содержащие секреты, хранящиеся в JSON-формате с полем "isSecret":true. Примечательно, что стандартный автоматический токен github_token при этом отфильтровывался - атакующим были нужны только долгоживущие ключи.
После сбора локальных данных запускались семь облачных коллекторов. Они поочерёдно пытались аутентифицироваться в AWS (через STS, Secrets Manager и SSM Parameter Store), Azure Key Vault, GCP Secret Manager, Kubernetes Secrets, HashiCorp Vault, а также через GitHub API для извлечения секретов Actions. Каждый коллектор использовал все доступные способы аутентификации: переменные окружения, файлы токенов, метаданные экземпляров, веб-токены OpenID Connect. В AWS Secrets Manager скрипт обходил все секреты через постраничный перебор и вызывал GetSecretValue для каждого. В AWS SSM Parameter Store он перебирал параметры страницами по 50 и извлекал значения группами по 10 с экспоненциальной задержкой при превышении лимитов - это указывает на то, что разработчики тестировали код против крупных учётных записей.
Все собранные данные перед отправкой шифровались. Для каждой жертвы генерировался уникальный сеансовый ключ AES-256, который затем шифровался открытым ключом RSA-4096 злоумышленника. Основной ключ был извлечён из полезной нагрузки и опубликован. Математически прочитать украденные данные без приватного ключа невозможно, поэтому жертвам и командам реагирования приходится исходить из наихудшего сценария - считать скомпрометированными все возможные секреты.
Для передачи данных использовался основной канал: HTTPS POST на домен zero.masscan.cloud по пути /v1/telemetry. Если сервер был недоступен, включался запасной механизм: червь создавал на учётной записи GitHub жертвы публичный репозиторий со случайным названием из прилагательных и существительных на тему "Дюны" (всего 256 000 возможных комбинаций) и описание "A Mini Shai-Hulud has Appeared". В этот репозиторий коммитились зашифрованные файлы результатов. Коммит маскировался под сообщение "chore: update dependencies". Если токен имел достаточно прав, в сообщение коммита дополнительно встраивался сам украденный токен.
Ещё более изощрённой была система ротации командного центра. Если основной сервер становился недоступен, червь искал через GitHub API коммиты, содержащие строку "beautifulcastle", проверял их цифровую подпись с помощью второго встроенного открытого ключа RSA-4096 и только при успешной верификации принимал новый URL как команду. Это предотвращает захват канала управления третьими сторонами, которые могли бы опубликовать фальшивые коммиты.
Четыре механизма распространения делали ситуацию особенно опасной. Первый - OIDC-обмен токенами: если вредоносная программа запускалась внутри официальной сборочной линии intercom-node на GitHub Actions, она использовала собственный токен OpenID Connect для запроса токена npm и публикации троянизированной версии того же самого пакета. Второй механизм - публикация через украденный npm-токен: червь проверял, имеет ли токен флаг bypass_2fa (обход двухфакторной аутентификации) и право писать пакеты, и если да - он троянизировал и публиковал все пакеты, к которым у токена был доступ, потенциально поражая сразу десятки или сотни проектов.
Третий механизм - внедрение рабочего процесса в GitHub Actions. Червь создавал новую ветку с именем "dependabout/github_actions/format/setup-formatter" (опечатка в слове dependabot - намеренная, чтобы обойти правила защиты веток). В эту ветку коммитился файл .github/workflows/format-check.yml, который маскировался под форматировщик кода. На самом деле он содержал строку ${{ toJSON(secrets) }}, которая сбрасывала все секреты репозитория в переменную окружения, записывала их в файл и загружала как артефакт сборки. Рабочий процесс запускался автоматически по событию push, без участия человека. Червь дожидался завершения выполнения и скачивал артефакт со всеми секретами.
Четвёртый механизм - отравление веток с подделкой под Claude AI. Червь добавлял в каждую ветку репозитория пять файлов: .vscode/tasks.json (запускался при открытии проекта в VS Code), .claude/router_runtime.js (основная полезная нагрузка), .claude/settings.json (запускался при старте сессии Claude Code), и два экземпляра загрузчика в обеих директориях. Автор коммита подписывался как claude@users.noreply.github.com с сообщением "chore: update dependencies". В среде, где разработчики привыкли к автоматическим коммитам от ботов, такая запись могла пройти незамеченной. Любой разработчик, клонировавший отравленную ветку и открывший её в VS Code или запустивший Claude Code, автоматически заражался.
Таким образом, червь реализовал пять различных векторов заражения: прямой npm install, заражение через CI/CD с перепубликацией пакетов, внедрение рабочего процесса, срабатывающее без участия человека, автоматический запуск при открытии проекта в VS Code и аналогичный запуск при работе с Claude Code. Все эти векторы самоподдерживались: заражённая машина могла дать новые токены, которые позволяли распространяться дальше.
На момент публикации анализа эксперты сообщили, что обнаружили несколько активных репозиториев с описанием "A Mini Shai-Hulud has Appeared". Попытки расшифровать хранящиеся в них данные не удались из-за использования RSA-4096. Всем, кто устанавливал версию 7.0.4 пакета intercom-client или клонировал репозиторий, к которому были применены описанные техники, настоятельно рекомендуется считать скомпрометированными все секреты, ключи и токены, и немедленно их заменить. Отсутствие возможности определить, какие именно данные похищены, диктует самый консервативный подход к реагированию.
Индикаторы компрометации
URLs
- https://zero.masscan.cloud/v1/telemetry
SHA1
- b07c01bb29b6ef2fe9e2161fb1b99a9d6a17b67e
SHA256
- 214a8867e1bbb66c6603461d72fcc3baff1352aceb398d9e3c6e7ef06051d986
- 5ae8b2343e97cc3b2c945ec34318b63f27fa2db1e3d8fbaa78c298aa63db52ed
- e71ba441d172460c01fdde2c1a9bc80f432456a70b55f625d21aa6ed77e6f49c
- fe64699649591948d6f960705caac86fe99600bf76e3eae29b4517705a58f0e2