Охотник за ключами: как утечка Shai-Hulud превратила кражу секретов CI/CD в общедоступный инструмент.
Охотник за ключами: как утечка 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-токена без предварительной подготовки запускает удаление домашнего каталога пользователя.
Механика простая: компонент следит за валидностью токена. Как только токен перестаёт работать — выполняется деструктивная команда. Расчёт на то, что администратор обнаружил компрометацию и первым делом пошёл отзывать токены.
Правильный порядок действий при подозрении на заражение:
- Сначала найти и отключить persistence:
# Остановить и отключить подозрительные пользовательские юниты
systemctl --user stop [имя-юнита]
systemctl --user disable [имя-юнита]
# Удалить файл юнита
rm ~/.config/systemd/user/[имя-юнита].service
systemctl --user daemon-reload
- Только после этого — отзывать токены и менять ключи.
Если сделать наоборот — сначала отозвать токен, потом искать 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
Ротация ключей и токенов при подозрении на компрометацию — строго в таком порядке:
- Отключить persistence (systemd-юниты пользователя)
- Сменить SSH-ключи деплоя на всех серверах — сначала добавить новый ключ в authorized_keys, потом удалить старый
- Отозвать GitHub-токены и npm-токены
- Сменить секреты окружения в CI/CD (GitHub Secrets, переменные runner)
- Проверить 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 — уже не экзотика.
