javascript

От одного npm‑пакета к кластеру Contagious Interview: практическое расследование supply chain‑кампа…

  • четверг, 20 августа 2026 г. в 00:00:04
https://habr.com/ru/articles/1072054/

В прошлой статье я рассказывал о случайно попавшем ко мне образце ChainVeil - вредоносном ПО из источника, который на тот момент не был зафиксирован в публичных исследованиях.

На этот раз первым сигналом стал мой собственный supply chain‑мониторинг. Он сообщил о новом npm‑пакете plugin-react-vite. На первый взгляд это выглядело как очередной тайпсквоттинг - ничего особенно необычного.

Но я решил пойти дальше: сначала проверить метаданные пакета, затем код, затем инфраструктуру, а уже после этого попытаться понять, кто стоит за образцом и связан ли он с чем‑то уже известным.

В результате расследование ушло далеко за пределы одного package.json: от подозрительной зависимости я пришел к второму этапу размером около 30 тысяч строк, а затем - к нескольким месяцам активности, связанным пакетам, нескольким npm‑аккаунтам и уже известному кластеру.

Именно этот путь я и хочу разобрать.

Начало - plugin‑react‑vite

Первое, что бросилось в глаза, так это схожесть названия с популярным vite-plugin-react. Это классический тайпсквоттинг - знакомая конструкция, лишь слова переставлены местами.

Но само по себе это мало о чем говорит. Поэтому обратим внимание на package.json:

{
  "name": "plugin-react-vite",
  "version": "2.1.3",
  "author": "copperadev",
  "dependencies": {
    "@primno/dpapi": "^2.0.1",
    "axios": "^1.11.0",
    "better-sqlite3": "^12.2.0",
    "express": "^4.21.2",
    "node-machine-id": "^1.1.12",
    "socket.io-client": "^4.8.1"
  }
}

Сразу видно первое несоответствие и, как оказалось, кусочек пазла для дальнейшего расследования: зачем простому React‑плагину все эти зависимости? Уже на этом этапе можно предположить, что ждет внутри кода.

Дальше меня заинтересовал README‑файл - он явно был позаимствован из другого пакета. В нем находилось руководство по установке velora‑kit. Но вы спросите: «Пакет же называется plugin‑react‑vite, что еще за velora?». И тут вы окажетесь правы, я задался ровно таким же вопросом, и именно это несоответствие стало ниточкой, ведущей к целому кластеру пакетов.

А пока вернемся к анализу React‑плагина.

Функционал пакета

Открыв index.js, я обнаружил незамысловатый загрузчик следующей стадии полезной нагрузки. Код формировал URL вида https://<C2>/icons/106, делал GET‑запрос c заголовком client_id: 106 и получал JSON с обфусцированным кодом в поле credits. На этом этапе я получил C2-адрес в качестве артефакта для атрибуции.

В ходе анализа второй стадии полезной нагрузки мне удалось сократить ~30 000 строк исходного обфусцированного кода до ~400 строк логики. И тут начинается самая интересная часть функционала.

Вторая стадия: Стиллер вкупе с RAT

Код работал как на Windows, так и на macOS:

  • Windows: Читает файл Local State, достает зашифрованный ключ, снимает с него защиту через нативный модуль @primno/dpapi и дешифрует базу Login Data.

  • macOS: Исполняет системную команду security find-generic-password -s "Chrome Safe Storage", деривирует ключ через PBKDF2 и дешифрует данные через AES-128-CBC.

Для работы с LevelDB‑хранилищами расширений на macOS использовалась библиотека level - данные с определенным префиксом расшифровывались в памяти. А чтобы базы не блокировались запущенным браузером, перед чтением файлы копировались во временные директории.

Вспомним, что это также RAT‑ник - при получении команды wallets код не делает дамп всей базы кошелька на диск. Он читает LevelDB через fs.createReadStream, извлекая Ethereum‑адреса по регулярному выражению /0x[a-fA-F0-9]{40}/g, и передает только список уникальных адресов на C2.

Целевыми расширениями браузера оказались MetaMask, Coinbase, TronLink, Ronin, Trust Wallet, Exodus, Electrum, Guarda и др.

Для рекурсивного поиска файлов .env, hardhat.config.ts запускались нативные утилиты PowerShell или find. Параллельно воровались целые каталоги ~/.aws, ~/.config/gcloud и ~/.azure.

Одновременно с этим, код предоставлял оператору команды удаленного управления:

  • exec - выполнение произвольных команд.

  • dir и cd

  • read_file

  • ss_upf и ss_upd - загрузка произвольных файлов с зараженной машины на C2.

Стоит отметить, что в коде также реализован бэкдор: при перехвате сигнала SIGINT код форкает процесс express.js с параметром detached: true. Это позволяет сохранить доступ к системе после остановки основного скрипта.

Развязка - расследование цепочки пакетов

