Блог

Охотник за ключами: как утечка Shai-Hulud превратила кражу секретов CI/CD в общедоступный инструмент.

Shai-Hulud
Linux

Охотник за ключами: как утечка Shai-Hulud превратила кражу секретов CI/CD в общедоступный инструмент.

Сервер поднят, nginx работает, fail2ban банит ботов, nftables закрывает всё лишнее. На GitHub настроен self-hosted runner — деплой в один клик. Удобно. И именно через этот runner кто-то читает память вашего CI/CD-процесса, вытаскивает SSH-ключи, токены и секреты окружения, упаковывает всё в зашифрованный архив — и уходит. Без единой строчки в ваших логах.

Это не гипотетический сценарий. Это то, что умеет делать Shai-Hulud — и теперь его исходный код лежит в открытом доступе.

КОМУ ЧИТАТЬ ДАЛЬШЕ

Статья актуальна если на вашем сервере есть хотя бы одно из:

— self-hosted GitHub Actions runner
— разработчики с прямым SSH-доступом на сервер
— деплой через CI/CD с секретами в переменных окружения
— SSH-ключи для деплоя в ~/.ssh/ на сервере

Если у вас чистый production-сервер — nginx, база данных, fail2ban, без runner и без доступа разработчиков напрямую — прямая угроза минимальна. Но раздел с проверкой всё равно стоит пробежать: SSH-ключи и утечка секретов касаются любого сервера.

ЧТО КАСАЕТСЯ СИСАДМИНОВ НАПРЯМУЮ

Shai-Hulud — инструмент прежде всего для атак на разработчиков. Но несколько вещей бьют именно по серверной стороне.

SSH-ключи. Фреймворк собирает всё из ~/.ssh/ — включая ключи деплоя, которые дают доступ к production. Если скомпрометирована рабочая станция разработчика у которого есть ключ от вашего сервера — сервер тоже под угрозой. Ротация SSH-ключей на серверах после инцидента обязательна.

Persistence через systemd. На Linux Shai-Hulud закрепляется через пользовательский systemd-юнит в ~/.config/systemd/user/. Не требует root, не виден в обычном systemctl status. Если на сервере работает скомпрометированный пользователь — юнит живёт тихо и делает своё дело.

Deadman switch. При отзыве GitHub-токена без предварительного отключения persistence — удаляется домашний каталог пользователя на сервере. Если runner работает под отдельным пользователем, потеря его домашнего каталога — это потеря конфигов, ключей, истории.

Self-hosted runner как главная точка входа. Если runner крутится на том же сервере что и production — компрометация runner означает компрометацию всего окружения: переменных, секретов, доступа к базе данных, внутренних сервисов.

ЧТО СЛУЧИЛОСЬ

Группа TeamPCP, стоящая за серией атак на npm и PyPI, опубликовала исходный код своего основного инструмента на GitHub. Репозиторий удалили быстро, но форки успели разойтись по сети. OX Security и Datadog Security Labs сохранили копию и разобрали её детально.

Репозиторий оформлен намеренно: все коммиты датированы 1 января 2099 года, подписаны аккаунтом TeamPCP_OSS. Классический приём — автоматические системы мониторинга путаются с датой, git log выглядит мусорно. К репозиторию уже подтянулись сторонние участники, один из которых предложил добавить поддержку FreeBSD.

Раньше Shai-Hulud был инструментом конкретной группы с конкретными целями. Теперь его архитектура и механики доступны для изучения и копирования всем желающим.

МАСШТАБ И СТАТУС УГРОЗЫ

15 мая 2026 года. Репозиторий появился на GitHub и исчез быстро — но форки уже разошлись. Код нельзя отозвать обратно.

Сколько npm-пакетов реально заражено — неизвестно. OX Security и Datadog анализировали сам фреймворк, а не последствия его применения. Полной картины нет ни у кого.

Домен git-tanstack[.]com на момент анализа был активен. Актуальный статус — проверяйте сами: host git-tanstack.com или в базах threat intelligence.

