Вредоносные пакеты npm: индикаторы атаки для SOC L1 до заражения CI-раннера

В марте 2022 года в npm-реестре появились вредоносные версии библиотеки node-ipc (CVE-2022-23812, CVSS 9.8) — пакета с миллионами загрузок в неделю. npm — менеджер пакетов для JavaScript/Node.js, и если ты работаешь с фронтендом или бэкендом на Node, то npm install для тебя как дыхание. Тот случай — protestware, перезаписывающий файлы пользователей из определённых регионов — стал одним из первых громких примеров supply chain атаки через npm. С тех пор такие кампании идут регулярно: по данным StepSecurity, вредоносные пакеты npm всё чаще содержат обфусцированные стилеры, вытаскивающие десятки категорий секретов. Каждый CI-раннер (сервер, который автоматически собирает и тестирует код), установивший заражённый пакет, рискует отдать атакующему AWS-ключи, GitHub-токены и SSH-ключи. Ниже — разбор конкретных индикаторов, которые аналитик SOC L1 (первая линия Security Operations Center — те, кто обрабатывает входящие алерты) должен видеть в логах до того, как раннер станет точкой входа.
Бизнес-логика атаки: зачем ломают npm и CI-раннеры
Прежде чем разбирать индикаторы — мотивация. Атакующий не ломает npm ради npm. Цепочка такая:
- Скомпрометированный пакет попадает в CI/CD-пайплайн (CI/CD — Continuous Integration / Continuous Delivery, автоматизация сборки, тестирования и доставки кода).
- На CI-раннере лежат секреты: npm-токены, GitHub PAT (Personal Access Token для доступа к API), ключи AWS/GCP/Azure, Kubernetes-конфиги, SSH-ключи.
- Украденные секреты открывают доступ к продакшену — облачным хранилищам, базам данных, внутренним репозиториям.
- Дальше — монетизация: кража данных, развёртывание ransomware, подмена криптокошельков в веб-приложениях.
По данным Microsoft Threat Intelligence, кампания ChainDrop использовала именно этот вектор: вредонос, спрятанный в сотнях npm-пакетов, аутентифицировался в AWS, Kubernetes и HashiCorp Vault через украденные с раннеров токены. Credential theft из CI/CD — основная цель.
В терминах MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1195 — её идентификаторы) supply chain attack на npm покрывает сразу несколько тактик:
| Этап атаки | Что происходит | MITRE ATT&CK |
|---|---|---|
| Компрометация зависимости | Внедрение кода в легитимный пакет | T1195.001 / T1195.002, Initial Access |
| Выполнение payload | JavaScript запускается при установке или импорте | T1059.007, Execution |
| Кража секретов из файлов | Сканирование .npmrc, .env, ~/.aws/credentials | T1552.001, Credential Access |
| Загрузка инструментов | Скачивание TruffleHog, дополнительных payload | T1105, Command and Control |
| Связь с C2 | Обращение к C2-серверу по HTTPS | T1071.001, Command and Control |
| Эксфильтрация | Вывод данных через HTTPS или в GitHub-репозиторий | T1567.002, Exfiltration |
Зачем эта таблица: когда видишь алерт и не понимаешь контекст, поиск по T-коду сразу покажет, к какой стадии атаки относится событие. Привыкай маппить каждый IOC (Indicator of Compromise — конкретный артефакт, указывающий на атаку: хеш, домен, IP) на технику ATT&CK. Это не бюрократия — это язык, на котором ты будешь разговаривать с L2 при эскалации.
Анатомия свежей npm malware кампании
Фишинг мейнтейнера и захват аккаунта
Атака на npm-пакеты начинается не с кода, а с человека. В кампании Shai-Hulud (предположительно, сентябрь 2025) атакующие отправили мейнтейнерам популярных пакетов фишинговые письма от имени npm — с просьбой «обновить настройки двухфакторной аутентификации». Фишинговый домен npmjs[.]help имитировал официальный npmjs.com (требует проверки по первоисточнику). Введённые учётные данные давали атакующему полный доступ к публикации новых версий.
В случае с node-ipc вектор был другой: публикация через аккаунт atiertant, который числился мейнтейнером, но ранее не выпустил ни одной версии (данные StepSecurity). Двадцать один месяц без релизов — затем три одновременные публикации. Паттерн «спящий аккаунт внезапно публикует» — типичный маркер supply chain attack.
Чем это полезно на практике: если у вас настроен мониторинг npm-публикаций для внутренних пакетов, алерт на изменение _npmUser (поле, указывающее публикатора) между версиями — простой и рабочий детект.
Lifecycle hooks, IIFE и обфускация payload
Когда атакующий получает контроль над аккаунтом, он модифицирует пакет одним из двух способов.
Lifecycle hooks. В package.json добавляется скрипт postinstall (или preinstall), который запускается автоматически при npm install. Shai-Hulud использовал postinstall для запуска node bundle.js — файла размером более 3 МБ с обфусцированным (намеренно запутанным) вредоносным кодом, по данным Securelist. Следующая итерация вредоноса (предположительно, ноябрь 2025) переключилась на preinstall — payload срабатывает ещё до завершения установки, обходя сканеры более поздних стадий сборки.
IIFE без хуков. node-ipc пошёл тоньше: никаких preinstall/postinstall в package.json. Вместо этого 80 КБ обфусцированного кода добавлены в конец CommonJS-бандла как IIFE (Immediately Invoked Function Expression — самовызывающаяся функция, которая исполняется в момент загрузки модуля). Payload запускается не при установке, а при первом require('node-ipc') в коде приложения. По данным StepSecurity, это делает его невидимым для инструментов, проверяющих только lifecycle hooks.
Для SOC разница принципиальная. Hook-based payload оставляет след в логах npm install. IIFE-based — только в runtime-логах приложения. Второй тип без runtime-мониторинга не поймать.
Саморепликация через npm publish
Червь Shai-Hulud после кражи npm-токена жертвы находил 20 самых скачиваемых пакетов мейнтейнера, скачивал каждый, добавлял свой bundle.js, прописывал postinstall-хук, инкрементировал версию и публиковал заражённый релиз от имени жертвы (данные Securelist). Полностью автоматизировано: одна скомпрометированная учётная запись порождала десятки заражённых пакетов без участия оператора.
Один пакет zevairouter предположительно получил десятки вредоносных версий за неделю. Другой кластер пакетов, имитирующих сервисы PayPal, был опубликован за считанные минуты с завышенной версией. Завышенная версия обгоняет легитимный внутренний пакет при разрешении зависимостей — это техника dependency confusion. Ещё один пакет, @umschool/platform, тоже был опубликован с завышенной версией.
SOC-алерты и индикаторы компрометации CI/CD пайплайна
Теперь к главному — что конкретно ловить в алертах. Индикаторы разделены на три группы по источнику данных.
Сетевые IOC вредоносных пакетов
Первое, что делает npm-стилер — отправляет украденные данные наружу (эксфильтрация). CI-раннер в нормальном режиме обращается к ограниченному набору доменов: registry.npmjs.org, github.com, ваш внутренний реестр. Всё остальное — повод для алерта.
Конкретные сетевые индикаторы из разобранных кампаний:
- C2-домены (Command and Control — серверы, через которые оператор управляет вредоносом): предположительно
sh.azurestaticprovider.net:443(node-ipc),npm-cache[.]com,pypi-get[.]com,js-mirror[.]com(требуют проверки по первоисточникам). Обрати внимание: домены мимикрируют под легитимную инфраструктуру — Azure, npm, PyPI. - Фишинговый домен
npmjs[.]help(кампания против мейнтейнеров, данные Kaspersky). - Обращения к GitHub API для создания публичных репозиториев с именем
Shai-Huludили описанием «Sha1-Hulud: The Second Coming» — fallback-канал эксфильтрации (данные Unit 42 и Microsoft). - Обращения к Ethereum-контракту (предположительно
0xE1f2395ee43e45A1556EC6438a88c31B83493103, требует проверки) для получения актуального C2-домена. Да, через блокчейн — C2-адрес хранится в смарт-контракте, и вредонос его оттуда вытаскивает.
Что настроить в SIEM (система сбора и корреляции логов безопасности — Splunk, ELK, MaxPatrol SIEM): правило на исходящие HTTPS-соединения из CI-раннеров к доменам вне whitelist.
index=ci_runner sourcetype=network_traffic direction=outbound
| where NOT match(dest_host, "registry\.npmjs\.org|github\.com|your-registry\.com")
| stats count by dest_host, dest_port, src_host
| where count > 5
Запрос фильтрует весь исходящий трафик CI-раннеров, исключает известные легитимные домены и показывает всё остальное. Если в выдаче появляется azurestaticprovider.net или npm-cache.com — это не ложное срабатывание. Пустой результат — нет аномалий. Строки с count > 5 указывают на устойчивые соединения с нетипичными хостами — повод для триажа.
Файловые и процессные аномалии
Вредоносные пакеты оставляют характерные артефакты на файловой системе CI-раннера:
- Файлы в
/tmp/:processor.sh,migrate-repos.sh— bash-скрипты, которые Shai-Hulud создаёт для эксфильтрации секретов через GitHub API (данные Securelist). - Аномально крупные JS-файлы в
node_modules:bundle.js(3+ МБ),setup_bun.js,bun_environment.js(10+ МБ), по данным Unit 42. Легитимные модули редко содержат файлы такого объёма в одном JS-бандле. Если видишь 10-мегабайтный.jsв зависимости — это повод посмотреть внимательнее. - Скачивание TruffleHog (инструмент поиска секретов в коде) из GitHub-репозитория на CI-раннер. Сам инструмент легитимный, но его появление на раннере без указания в pipeline-конфиге — красный флаг.
- Архив
package.tarвнутри npm-пакета вместо архива с именем пакета — характерная черта пакетов, модифицированных Shai-Hulud (данные Securelist). - Фоновые процессы Node.js: Shai-Hulud 2.0 и node-ipc отсоединяют (fork/detach) себя в фоновый процесс, чтобы
npm installзавершился «нормально», а стилер продолжал работать (данные Unit 42 и StepSecurity). В логах процессов это видно как дочернийnode-процесс, живущий после завершения установки.
Если на CI-раннере настроен аудит файловой системы (auditd на Linux), ищи создание файлов в /tmp/ во время npm install и запуск node с аргументами bundle.js, setup_bun.js или bun_environment.js.
Аномалии в npm-реестре и GitHub
Третья группа индикаторов — не на раннере, а в самих реестрах:
- Неожиданная публикация новой версии пакета без соответствующего PR, тега или коммита в исходном репозитории (по данным Microsoft, основной паттерн ChainDrop).
- Резкий рост версий: пакет за день получает десятки патч-релизов.
- Публикация от нового мейнтейнера у давно существующего пакета (как
atiertantв node-ipc). - Появление репозитория «Shai-Hulud» в GitHub-аккаунте разработчика — прямой индикатор компрометации.
- Новая ветка
shai-huludи workflow-файлshai-hulud-workflow.ymlв.github/workflows/(данные Securelist и Trend Micro). - Изменения в
package-lock.jsonбез соответствующего Pull Request — должно блокироваться на уровне CI-политики.
SOC L1 триаж алертов: делай раз, делай два, делай три
SIEM выдал алерт: CI-раннер обращается к домену вне whitelist. Вот пошаговый триаж.
Предпосылки: доступ к SIEM-консоли (Splunk, ELK или аналог), доступ к логам CI-раннера (stdout/stderr пайплайна), базовое понимание структуры npm-проекта.
Шаг 1. Определи контекст запуска. Открой лог пайплайна, в котором сработал алерт. Найди шаг с npm install или npm ci. Проверь, какие пакеты устанавливались — они перечислены в выводе npm. Если видишь строку postinstall или preinstall с запуском неизвестного JS-файла (node bundle.js, node setup_bun.js) — это прямой индикатор вредоносного lifecycle hook. Результат: ты знаешь, какой пакет и какая версия вызвали подозрительную активность.
Шаг 2. Проверь пакет. Выполни npm view <имя>@<версия> --json на чистой машине (не на заражённом раннере!). Сравни поля scripts.preinstall и scripts.postinstall с предыдущей версией. Проверь поле _npmUser — кто опубликовал версию и совпадает ли это с обычным публикатором. Незнакомый аккаунт или появившийся неизвестный хук — подтверждение компрометации пакета. Результат: ты понимаешь, добавлен ли подозрительный хук и сменился ли публикатор.
Шаг 3. Изолируй и эскалируй. Если подтверждается вредоносный хук или неизвестный публикатор: останови все активные пайплайны, использующие этот пакет; ротируй (обнови) все секреты, доступные скомпрометированному раннеру — npm-токены, GitHub PAT, AWS/GCP-ключи, SSH-ключи — причём с заведомо чистой машины; зафиксируй сетевые IOC (домены, IP из логов) и передай инцидент на L2/L3 с маппингом на MITRE ATT&CK T1195.001/T1195.002 и T1552.001. Результат: угроза локализована, секреты ротированы, инцидент эскалирован с достаточным контекстом.
Мониторинг CI/CD пайплайна: превентивная защита CI-раннера
Триаж работает после срабатывания алерта. Но лучше не допустить заражения.
[npm ci](https://docs.npmjs.com/cli/v10/commands/npm-ci) вместо npm install. Команда npm ci устанавливает зависимости строго по package-lock.json с проверкой хешей. Это закрывает dependency confusion — технику, при которой атакующий публикует пакет с завышенной версией, чтобы обогнать ваш внутренний пакет при разрешении зависимостей.
--ignore-scripts для недоверенных пакетов. Флаг предотвращает выполнение preinstall/postinstall. Ломает некоторые легитимные пакеты (те, что компилируют нативные модули), но для CI-среды trade-off оправдан.
npm ci --ignore-scripts
npm audit --omit=dev
Первая команда устанавливает пакеты строго по lockfile без запуска установочных скриптов. Вторая проверяет зависимости на известные уязвимости. Нюанс, который часто упускают: вредоносные пакеты npm не получают CVE-идентификаторов (стандартных номеров уязвимостей), потому что это не баг в легитимном коде, а умышленно созданный вредонос. Классические SCA-сканеры (Software Composition Analysis — инструменты анализа зависимостей) этот класс угроз структурно не видят. Об этом хорошо написали в Xygeni.
Network policy на CI-раннерах. Разрешите исходящие соединения только на whitelist доменов: registry.npmjs.org, github.com, ваш artifact registry. Всё остальное — block и alert. Это единственная мера, которая ловит и hook-based, и IIFE-based payload одновременно: оба типа в итоге делают исходящее соединение к C2.
package-lock.json как security-артефакт. Любое изменение в lockfile без соответствующего PR должно блокировать merge. Это не DevOps-гигиена, а security control: именно через lockfile приходят подменённые версии с завышенными номерами.
Я разобрал три конкретные кампании, но вредоносные пакеты npm — это ритм, а не разовый инцидент. По оценкам исследователей, за несколько недель мониторинга обнаруживаются сотни вредоносных пакетов. Некоторые живут в реестре считанные минуты, другие — несколько часов. Кампания PhantomSync, по имеющимся данным, запускала стилер с задержкой после импорта — чтобы пройти мимо синхронных проверок.
Для SOC L1 из этого следует простое правило: нет алерта на исходящие соединения с CI-раннера в реальном времени — узнаёшь о компрометации из чужого отчёта. Кампания ChainDrop, по имеющимся публикациям, показала, что вредонос выходил из процесса на системах с определённой локалью. Но это артефакт конкретного оператора — следующий может не быть таким избирательным.
Я работаю с CI/CD-безопасностью последние два года и вижу одну устойчивую проблему: SOC-команды мониторят продакшен-серверы, endpoint-станции сотрудников, почтовый трафик — а CI-раннеры остаются в слепой зоне. При этом на раннере лежат ключи от всех перечисленных систем. SOC, не мониторящий CI/CD-инфраструктуру, мониторит половину поверхности атаки. Разработчические машины и раннеры — это уже не «зона ответственности DevOps». Это зона, где хранятся production-секреты, и кто мониторит секреты — тот мониторит раннеры. На IB Basics показывают, что делать в первый месяц после «хочу в ИБ» — и CI/CD-мониторинг в программу входит не случайно.
Эту тему и смежные навыки разбирают на практике в курсе «Аналитик SOC» Codeby Academy.