В ходе анализа кода я собрал достаточно много артефактов, по которым можно было начать искать связанные пакеты. Я начал с исследования velora-kit, упомянутого в README. Код пакета оказался идентичен тому, что находился в пакете React‑плагина, а что самое важное - идентичным оказался package.json и содержал строку:

  "author": "copperadev"

С этого момента я начал искать данную строку по другим npm‑пакетам, и неожиданно для себя, мне удалось обнаружить несколько совпадений.

Пакет

Автор (package.json)

Maintainer

Дата публикации

polymarket‑gamma‑api

copperadev

byteranger318

2026–03-16 02:51:58

rollup‑plugin‑polyfill‑connect

copperadev

julius‑dev

2026–04-29 18:19:57

react‑icon‑svgs

copperadev

julius‑dev

2026–05-08 18:08:11

velora‑kit

copperadev

leonardo0908

2026–08-12 01:56:41

vexium‑kit

copperadev

leonardo0909

2026–08-13 12:58:44

tailwind‑toolkit

copperadev

chanstealer318

2026–08-13 19:37:45

tailwind‑plugin‑kit

copperadev

ninja318

2026–08-14 08:35:00

config‑helper‑kit

copperadev

nami318

2026–08-18 13:02:29

plugin‑react‑vite

copperadev

aurorasmith100

2026–08-18 17:35:36

И вот здесь одиночный пакет начал превращаться в часть кластера. Но совпадение по copperadev не является достаточным доказательством, поэтому вспомните package.json из самого начала статьи:

{
  "author": "copperadev",
  "keywords": [
    "react",
    "helper",
    "svg"
  ],
  "dependencies": {
    "@primno/dpapi": "^2.0.1",
    "axios": "^1.11.0",
    "better-sqlite3": "^12.2.0",
    "express": "^4.21.2",
    "node-machine-id": "^1.1.12",
    "socket.io-client": "^4.8.1"
  }
}

Я не просто так сказал, что он стал кусочком пазла в расследовании - в большинстве пакетов эти поля полностью идентичны, таким образом я получил своеобразный fingerprint этих пакетов.

После этого я вернулся в самое начало - к README пакета. Оказалось, что velora‑kit уже присутствует среди зафиксированных npm‑пакетов кампании Contagious Interview. Большинство представленных выше пакетов по данным исследователей также были связаны с Contagious Interview, в том числе и polymarket‑gamma‑api, появившийся еще в марте этого года. В этот момент связь plugin‑react‑vite с кампанией становится сильнее.

Maintainer‑аккаунты: случайность или паттерн?

Здесь появляется еще один интересный индикатор - у нескольких аккаунтов повторяется суффикс 318.

Но я не стал интерпретировать это как прямой IOC: такой суффикс вполне может быть случайностью. Но вместе с одинаковым author, одинаковыми зависимостями и повторным использованием одного кода он становится еще одним элементом общей картины.

В результате получается своеобразный граф:

От метаданных к коду

Я уже рассказал про код в пакете React‑плагина, но что насчет остальных пакетов на графе?

После поверхностной деобфускации кода всех представленных выше пакетов оказалось, что логика во всех них практически идентична. Единственным существенным отличием был C2-адрес: в пакете polymarket-gamma-api он отличался от того, что использовался в августовских релизах. Однако по структуре, функционалу конечной полезной нагрузки, способу ее загрузки и используемым модулям код всех пакетов был одинаков.

Изначально это навело меня на мысль о Malware‑as‑a-Service, где эти числа могли бы быть идентификаторами разных покупателей панели. Однако, как вы видели, предположение оказалось неверным. Тот факт, что за всеми этими пакетами с разными client_id стоит один и тот же автор, разрушает гипотезу MaaS. На данный момент я предполагаю, что это внутренние идентификаторы волн кампании или версий сборки одной и той же группы разработчиков.

Заключение: почему контекст важнее одного совпадения

Я не начинал это расследование с мыслью, что это Lazarus, чтобы потом подгонять факты под эту гипотезу. Атрибуция сложилась как пазл, когда разрозненные артефакты начали указывать в одну сторону.

Почему с высокой долей уверенности можно связать этот кластер с кампанией Contagious Interview, которая в публичных исследованиях связывается с активностью Lazarus?

    1. Единая кодовая база и метаданные: Одинаковый автор, идентичные наборы зависимостей и общая архитектура многоступенчатой загрузки.

    2. Тактика обхода модерации: разные пакеты публиковались через разные maintainer‑аккаунты, при этом поле author и другие метаданные сохранялись. Такая схема затрудняет кластеризацию пакетов по одному npm‑аккаунту и может усложнять реакцию на блокировку отдельных публикаций.

    3. Совпадение профилей: Вектор доставки, цель и механика полностью совпадают с публичными отчетами исследователей о Contagious Interview.

    Если вы сталкивались с похожими случаями подозрительной активности в npm‑пакетах или у вас есть вопросы по разбору кода, добро пожаловать в комментарии. Будет интересно обсудить.