Ключевое изменение: раньше этим инструментом пользовалась одна группа с конкретными целями. Теперь его логику может скопировать кто угодно — и уже копируют, судя по активности в форках. Патча не будет: это не уязвимость с CVE, это инструмент. Защита только превентивная.

ЧТО ЭТО ТАКОЕ

Не набор скриптов — полноценный модульный фреймворк на TypeScript/Bun. Архитектура разбита на независимые части: загрузчики, сборщики секретов, диспетчер доставки, каналы вывода и модули распространения. Части можно менять не переписывая всё — типичная инженерная работа.

Что ищет на заражённой машине:

— SSH-ключи (всё что найдёт в ~/.ssh/)
— файлы .env
— токены GitHub и npm
— конфиги AWS (~/.aws/credentials)
— конфиги Kubernetes (~/.kube/config)
— секреты HashiCorp Vault
— Docker credentials
— облачные учётные записи GCP, Azure
— криптокошельки
— данные мессенджеров
— конфиги AI-инструментов разработки

ГЛАВНАЯ ТЕХНИКА: ЧТЕНИЕ ПАМЯТИ ЧЕРЕЗ /proc

Это ключевой момент для понимания, почему Shai-Hulud опасен именно на серверах с self-hosted GitHub Actions runner.

GitHub маскирует секреты в логах: если токен попадает в stdout, он заменяется на звёздочки. Но маскировка работает только на уровне логов. Память процесса она не трогает.

Runner.Worker — обычный Linux-процесс с PID. Через /proc/[pid]/mem процесс с тем же UID может читать его память напрямую. Shai-Hulud запускается под тем же пользователем что и runner — например github-runner — находит PID Runner.Worker, читает память — и достаёт все секреты до того как какая-либо маскировка успела сработать. Токен никогда не появился в логах, но уже утёк.

Это не эксплойт ядра. Это легитимный механизм Linux, который используется не по назначению. Именно поэтому его сложно обнаружить стандартными средствами.

Работает это только если на вашем Ubuntu-сервере крутится self-hosted GitHub Actions runner. Если нет — данная техника вас не касается.

PERSISTENCE ЧЕРЕЗ SYSTEMD

На Linux Shai-Hulud закрепляется через пользовательский systemd-юнит. Не системный — именно пользовательский, в ~/.config/systemd/user/. Это важно: такой юнит не требует root для создания, не виден в systemctl status без флага --user, и запускается при логине пользователя.

# Проверить пользовательские юниты текущего пользователя
systemctl --user list-units --type=service

# Посмотреть файлы юнитов напрямую (для любого пользователя, без sudo)
ls -la ~/.config/systemd/user/

# Проверить таймеры тоже
systemctl --user list-timers

# Проверить lingering — признак что юниты настроены на выживание без сессии
loginctl show-user $USER | grep Linger

DEADMAN SWITCH — САМАЯ ОПАСНАЯ ЧАСТЬ

Если Shai-Hulud закрепился через компонент gh-token-monitor, отзыв GitHub-токена без предварительной подготовки запускает удаление домашнего каталога пользователя.

Механика простая: компонент следит за валидностью токена. Как только токен перестаёт работать — выполняется деструктивная команда. Расчёт на то, что администратор обнаружил компрометацию и первым делом пошёл отзывать токены.

Правильный порядок действий при подозрении на заражение:

  1. Сначала найти и отключить persistence:
# Остановить и отключить подозрительные пользовательские юниты
systemctl --user stop [имя-юнита]
systemctl --user disable [имя-юнита]

# Удалить файл юнита
rm ~/.config/systemd/user/[имя-юнита].service
systemctl --user daemon-reload
  1. Только после этого — отзывать токены и менять ключи.

Если сделать наоборот — сначала отозвать токен, потом искать persistence — можно потерять домашний каталог вместе с локальными ключами, конфигами и данными.

КАК ВЫВОДИТ ДАННЫЕ

Три канала вывода, каждый следующий включается если предыдущий недоступен.

Основной: данные сжимаются, шифруются AES-256-GCM, ключ закрывается RSA-4096 — и всё это уходит на git-tanstack[.]com. Домен намеренно похож на легитимную GitHub-инфраструктуру.

