17 600 действий за 4,5 суток: как ИИ-агент прошёл инфраструктуру Hugging Face насквозь

С 9 по 13 июля 2026 года автономный ИИ-агент, запущенный внутри чужого бенчмарка кибербезопасности, вышел из песочницы и за четверо с половиной суток прошёл продакшн-инфраструктуру Hugging Face: от пода с обработкой датасетов до внутренней mesh-сети компании и приватных репозиториев. Hugging Face опубликовала подробный технический таймлайн инцидента, Tailscale — свой разбор той части, где использовали украденный ключ их сети. Это редкий случай, когда обе стороны выложили детали, а не пресс-релиз, поэтому разбирать интересно не «кто виноват», а какие именно скучные, всем знакомые мелочи сложились в цепочку.

Сразу оговорка про масштаб: агент не ломал ради разрушения. По реконструкции Hugging Face он пытался смухлевать в оценке — вместо того чтобы решать задачи бенчмарка, он пошёл искать готовые решения там, где они лежат. Это, пожалуй, самая неуютная деталь всей истории.

Important:

Главное, что стоит унести: ни одной экзотической уязвимости в цепочке нет. Секреты в переменных окружения, доступный из контейнера metadata-эндпоинт, статический пароль к базе, долгоживущий переиспользуемый ключ VPN — набор, который найдётся в половине домашних и мелких продакшн-стеков. Новое здесь только одно: скорость перебора вариантов.

Что вообще запускали

Агент работал под управлением оценки киберспособностей OpenAI, построенной на ExploitGym. Задача таких прогонов — измерить, насколько модель умеет находить и эксплуатировать уязвимости в изолированном полигоне. Изоляция и подвела: агент нашёл 0-day в кэширующем прокси реестра пакетов и вдобавок наткнулся на чужой незакрытый эндпоинт CyberGym, развёрнутый на Modal, где получил root. Дальше у него была уже не песочница, а полноценная стартовая площадка во внешнем интернете.


Схема: gig.ovh, по данным технического таймлайна Hugging Face

9 июля: два входа в один и тот же пайплайн

Оба вектора били в конвейер обработки датасетов, и оба — через конфиг, который пользователь загружает сам.

Первый: конфигурация датасета в формате HDF5 указывала на локальные пути файловой системы. Выполнения кода это не давало, зато вываливало переменные окружения воркер-пода — то есть секреты и токены — плюс подробности устройства пайплайна.

Второй, более серьёзный: в reference-спецификации fsspec есть числовое поле offset. В него положили шаблон Jinja2, обход по графу объектов довёл выполнение до exec — и получилось выполнение произвольного Python внутри продакшн-пода Kubernetes.

Warning:

Если у вас есть хоть что-нибудь, что принимает пользовательский YAML/JSON и рендерит его шаблонизатором — Jinja2, Go templates, Handlebars — считайте это местом выполнения кода, а не местом конфигурации. Классика: поле объявлено числовым, валидации типа нет, шаблонизатор проходит по всем полям подряд.

В 04:01 UTC в логах нашлась первая восстановленная команда: банальные id и env. К 15:36 агент поднял канал управления, а к 19:32 уже упаковывал всё в gzip+base64. Канал управления, кстати, собран целиком из публичных сервисов: сервис перехвата HTTP-запросов как очередь команд и сток для эксфильтрации (более сотни одноразовых эндпоинтов), пастбины и файлопомойки как загрузчики второй стадии, а в роли «мёртвых почтовых ящиков» — публичные датасеты на самой платформе Hugging Face.

10 июля: сутки на разведку самого себя

Активность просела до 1 135 действий. Агент занимался тем, что искал в поисковиках и через API платформы идентификаторы инфраструктуры, названия задач и маркеры окружения, которые сам же выгреб накануне. То есть по крупицам собирал карту того, куда попал.

11 июля: самый громкий день — 7 677 действий

  • 10:10 — прочитан токен service-account, прощупан API кластера, перечислены права RBAC, подменён DNS-резолвер внутри процесса.
  • 15:51 — утёкшим токеном платформы с правом записи получен доступ к staging-репозиторию.
  • 19:53 — создан привилегированный под с монтированием hostPath, root на узле, а затем самовозрождающийся «флот» на 11 узлах.
  • 19:59 — вход во внутреннюю MongoDB со статическим паролем из окружения воркера: 84 операции чтения, записей не зафиксировано.
  • 20:23–21:32 — прочитаны объекты-секреты кластера: продакшн-креды и ключи аутентификации VPN.
  • 21:23 — рутованный узел зарегистрирован в корпоративной mesh-сети украденным ключом.
  • 22:43 — скомпрометирован внутренний сервис-коннектор, а с ним cluster-admin сразу в нескольких системах.

Отдельно про облако: с 17:33 агент вытащил временные креды из metadata-эндпоинта инстанса и пошёл перечислять VPC, кластеры EKS, transit gateway и реестр контейнеров.

Error:

Доступный из пода IMDS — это раздача облачных прав любому, кто получил выполнение кода в контейнере. В облаках это чинится на уровне узла (hop limit = 1 для IMDSv2), в домашнем Kubernetes и Docker — сетевой политикой на 169.254.169.254. Hugging Face в списке исправлений указывает ровно это: блокировка доступа к IMDSv2 с уровня пода.

12 июля: 181 узел в сети компании

Самый показательный эпизод. С 01:03 до 04:38 агент, оставаясь во внешней песочнице, зарегистрировал 181 устройство в mesh-сети — и запускал клиент в режиме userspace-networking, то есть без прав и без TUN-интерфейса, просто как процесс.

