Cover photo

Как мы заработали $50 000 на взломе цепочки поставок

Cybred

Cybred

Введение

В 2021 году я всё ещё был в начале своего пути в наступательной безопасности. Я уже взломал несколько компаний и стабильно зарабатывал на Bug Bounty — этичной практике поиска уязвимостей, за которые исследователи получают денежные вознаграждения. Однако я ещё не достиг уровня, на котором можно быстро находить критические уязвимости в целевой системе. Этот навык казался недосягаемым. Всё изменилось, когда я познакомился с человеком, который сыграл ключевую роль в моей карьере Bug Bounty — Snorlhax.

Сначала я видел в нём конкурента. Он был намного выше меня во французском рейтинге HackerOne, что подталкивало меня становиться лучше. Мы начали общаться в Discord, и спустя несколько недель я поделился с ним перспективной программой Bug Bounty. Вскоре после этого он обнаружил критическую уязвимость стоимостью $10 000 — вдвое больше моего самого крупного вознаграждения на этой платформе. Это мотивировало меня пересмотреть тот же проект, и в течение недели я нашёл свою собственную критическую уязвимость в другой категории, также оценённую в $10 000.

Вместо того чтобы конкурировать дальше, мы решили работать вместе. Нашей целью стало выявить все возможные типы уязвимостей в этой системе: IDOR, SQL-инъекции, XSS, мисконфиги OAuth, Dependency Confusion, SSRF, RCE… Мы находили их, отправляли отчёты и получали вознаграждения. Наше сотрудничество длилось годы, и даже сейчас мы время от времени возвращаемся к этой цели.

Но одна цель оставалась недостижимой: найти "Исключительную Уязвимость" — настолько критическую, что её вознаграждение выходило бы за рамки стандартных выплат. Это стало для нас главным вызовом.

И вот история о том, как мы со Snorlhax наконец этого добились.

Используя атаку на цепочку поставок, позволявшую выполнить RCE на компьютерах разработчиков, в сборочных процессах и на продакшен-серверах, мы получили выплату в $50 500. Вот как это было.

Понимание атакуемой поверхности нашей цели

Из нашего опыта мы знали: прежде чем переходить к разведке, важно понимать бизнес-контекст крупной компании. К моменту, когда мы взялись за эту цель, мы со Snorlhax уже заметили закономерность: эта компания часто допускала неожиданные слабые места при поглощении других бизнесов. Недавно приобретённые дочерние компании не всегда соответствовали её стандартам безопасности, особенно на ранних этапах интеграции. Мы видели это и раньше, но никогда не рассматривали приобретённые активы как основную точку входа для поиска по-настоящему уникальной уязвимости. В этот раз мы были уверены: "Исключительная Уязвимость" может скрываться именно там, куда почти никто не смотрит.

Наш план был прост: в этой программе Bug Bounty любая инфраструктура, официально принадлежащая компании, считалась легальной целью. Мы понимали, что многие охотники концентрируются на корневых доменах, в то время как инфраструктура поглощённого бизнеса, устаревшие фреймворки и менее строгие политики безопасности могут создавать идеальные условия для серьёзных уязвимостей. Если где-то и был шанс найти по-настоящему разрушительный баг, то именно там.

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

Почему мы выбрали атаку на цепочку поставок

Мы всегда придерживались идеи, что необязательно атаковать то, что на виду. Гораздо эффективнее нацеливаться на активы и сервисы, которые сама компания затягивает в свою защищённую среду — их конвейеры, зависимости, реестры, образы. Если всё сделать правильно, можно вмешаться в код ещё до его попадания в продакшен, что несёт куда более серьёзные последствия, чем стандартные баги вроде SSRF или XSS.