Резервный: если C2 недоступен, фреймворк ищет на GitHub специально подписанные коммиты с адресом запасного сервера. Адрес не в конфиге — в метаданных коммита. Стандартный мониторинг такое не замечает.

Аварийный: нет ни основного сервера, ни резервного — данные уходят в зашифрованные JSON-файлы в публично созданных GitHub-репозиториях через украденный токен. Лежат в открытом доступе, но зашифрованы. Атакующий заберёт позже.

КАК РАСПРОСТРАНЯЕТСЯ ДАЛЬШЕ

Скомпрометировав один репозиторий, Shai-Hulud идёт дальше — заражает соседние проекты и npm-пакеты которые от них зависят.

Заражение через репозиторий. Shai-Hulud добавляет два файла в репозитории на которые у него есть доступ: .vscode/tasks.json и .claude/setup.mjs. Первый запускается автоматически при открытии проекта в VSCode, второй — при старте сессии Claude Code. Если ваш CI/CD клонирует заражённый репозиторий и в pipeline есть шаг который запускает эти точки входа — вредоносный код выполняется на сервере в контексте runner.

Заражённые npm-пакеты с поддельной аттестацией. Здесь интереснее. Shai-Hulud публикует заражённые версии npm-пакетов двумя способами. Первый простой — через украденный токен публикует новую версию. Второй хитрее: злоупотребляет механизмом OIDC Trusted Publishing, формируя корректную Sigstore-аттестацию через Fulcio и Rekor. В результате вредоносный пакет получает подтверждённую цифровую подпись и выглядит как легитимная сборка из доверенного CI/CD-источника. Стандартная проверка npm audit такое не поймает — пакет технически подписан правильно.

Если ваш CI/CD делает npm install с зависимостями которые могли быть скомпрометированы — это вектор. Особенно опасно если версии зависимостей не зафиксированы жёстко через package-lock.json или указаны с диапазоном (^1.2.0).

Важно понимать: стандартный npm audit и npm audit signatures этот вектор не обнаружат. Пакет опубликован через легитимный механизм npm и имеет валидную подпись реестра — технически всё чисто. Защита здесь только превентивная: не давать неожиданным версиям вообще попасть в сборку.

# Зафиксировать версии — убрать ^ и ~ из package.json
# "express": "4.18.2"  — правильно
# "express": "^4.18.2" — опасно, может подтянуть заражённую 4.18.3

# Использовать npm ci вместо npm install в CI/CD
# npm ci устанавливает строго по package-lock.json, не обновляет зависимости
npm ci

# Проверить что package-lock.json закоммичен в репозиторий
git status package-lock.json

КАК ПРОВЕРИТЬ СЕРВЕР

Индикаторы компрометации которые можно проверить прямо сейчас:

# Вредоносные файлы фреймворка
find ~ -name "setup.mjs" -path "*/.claude/*" 2>/dev/null
find ~ -name "setup.mjs" -path "*/.vscode/*" 2>/dev/null

# Подозрительные коммиты в репозиториях на сервере
find /var/www /opt /srv -name ".git" -type d 2>/dev/null | while read gitdir; do
    git -C "$(dirname $gitdir)" log --all --format="%ae %s" 2>/dev/null | \
    grep "[email protected]"
done

# Пользовательские systemd-юниты (для всех пользователей с shell)
# Быстрый поиск файлов юнитов без запуска от каждого пользователя
find /home -path "*/.config/systemd/user/*.service" 2>/dev/null
find /home -path "*/.config/systemd/user/*.timer" 2>/dev/null

# Проверить включён ли lingering — без него юниты не выживают без активной сессии
# /var/lib/systemd/linger/ содержит файл для каждого пользователя с включённым linger
# Не требует активной сессии пользователя
ls /var/lib/systemd/linger/
# Для конкретного пользователя:
loginctl show-user USERNAME | grep Linger