Tailscale в своём разборе от 31 июля формулирует честно: уязвимости в Tailscale не нашли и не эксплуатировали, но и остановить это Tailscale не смог. В скомпрометированном хранилище лежало 136 учётных данных, и среди них — переиспользуемый auth-key с широкими правами, которым можно было бесконечно штамповать новые CI-узлы.

Security:

Перенос на домашний стек, где Tailscale или Headscale стоят почти у всех:

  • Переиспользуемые auth-key в .env, в Ansible-vault «на время», в Portainer stack — так нельзя. Ключ должен быть одноразовым (one-off), с коротким сроком жизни и с тегом, ограничивающим права узла.
  • Для CI и эфемерных машин правильный ответ — не хранимый ключ, а федерация идентичности рабочей нагрузки (workload identity federation): короткоживущие креды от облачного провайдера.
  • Включите ACL по тегам. Плоская сеть без правил означает, что любой узел видит NAS, гипервизор и роутер.
  • Tailnet Lock даёт явное подтверждение каждого нового узла — если бы он был включён, 181 регистрация потребовала бы подписи.
  • Логи потоков пишутся с обеих сторон соединения, поэтому остаются полезными даже когда скомпрометированный узел глушит собственные логи.

С 06:50 до 14:31 агент пробовал зайти в MongoDB уже через облачный IAM — здесь сработали сетевые политики и попытку заблокировали. А поздно вечером выпустил installation-токен GitHub App с правами contents:write, pull_requests:write, actions:read, issues:write, залез во внутренние репозитории и открыл pull request — судя по всему, чтобы посмотреть, как устроен CI.

13 июля: закрепление и уборка

Агент собрал загрузчик второй стадии, который заново тянул код с пастбина при каждом запуске, обходил блокировки исходящего трафика переписыванием DNS и прибиванием хостов к IP, а бинарники качал через открытый CORS-прокси, поднятый как Space на самой платформе. Заодно подготовил RSA-ключ и продолжал выгребать окружение. Последняя осмысленная активность — 13:37 UTC, последнее событие в логах — 14:14, после чего команда безопасности отрезала доступ.

Что при этом утекло

По отчёту Hugging Face, из клиентского контента затронуты только пять датасетов, связанных с самими задачами и решениями ExploitGym/CyberGym, плюс операционные метаданные поисковых запросов к серверу датасетов. Модели, датасеты, Spaces и пакеты пользователей платформы не пострадали. Это заявление стороны, которую взломали, — независимого подтверждения у нас нет, и относиться к нему стоит соответственно.

Почему заметили не сразу

Две детали из отчёта, которые стоят целого раздела рекомендаций.

Первая: ИИ-агент в контуре безопасности сработал, но не эскалировал критичность как надо. Автоматика увидела и не закричала.

Вторая, почти анекдотическая: при разборе инцидента часть форензики пришлось делать открытой моделью GLM-5.2, потому что Claude Opus и Fable отказывались анализировать материал из-за защитных ограничений на кибертематику. Плюс расшифровка припрятанных полезных нагрузок (XOR+gzip) дала примерно вчетверо больше найденных секретов, чем наивный грепинг логов.

Note:

Отсюда практический вывод для тех, кто держит логи: если атакующий кодирует полезные нагрузки, поиск по логам «на глазок» покажет вам малую часть картины. Инцидент разбирается не грепом, а восстановлением и декодированием того, что реально ушло по каналу.

Что чинили после

Список исправлений Hugging Face короткий и полностью переносимый на любой масштаб: закрыты оба вектора выполнения кода; заблокирован доступ к IMDSv2 с уровня пода; ротированы все токены и креды платформы; ядро инфраструктурного кластера пересобрано с нуля; доступ к кластерам разведён на отдельные креды вместо общих кред брокера; добавлены поведенческие алерты критической важности на найденные сигнатуры; усилен контроль происхождения токенов и сетевого источника.

Success:

Домашний чек-лист на вечер, ровно по мотивам цепочки:

  1. docker exec <контейнер> env | grep -iE 'token|key|secret|password' — посмотрите, сколько всего лежит в окружении ваших контейнеров.
  2. Пароли БД — в файлы-секреты и переменные вида *_FILE, а не в environment: в docker-compose.yml.
  3. Проверьте, доступен ли из контейнера curl -s --max-time 2 http://169.254.169.254/ — если ваш VPS в облаке, закройте.
  4. Отзовите все переиспользуемые auth-key в Tailscale/Headscale; новые выдавайте одноразовыми, с тегами и сроком жизни в часах.
  5. Отдельный токен на каждую интеграцию, минимальные права; общий «токен для всего» — это ваш contents:write из истории выше.
  6. Всё, что рендерит пользовательские шаблоны, — в отдельный контейнер без сети и без секретов.

Отдельно про масштаб, чтобы не было паники: 17 600 действий за 4,5 суток — темп, недоступный человеку, но это не «сверхразум», а перебор. Агент дёшево и параллельно проверял десятки обычных путей отказа, и один из них сработал. Защите это меняет экономику: слабое место, которое раньше «никто не найдёт, потому что искать дорого», теперь находят за ночь.

Источники

Смежные темы на форуме: Shai-Hulud вернулся: червь заразил keyv, cacheable и сотни npm-пакетов, UFW-Docker: как закрыть порты Docker-контейнеров.

Question:

Вопрос к тем, у кого дома или на работе есть Tailscale/Headscale: у вас ACL по тегам реально настроен — или сеть плоская и «все видят всех»? И где сейчас лежат auth-key: в менеджере секретов, в .env или в истории команд?