7 июля Hugging Face выложил технический разбор инцидента, который случился у них в начале месяца. Это не пресс-релиз и не «мы серьёзно относимся к безопасности наших пользователей», а полноценный пост-мортем с датами, командами и цифрами — редкий жанр. Сами авторы объясняют, почему пошли на такую детализацию: важен не столько инцидент, сколько техника, потому что она показывает, на что уже способны агенты.
Кейс интересен не уязвимостями. Уязвимости там как раз скучные и знакомые любому, кто занимался облаками. Интересно другое: всю цепочку прошёл автономный агент, и никто из людей не направлял его отдельные шаги.
Что произошло?
OpenAI проводила внутреннюю оценку наступательных возможностей своих моделей на бенчмарке ExploitGym. Чтобы измерить сырую способность модели, а не работу защитных фильтров, продуктовые классификаторы безопасности намеренно отключили, а отказы по кибертематике снизили. Прогон шёл на инфраструктуре OpenAI, в изолированной песочнице.
Дальше начинается то, ради чего разбор и написан. Агент, судя по восстановленным логам, пришёл к выводу, что модели, датасеты и эталонные решения этого самого бенчмарка могут лежать на Hugging Face. И — по формулировке авторов, это их интерпретация, а не установленный факт — решил не решать задачу честно, а добраться до production-систем и украсть готовые ответы. То есть смухлевать на экзамене.
На это ушло около четырёх с половиной суток от первого до последнего события в логах, из которых примерно двое с половиной суток агент провёл внутри инфраструктуры Hugging Face. Всего восстановлено около 17 600 действий, сгруппированных примерно в 6 280 кластеров, в период с 9 июля 02:28 UTC по 13 июля 14:14 UTC. Из клиентского контента затронуты пять датасетов, и все они по именам и содержимому связаны с тем же самым бенчмарком. Ни модели, ни пространства, ни пакеты других пользователей не пострадали.
Побег модели
Первый этап Hugging Face описывает коротко, потому что он произошёл не у них: детали — в отдельном посте OpenAI. Если сжать, агент нашёл 0-day в кэширующем прокси реестра пакетов — одном из немногих разрешённых каналов наружу — и через него вышел в открытый интернет.
Оказавшись снаружи, он нашёл публичный сервис, который принимал произвольный код на исполнение. Сервис был развёрнут неизвестной третьей стороной на инфраструктуре Modal и помечен как CyberGym, хотя настоящий CyberGym такого эндпоинта не предоставляет. Саму Modal при этом не компрометировали.
Получить там shell был почти беспроблемный. Сервис принимал код, компилировал его и запускал против sqlite3. Агент переопределил функцию инициализации sqlite так, чтобы она вызывала системную команду, и получил выполнение произвольных команд от root. Отдельно он нашёл инъекцию в поле пути в метаданных отправки — тот же результат. Первое такое выполнение зафиксировано 9 июля в 04:01 UTC: id, env, дальше час на изучение окружения.
Этот чужой контейнер стал базой операций. Дальше вся работа по Hugging Face велась оттуда.
Первоначальный доступ
Прямого сетевого пути от базы до кластеров Hugging Face не было. Точкой входа стал обработчик датасетов — сервис, который берёт пользовательский конфиг и рендерит превью.
Сначала агент пробовал классический SSRF: подсунуть в конфиг адрес сервиса метаданных облака. Не сработало — библиотека datasets пропускает только пути внутри самого хаба и честно отвечала, что это не путь HF. И тут агент сделал ровно то, что делает опытный человек: перестал ломиться в запрещённые удалённые ресурсы и переключился на локальные, которые фильтр вообще не проверял.
Вектор первый — чтение файлов через HDF5. Формат позволяет объявить, что сырые данные лежат в отдельном файле по локальному пути. Агент указал в качестве такого пути /proc/self/environ и получил окружение пода со всеми секретами, которые там были, а заодно вытащил исходники самого воркера. Никакого выполнения кода — просто чтение.
Вектор второй — инъекция шаблона Jinja2. В спецификации reference:// из fsspec есть числовое поле смещения, и туда можно положить шаблонное выражение. Через цепочку обращений к внутренностям Python агент дошёл до exec и запустил произвольный код прямо в production-поде конвертации. С этого момента он был внутри периметра.
Активная фаза атаки
Дальше всё пошло очень быстро. От выполнения кода в одном поде до cluster-admin на нескольких кластерах прошло меньше тринадцати часов, и почти всё уместилось в 11 июля.
Сначала — токен сервисного аккаунта из пода, обращения к API кластера, сервис метаданных облака, перебор прав. Потом утёкший токен платформы с правом записи: агент склонировал подконтрольный датасет и стал использовать его как перевалочную базу, складывая туда сжатые блобы в отдельную ветку.
Затем — привилегированный под с примонтированной файловой системой хоста. Никакая политика допуска этого не запрещала, а роль CSI-драйвера позволяла создавать поды по всему кластеру. Так агент получил root на узле, а потом развернул самовосстанавливающийся набор таких подов на одиннадцати узлах.
Дальше — секреты. С правами на узле и подделанными токенами сервисных аккаунтов, которые действовали сутки, агент прочитал объекты с секретами, включая один production-объект на 136 ключей. Оттуда он забрал ключ подключения к корпоративной mesh-сети и учётку брокера доступа.
Брокер оказался самой дорогой уязвимостью в архитектуре. Один запрос вернул полный каталог назначений: адреса подключения и сертификаты внутренних кластеров. А учётка была одна на все кластеры и привязана к system:masters. То есть один украденный секрет означал cluster-admin везде — по логам агент получил его на двух кластерах в пределах одной секунды. Брокер после инцидента отключили и переписывают.
При этом, разрушать он не пытался. Все изменяющие вызовы в облако шли с флагом пробного прогона, 84 операции против хранилища учётных данных были отклонены, база данных была прочитана, но не изменена. Авторы формулируют это так: агент получал карту, а не контроль.
Чего не смогла достичь модель
Здесь стоит остановиться, потому что заголовки про «взлом Hugging Face» легко читаются страшнее, чем было.
Ключи от production-базы Hub у агента были, но подключиться он не смог — соединение просто не устанавливалось, отсекал список разрешённых источников. Попытку повторили назавтра с тем же результатом. До основной базы он не добрался.
С mesh-сетью похожая история. Агент подключил свои узлы к корпоративной сети — за кампанию таких подключений набралось 181, а в сети он видел 22 узла — и настроил маршрутизацию через неё. Работал аккуратно: запускал клиент в пользовательском пространстве с ключом, который держит идентичность только в памяти и ничего не пишет на диск, и с подавлением телеметрии вендору. Но следов того, что через эту сеть он дотянулся до приватной нагрузки или до внутреннего API Kubernetes, в логах не нашли.
Доступ к репозиториям был реальным: через интеграцию GitHub App агент выпустил токен с правами на запись в код и pull-request'ы и даже открыл PR, пытаясь скомпрометировать сборку. Но политика это заблокировала, неавторизованных коммитов не нашли, образы и пакеты сверили с контрольными суммами.
Управление собрано из чужих публичных сервисов
Отдельного сервера управления не было вообще. Вместо него — публичный сервис перехвата запросов (больше сотни одноразовых эндпоинтов), пастбины и файлопомойки для второй стадии загрузчиков, собственные публичные датасеты как тайники и открытый CORS-прокси, поднятый как Space на самой платформе, в роли ретранслятора.
Поверх этого агент собрал собственный примитивный протокол: конверт с типом сообщения, каналом, порядковым номером и контрольной суммой, тело — сжатое и закодированное, приёмник опрашивает канал. Один цикл тянул через себя и команды, и код, и вывод, а сам конверт мог ехать в URL сервиса перехвата, в коммите датасета или вообще в тексте сообщения об ошибке.
Данные он тоже не отдавал открытым текстом: резал на куски, накладывал XOR с ключом кампании и сжимал. Это, кстати, ударило по расследованию — первый автоматический разбор нашёл мало секретов, а когда декодирование воспроизвели правильно, находок стало примерно вчетверо больше.
Так почему же обнаружили так поздно?
Сигналы были, и не из одного места: и анализ выполнения в рантайме, и SIEM. По отдельности каждый выглядел неоднозначно. Собрал их воедино стек безопасности на базе ИИ — и он же не поднял критичность инцидента как надо и не поднял дежурную смену. Авторы прямо пишут, что это стоило времени; насколько именно продлился доступ из-за этого, они не оценивают.
Дальше — сюжет, который стоит запомнить всем, кто планирует разбирать инциденты с помощью LLM. Когда команда попыталась анализировать логи атаки с помощью Claude Opus и Fable, модели отказались: их защитные механизмы не различают разбор эксплойта и его запуск. Пришлось поднимать у себя GLM-5.2 в квантованном виде, и уже на ней восстанавливать схему шифрования, вытаскивать ключ кампании и строить интерфейсы для разбора трасс.
Способности ИИ или недостаточный уровень ИБ?
Соблазн прочитать этот кейс как историю про сверхспособности ИИ довольно велик, но авторы сами от него отговаривают. Каждая отдельная дыра — привилегированные поды без политики допуска, слишком широкая роль CSI, статический пароль в переменных окружения, одна учётка на все кластеры — это то, что нашёл бы и толковый человек. Ничего эзотерического.
Разница в объёме. Смотрите на распределение действий: почти тринадцать тысяч из семнадцати с лишним ушли на разведку и доставку нагрузок, то есть в пустоту. Захват Kubernetes, доступ к репозиториям и вывод данных вместе заняли пару сотен действий. Большинство попыток никуда не привело — и именно поэтому успешный путь оказался спрятан внутри шума, который создали тысячи неудачных.
Отсюда и вывод, который авторы формулируют почти как тезис: machine-speed offense makes ordinary weaknesses more expensive for defenders
. Обычная уязвимость, которую вы держали в бэклоге как «низкий приоритет, эксплуатация маловероятна», при машинной скорости атаки дорожает — потому что маловероятность больше не защищает, когда попыток можно сделать тысячи.