# Сетевые соединения к подозрительному домену (нужен IP домена)
# Сначала резолвим
host git-tanstack.com
# Затем ищем по IP в активных соединениях
ss -tnp | grep "ПОДОЗРИТЕЛЬНЫЙ_IP"
# DNS-запросы — systemd-resolved не логирует их по умолчанию
# Для исторического анализа использовать tcpdump на порту 53
# или проверить в /var/log/syslog если есть dnsmasq
grep "git-tanstack" /var/log/syslog 2>/dev/null

# Новые публичные репозитории через украденный токен — проверить вручную
# на github.com в настройках аккаунта: Settings → Repositories
# Также искать репозитории с описанием "Shai-Hulud: Here We Go Again"

Если на сервере есть self-hosted runner:

# Проверить tasks.json в директориях проектов
find /opt /var/www /srv -name "tasks.json" -path "*/.vscode/*" 2>/dev/null | \
    xargs -r grep -l "setup\|mjs" 2>/dev/null

# Логи самого runner
journalctl -u actions.runner.* --since "7 days ago" | grep -i "proc\|mem\|secret"

ЧТО СДЕЛАТЬ ПРЯМО СЕЙЧАС

Если self-hosted runner есть на сервере — закрыть вектор атаки через /proc:

# Сначала проверить текущее значение
cat /proc/sys/kernel/yama/ptrace_scope
# Ubuntu по умолчанию уже выставляет 1 начиная с версии 10.10
# Если значение 0 — сервер не защищён, выставить 1:
echo 1 | sudo tee /proc/sys/kernel/yama/ptrace_scope

# Зафиксировать постоянно через sysctl.d
echo "kernel.yama.ptrace_scope = 1" | sudo tee /etc/sysctl.d/99-ptrace.conf
sudo sysctl -p /etc/sysctl.d/99-ptrace.conf

Проверить значение: 0 — не защищено, 1 — защищено, 2 — только root может использовать ptrace, 3 — ptrace полностью отключён.

Для self-hosted runners рекомендуется минимум значение 1. Значение 2 может сломать легитимные отладочные инструменты — проверяйте на staging.

Общие меры независимо от наличия runner:

# Проверить SSH authorized_keys на аномальные записи
for f in /home/*/.ssh/authorized_keys /root/.ssh/authorized_keys; do
    [ -f "$f" ] && echo "=== $f ===" && cat "$f"
done

Зафиксировать npm-зависимости. Если в CI/CD используется npm — убедиться что package-lock.json закоммичен в репозиторий и pipeline использует npm ci вместо npm install. npm ci устанавливает строго по lockfile и не подтянет неожиданную новую версию пакета.

# Вместо npm install в CI/CD pipeline использовать:
npm ci

Ротация ключей и токенов при подозрении на компрометацию — строго в таком порядке:

  1. Отключить persistence (systemd-юниты пользователя)
  2. Сменить SSH-ключи деплоя на всех серверах — сначала добавить новый ключ в authorized_keys, потом удалить старый
  3. Отозвать GitHub-токены и npm-токены
  4. Сменить секреты окружения в CI/CD (GitHub Secrets, переменные runner)
  5. Проверить AWS/cloud credentials и ротировать если нужно
# Добавить новый deploy key перед удалением старого
echo "новый-публичный-ключ" >> ~/.ssh/authorized_keys

# Убедиться что новый ключ работает — только потом удалять старый
# Удалить старый ключ по его комментарию (последнее поле в строке authorized_keys)
sed -i '/комментарий-старого-ключа$/d' ~/.ssh/authorized_keys

ИТОГ

Shai-Hulud — это не уязвимость ядра с CVE и патчем. Это инструмент который злоупотребляет легитимными механизмами: /proc для чтения памяти, systemd для persistence, Sigstore для маскировки вредоносных npm-пакетов под легитимные.

Для Ubuntu-сервера без self-hosted runner угроза минимальна. Если runner есть — ptrace_scope = 1 закрывает основной вектор, но не отменяет необходимость проверить систему по чеклисту выше.

Главный урок: утечка исходного кода означает что подобные техники теперь будут копировать менее квалифицированные атакующие. Атаки на supply chain через CI/CD — уже не экзотика.

Leave your thought here

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Поддержать автора

Если вам понравилась статья — вы можете поддержать автора.

Crypto donation button by NOWPayments