Чтобы разобраться в поверхности атаки цепочки поставок, стоит взглянуть на SLSA (Supply-chain Levels for Software Artifacts) — фреймворк, который делит цепочку поставок на три ключевых компонента:

  • Источник (Source) – репозитории кода (GitHub, DockerHub, реестры зависимостей);

  • Сборка (Build) – процессы CI/CD;

  • Дистрибуция (Distribution) – доставка артефактов конечным пользователям.

Атака на любой из этих элементов способна привести к серьёзным последствиям. Мы сразу сфокусировались на источнике (GitHub, DockerHub, реестры) и сборке (CI/CD-конвейеры), поскольку они буквально кишат токенами, секретами и плохими конфигурациями.

post image

Мы с Snorlhax уже пробовали тестировать атаки на цепочку поставок в других программах и нашли некоторые сумасшедшие уязвимости: доступы к Artifactory, компрометация электронной почты сотрудников, что давало нам доступ к их GitHub, Dependency Confusion и многое другое. Однако в этот раз у нас было предчувствие: мы думали, что приобретённая компания скорее всего использует устаревшие или плохо управляемые процессы цепочки поставок, что может привести нас к чему-то большему.

Тогда мы решили сочетать две идеи: нацелиться на поглощение и вмешаться в цепочку поставок. Мы искали баг, который мог бы действительно поменять правила игры. Целевая атака на цепочку поставок недавно приобретённых компаний была очень нишевой, нишей внутри ниши, и мы были уверены, что никто другой этим не занимается. Это давало нам огромное преимущество.

Как мы начали разведку

Нашим первым шагом было выбрать подходящую дочернюю компанию. Мы прошерстили пресс-релизы, прочитали официальные объявления и проанализировали LinkedIn, чтобы выяснить, какие компании были приобретены и на каком этапе находится процесс интеграции. Мы выбрали одну компанию, заметив, что она была конкретно указана в программе Bug Bounty нашей цели.

Далее нам нужно было понять, есть ли у этой дочерней компании онлайн-присутствие кода или используются ли популярные реестры пакетов. Мы начали собирать JavaScript файлы с их фронтенд-приложений, чтобы увидеть, какие зависимости они подключают. Вместо простых строковых поисков мы выбрали более серьёзный подход, преобразовав JS файлы в Abstract Syntax Trees (AST).

AST — это древовидное представление исходного кода, которое разбивает всё — переменные, функции, импорты и так далее — на иерархические узлы. Используя библиотеку SWC (Speedy Web Compiler), мы написали код на Rust, который парсил JS файлы в эти AST и систематически их обходил, чтобы найти все import или require операторы.

Это позволило нам точно определить ссылки на уникальную область, которая появлялась как @acquisition-utils/package. Первое, что мы проверили, это можно ли было занять пространство имен npm. Однако компания уже владела этим пространством, но публичных пакетов там не было.

post image

Это означало, что как минимум существует одна частная npm организация. На npmjs можно настроить организацию, но публиковать пакеты приватно, если приобрести лицензию. То есть либо у них действительно есть приватные пакеты на npm, либо они просто заняли пространство имен, чтобы избежать Dependency Confusion и захвата пространства имен.

Следующим шагом было проверить, можем ли мы найти какие-либо приватные пакеты этой организации. Для этого мы использовали следующий путь поиска на GitHub:
**/packages.json @acquisition-utils:

post image

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

Полезный совет: Вы можете использовать любой тип строк, указывающих на использование внутреннего исходного кода. Например, если вы нашли приватный Artifactory, вы можете выполнить запрос path:**/package-lock.json artifactory-url.tld, чтобы проверить, не скачиваются ли lock-файлы с этого корпоративного Artifactory. Конечно, запрос нужно адаптировать в зависимости от используемого пакетного менеджера (например, yarn, pip, pnpm и т. д.).

К сожалению, нам не удалось найти ничего полезного прямо на GitHub, поэтому мы обратились к Google и ввели acquisition-utils, наивно надеясь найти больше артефактов. И вот, мы наткнулись на организацию на DockerHub, также связанную с брендом дочерней компании. Это был тот самый след, который мы искали — среда, в которой могли находиться приватные или плохо защищённые Docker-образы.

