Поиск Dependency Confusion в корпоративном GitLab
Не так давно на слуху была новость о векторе атаки Dependency Confusion. Это довольно простой, но в тоже время опасный вектор, позволяющий выполнять произвольный код. Давайте рассмотрим эту проблему с точки зрения команды ИБ.
Суть атаки
Современный сервис редко пишется полностью с нуля. Для решения большинства задач уже написаны библиотеки под все возможные языки. Зачем писать велосипед, если можно взять готовую библиотеку, модуль или пакет? Вашу задачу уже решили в другом внутреннем проекте? Можно пошарить приватную библиотеку. Именно тут узкое место возможны проблемы при работе с зависимостями. Если есть два пакета с одинаковым названием, но один в локальном репозитории, а другой находится в публичном репозитории, то сборщик может отдать предпочтение второму. Про саму атаку написано уже достаточно много, лично мне понравилась статья от Хакер'а. Я хочу рассказать о способе поиска уязвимых зависимостей в корпоративной кодовой базе.
Synopsys в 2020 году провела исследование исходного кода 1 253 проектов. Оказалось, что 99 % проектов содержат open-source компоненты. При этом сами проекты на 70 % состоят из open-source.
Вы сами можете загрузить свой пакет в публичные репозитории:
PHP, composer, https://packagist.org/
Python, pip, https://pypi.org/
JavaScript, npm, https://www.npmjs.com/
И т.д.
Ищем багу у себя
Как искать уязвимые зависимости? По сути, нужно найти во всех проектах все файлы зависимостей и проверить, нет ли среди них ссылок на несуществующие публичные репозитории. В нашем GitLab около 500 проектов, далеко не все из них покрыты Security Pipeline (и далеко не всем это требуется), поэтому нужна какая-нибудь автоматизация.
Есть пара инструментов для автоматической проверки наличия пакетов в публичных репозиториях: confused и repo-diff. Они принимают на вход файл с зависимостями и выдают, есть ли в нём ссылки на несуществующие в публичных репозиториях зависимости. У GitLab API, очевидно, есть возможность отдать файл, нужно только сформировать правильную ссылку.
Файлы зависимостей бывают разные, они могут лежать в разных местах проекта. А могут вообще быть проекты без зависимостей, и тогда нужен поиск по файлам. Для решения этой задачи я воспользовался open-source проектом gitlab-search. Этот пакет хорошо справляется с поиском файлов по имени, хотя и приходится немного фильтровать вывод. Ниже пример запуска поиска из bash:
gitlab-search setup --api-domain <server> <token>
gitlab-search -f composer.json '{' | grep <server> | grep composer.json | awk -F "#" '{print $1}' | sort -u | sed 's/\t//g'
gitlab-search -f composer.lock '{' | grep <server> | grep composer.lock | awk -F "#" '{print $1}' | sort -u | sed 's/\t//g'
gitlab-search -f package.json '{' | grep <server> | grep package.json | awk -F "#" '{print $1}' | sort -u | sed 's/\t//g'
gitlab-search -f requirements.txt '==' | grep <server> | grep requirements.txt | awk -F "#" '{print $1}' | sort -u | sed 's/\t//g'
gitlab-search -f pom.xml 'xml' | grep <server> | grep pom.xml | awk -F "#" '{print $1}' | sort -u | sed 's/\t//g'
...
Получается следующая архитектура софтины:
Поиск файлов зависимостей в проектах GitLab (gitlab-search).
Скачивание файлов (GitLab API).
Проверка зависимостей файлов (confused).
Скрипт состоит примерно из 200 строк. Шарить получившийся код нет особого желания, поскольку, как и любая обертка над существующими инструментами, он написан на коленке, и я уверен, что вы могли бы написать лучше =). Если возникнет необходимость тиражировать решение, то не помешает собрать под него Docker-контейнер. В окружении должны быть, как минимум, npm и confused.
Мне удалось найти одну потенциально уязвимую зависимость в двух проектах. Попробую провести эксплуатацию. Для этого создам модуль с именем pkg-resources и дождусь сборки проекта (или инициирую сборку сам). Но проблема в том, что в моем случае этот модуль нигде не вызывается и живет только в requirements.txt. Значит, код модуля не будет выполнен. Более того, эта зависимость является следствием бага pip и пакет с таким именем нельзя загрузить в pypi.
Эксплуатация
На этот раз не получилось найти реального примера уязвимости. Использовать её не позволяет правильно используемая область видимости зависимостей. Что ж, смоделируем синтетический вариант на примере pypi, раз уж зависимости Python обратили на себя мое внимание.
Прежде всего необходимо написать приложение, которое импортирует уязвимую зависимость. Затем соберем Python-модуль (мануал по сборке, исходник получившегося модуля). Помимо метаданных и структуры самого пакета достаточно написать простейший код для демонстрации уязвимости. Далее нужно собрать пакет и загрузить его в pypi.
python3 -m build
python3 -m twine upload --repository pypi dist/*
Теперь нужно установить вредоносную библиотеку, рекомендую использовать виртуальное окружение Python (venv).
python3 -m venv venv
source venv/bin/activate
pip3 install -r requirements.txt
Но установка и наличие вредоносного пакета в системе еще не приводит к его выполнению. Чтобы модуль выполнился, в исполняемых файлах уязвимого приложения должна находиться конструкция import, при этом будет выполнен код, содержащийся в файле __init__.py.
Как лечить найденные баги
В моем случае с pkg-resources это даже не уязвимость, достаточно удалить строчку с зависимостью из requirements.txt. В более общем случае стоит посмотреть эту статью Microsoft, в ней подробно расписаны основные варианты фиксов под разные сборщики:
Используйте единый приватный package registry. Большинство сборщиков не приоритизируют источники зависимостей и опросят всё в поисках свежей версии пакета. Публичные пакеты также стоит загрузить в единый приватный package registry.
Используйте пространство имен (scope) зависимостей. Некоторым сборщикам, например, npm, можно явно указать источник зависимостей для скоупа.
Используйте проверки пакетов на клиенте. Например, можно указывать конкретную версию пакета (version pinning), тогда не удастся атаковать с принудительным upgrade’ом. Также можно использовать хеши для контроля неизменности пакетов.
Используйте прокси для скачивания пакетов из интенет. На уровне компании договоритесь о наличии общего списка или нейминге приватных пакетов, например, private_package_name. Запретите на прокси скачивание из сети Интернет пакетов из белого списка или с указанными префиксом