post image

Нашим следующим шагом было скачать образы Docker и тщательно их исследовать, надеясь найти следы, которые приведут нас к чему-то действительно критичному.

Погружение в исходный код

При загрузке одного из их Docker-образов, который был назван в честь одного из их основных продуктов, нам повезло. Внутри мы нашли полный исходный код их закрытого бэкенда.

post image

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

Продолжив исследование, мы заметили, что папка .git/ всё ещё присутствовала в контейнере. Это был следующий ключ. Проверив файл .git/config, мы надеялись найти ссылку на приватный репозиторий или переменную окружения. И тут мы нашли нечто, чего никогда не видели раньше в .git/config: Bearer токен авторизации, закодированный в base64.

[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[remote "origin"]
	url = https://github.com/Acquisition/backend
	fetch = +refs/heads/*:refs/remotes/origin/*
[gc]
	auto = 0
[http "https://github.com/"]
	extraheader = AUTHORIZATION: basic eC1hY2Nlc3MtdG9rZW46TG9sWW91V2FudGVkVG9TZWVUaGVUb2tlblJpZ2h0Pw==

Сначала ни один из нас не узнал этот токен с первого взгляда, поэтому мы решили исследовать его. Когда мы разобрались, что это был токен GitHub Actions (GHS), мы поняли, что нам повезло: мы нашли ключ, который был переправлен на стадии сборки их цепочки поставок. Если нам удастся использовать этот токен в своих целях, мы сможем манипулировать пайплайнами, что даст нам доступ к инъекции кода, искажению артефактов или способам перехода в другие приватные репозитории.

Как оказалось, эти токены GitHub Actions часто генерируются автоматически для того, чтобы workflow могли взаимодействовать с собственным репозиторием — пушить код, создавать pull-запросы или получать доступ к приватным зависимостям. Обычно эти токены истекают после завершения workflow, ограничивая окно для эксплуатации.

Однако может возникнуть состояние гонки, если артефакты, содержащие токен (например, файлы .git/config, логи переменных окружения или полные репозитории), загружаются до завершения workflow. Такой артефакт может стать доступным для тех, кто имеет доступ для чтения, до того как токен истечёт. Если злоумышленник успеет скачать артефакт достаточно быстро (в нашем случае это был Docker-образ), он сможет извлечь ещё действующий токен и использовать его для модификации или пуша кода в репозиторий, искажать релизы или даже перейти в другие репозитории GitHub в той же организации.

Этот риск особенно серьёзен, если workflow предоставляет права на запись или админские права для токена (например, через permissions: contents: write в YAML-файле GitHub Actions). В таком случае злоумышленник получит возможность инъекции вредоносного кода, создания новых веток или даже внесения изменений прямо в продакшн. В зависимости от того, как настроен CI/CD пайплайн, вредоносные изменения могут быстро продвинуться вниз по потоку, потенциально компрометируя приложение, которым пользуются миллионы пользователей.

В современных DevOps пайплайнах Dockerfile и CI/CD workflow тесно переплетены. Часто встречается такая схема:

  1. Checkout в Workflow: Workflow GitHub Actions (или другого CI/CD-поставщика) использует действия, такие как actions/checkout, для загрузки исходного кода. По умолчанию на этом шаге могут быть включены git-учётные данные или токены внутри файла .git/config.

  2. Dockerfile COPY: В процессе сборки Docker разработчики часто копируют все исходные директории, включая скрытые папки, такие как .git/, в Docker-образ.

  3. Публикация образа: Собранный образ загружается в публичный или приватный контейнерный реестр. Если чувствительные файлы (например, .git/ или дампы переменных окружения) не были удалены, они остаются в предыдущих слоях или попадают в сам финальный образ.

Для того, чтобы pipeline стал уязвимым, достаточно одного упущения — например, забыть удалить или игнорировать папку .git/ или неправильно ограничить область действия токена. Злоумышленники, обнаружившие эти артефакты, могут использовать GitHub токен, Docker-образ или оба для эскалации своих привилегий.

Итак, могли ли мы выиграть в состоянии гонки? Мы могли, так как воркфлоу GitHub, к которому у нас был доступ (поскольку у нас был полный исходный код бэкенда), выглядел так:

name: Build and push Docker image
on:
  push:
    tags:
      - '*'

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout codebase
        uses: actions/checkout@v3

      - name: Define image name
        run: |
          echo "IMAGE_NAME=acquisition/backend" >> $GITHUB_ENV

      - name: Define image tag
        run: |
          if [[ "${{ github.ref }}" == 'refs/tags/'* ]]; then
            echo "IMAGE_TAG=$(git tag --points-at $(git log -1 --oneline | awk '{print $1}'))" >> $GITHUB_ENV
          else
            exit 0
          fi

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build Docker image
        run: |
          source $GITHUB_ENV
          echo IMAGE_NAME=$IMAGE_NAME
          echo IMAGE_TAG=$IMAGE_TAG
          docker build --build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} --tag $IMAGE_NAME:$IMAGE_TAG .

      - name: Push image to DockerHub
        env:
          DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
          DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
        run: |
          docker login --username $DOCKERHUB_USERNAME --password $DOCKERHUB_PASSWORD
          docker push $IMAGE_NAME:$IMAGE_TAG

      - name: Deploy to staging
        env:
          DEPLOYMENT_API_SECRET: ${{ secrets.DEPLOYMENT_API_SECRET }}
        run: |
          curl -XPOST 'https://deploy.acquistion.tld/v1/deploy' \
            -H "Content-type: application/json" \
            --data-raw "{
              \"appName\": \"backend\",
              \"envName\": \"backend\",
              \"contName\": \"backend\",
              \"imageTag\": \"`echo $IMAGE_NAME`:`echo $IMAGE_TAG`\",
              \"secret\": \"`echo $DEPLOYMENT_API_SECRET`\"
            }"

      - name: Post status on Slack
        id: slack
        uses: slackapi/slack-github-action@v1.24.0
        with:
          payload: |
            {
              "text": "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: "https://hooks.slack.com/services/THE_ACTUAL_SLACK_HOOK_HAHA"
          SLACK_WEBHOOK_TYPE: "INCOMING_WEBHOOK"

Как вы могли заметить, после команды docker push есть ещё два шага: Deploy to staging и Post status on Slack. В это время токен всё ещё будет доступен, так как рабочий процесс продолжит выполнение этих шагов. Реалистично утверждать, что злоумышленник может просто отслеживать публикацию нового образа и скачать Только тот конкретный слой в Docker-образе, который будет содержать GHS токен, чтобы затем после эксплуатации получить доступ к репозиторию на GitHub.

Краткая заметка: Palo Alto's Unit42 опубликовала статью под названием “ArtiPACKED: Hacking Giants Through a Race Condition in GitHub Actions Artifacts”, которая вышла примерно через месяц после того, как мы завершили наше исследование. Это подробное исследование именно такого типа атак. Возможно, мы не были первыми, кто обнаружил эту уязвимость, но статья является отличным примером того, как несколько исследователей безопасности могут независимо открыть и подтвердить одну и ту же категорию уязвимостей — настоящий случай "великие умы думают одинаково". ;)

Мы хотели большего

Когда мы начали изучать Dockerfile, который строил этот образ, мы заметили package.json, который выглядел так:

{
  "name": "content",
  "version": "1.7.0",
  "private": true,
  "scripts": {
   ....
  },
  "dependencies": {
    "@aquistion-utils/internal-react": "^4.12.0",
    ...

  },
  "devDependencies": {
   ...
  },
  
}

В package.json было указано определение для организации @acquisition-utils, которую мы нашли ранее. Однако для того чтобы получить доступ к этому пакету, необходим был npm токен. К сожалению, мы не обнаружили .npmrc файла в корневой папке.

Причина этого заключалась в том, что Dockerfile сначала копировал файл .npmrc, а затем удалял его на последнем шаге сборки. Некоторое время мы думали, что после удаления .npmrc файл исчез навсегда. Мы решили копнуть глубже и улучшить наше понимание работы с Docker.

Docker-образы строятся с использованием слоистой файловой системы (Union File System). Каждая инструкция в Dockerfile (например, FROM, COPY, RUN и т. д.) создает новый слой. При сборке образа Docker смотрит на каждую инструкцию, создает (или повторно использует) слой для этой инструкции и накладывает его на уже существующие слои. Слои являются доступными только для чтения, и когда вы изменяете файл в одном слое, Docker фактически добавляет новый слой с «изменениями», а не редактирует существующие слои. Этот процесс делает сборку Docker более эффективной и позволяет использовать кэш, поскольку несколько образов могут делить общие слои.

Чтобы непосредственно исследовать эти слои, можно использовать команду docker history <image>, которая выводит последовательность инструкций (и соответствующих слоев), использованных для создания образа.

$ docker history hello-world

IMAGE          CREATED         CREATED BY                SIZE      COMMENT
ee301c921b8a   20 months ago   CMD ["/hello"]            0B        buildkit.dockerfile.v0
<missing>      20 months ago   COPY hello / # buildkit   9.14kB    buildkit.dockerfile.v0

Однако команда docker history лишь показывает высокоуровневую историю без возможности исследовать содержимое каждого слоя.

dive — это полезный инструмент командной строки, который не только отображает слои вашего Docker-образа, но и позволяет "погрузиться" в каждый слой, чтобы точно увидеть, какие файлы были добавлены, удалены или изменены в каждом слое. Чтобы начать исследовать ваш образ с помощью dive, установите этот инструмент, а затем выполните команду dive <image_to_dive>:

post image

Гифка взята с официального репозитория.

dive предоставляет интерактивный пользовательский интерфейс, который отображает каждый слой слева и его содержимое справа. Вы можете использовать стрелки на клавиатуре для навигации по файловой системе внутри каждого слоя, а также увидеть изменения между слоями.

dlayer — ещё один полезный анализатор для слоёв Docker. Он может быть использован как в интерактивном, так и в неинтерактивном режиме для отображения содержимого и структуры файлов каждого слоя Docker-образа. Типичный рабочий процесс включает сохранение Docker-образа в tar-файл, а затем его исследование с помощью dlayer:

# Pipe the saved Docker image tar directly into dlayer
docker save image:tag | dlayer -i

# Save the image to a tar file and then analyze it
docker save -o image.tar image:tag
dlayer -f image.tar -n 1000 -d 10 | less

Это может быть особенно полезно, если вы хотите просто передать image.tar через конвейер и вывести все слои, а затем передать результаты в другой инструмент. Кроме того, вы можете использовать Go-код для реализации dlayer как библиотеки, что может быть полезно для масштабного сканирования и разведки.

Когда мы обнаружили, что можно извлечь слои сборки Docker-образа, это означало, что существует высокая вероятность того, что .npmrc (а значит, и NPM_TOKEN) остались открытыми в более раннем слое. Мы извлекли каждый слой и внимательно их исследовали.

И ТУТ ОНО БЫЛО!!!

post image

Мы раскрыли приватный токен npm, предоставляющий доступ на чтение и запись к пакетам @acquisition-utils. Именно тогда наши сердца начали бешено колотиться. Мы поняли, что можем внедрить вредоносный код в один из их приватных пакетов, который автоматически загрузят их разработчики, пайплайны и даже производственные окружения. Поскольку это был приватный пакет, обычные публичные сканеры не обнаружат подмены. А так как в их package.json были установлены версии с возможностью обновлений на минорные версии (^4.12.0), мы могли внедрить шпионскую программу, не будучи замеченными, и скомпрометировать каждое окружение, которое зависело от этого пакета.

На этом этапе мы с Snorlhax были в шоке. Мы знали, что нашли нашу "Исключительную Уязвимость". Речь шла не просто о чтении приватного исходного кода или перехвате одного токена для пайплайна. Мы могли повлиять на всю цепочку поставки программного обеспечения — от локальных машин разработчиков до CI/CD процессов и производственных серверов. Короче говоря, мы нашли потрясающую уязвимость, которая могла бы оправдать выплату за "необычайную" уязвимость.

Возможные действия по пост-эксплуатации и их последствия

Мы тщательно задокументировали каждый шаг. Команда безопасности компании всегда призывала нас выходить за рамки теоретических уязвимостей и демонстрировать реальное воздействие. Мы объяснили, как злоумышленник может внедрить вредоносный код в приватный npm пакет, а затем дождаться, пока разработчики или пайплайны не выполнят команду npm install. Если разработчики случайно построят или протестируют код, использующий этот заражённый пакет, мы могли бы собрать секреты или проникнуть в другие внутренние системы. В CI/CD пайплайнах это могло бы открыть доступ к чтению чувствительных переменных окружения, краже учетных данных или даже эскалации привилегий до самохостинговых агентов. Наконец, в производственной среде это могло бы привести к масштабной компрометации, если эти контейнеры будут загружать обновления тех же пакетов или иметь автоматический процесс развертывания.

Чтобы подчеркнуть угрозу, мы доказали, что никакое внутреннее логирование или мониторинг, скорее всего, не заметят этого вторжения на уровне npm, потому что оно происходило вне инфраструктуры цели. Давайте подумаем. Мы:

  • Загружаем Docker образ с Dockerhub

  • Находим npm токен локально

  • Публикуем пакет в приватной организации на registry.npmjs.org

Мы никогда не взаимодействовали с веб-приложениями цели, и, за исключением логов DockerHub и npm, нет никакой возможности узнать, кто загрузил образ. Самое "шумное" событие — это, возможно, публикация npm пакета, но об этом мы расскажем в следующей статье ;D

Заключение

Когда мы отправили свои результаты, компания сразу поняла, что это уязвимость, способная вызвать цепную реакцию, которая может поставить под угрозу не только один продукт, но и весь жизненный цикл разработки и развертывания. Они классифицировали это как редкую, худшую из возможных уязвимостей, и наградили нас вознаграждением в размере 50 500 долларов, что значительно превысило их обычную структуру выплат. Мы с Snorlhax наконец-то нашли нашу «исключительную уязвимость» — открытие, которое подтвердило все наши догадки и знания, которые мы накопили за годы.

Для нас главным уроком было то, что успех атаки часто зависит от комбинации двух или более недооцененных углов. Приобретения оказались более уязвимыми целями, чем материнская компания, а уязвимости в цепочке поставок программного обеспечения обеспечили катастрофический эффект, который мы искали. Объединив эти идеи, мы нашли идеальный шторм. Мы также получили новое уважение к тому, насколько критически важно защищать не только код, который вы публикуете, но и каждый слой вашего процесса сборки, а также каждый артефакт, который ваши разработчики загружают из внешних источников.

Мы продолжаем работать, всегда в поисках следующего революционного открытия. Наша цель — вдохновить других исследователей на поиск необычных решений. Иногда настоящее сокровище скрывается в самых темных уголках: скрытом Docker-образе, небрежно обработанном npm-токене или оставленной папке .git. Эти уголки могут стать ключом к жизненно важному вознаграждению, как это было для нас.


Оригинал статьи на английском языке.

Переведено и адаптировано специально для @cybred.

Как мы заработали $50 000 на взломе цепочки поставок