Читай и выполняй. Как работает эксплоит новой уязвимости в GitLab

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

Автор эксплоита, который мы разберем, — исследователь и разработчик из Австрии Уильям vakzz Боулинг (William Bowling). Он обнаружил, что класс UploadsRewriter при определенных условиях никак не проверяет путь до файла. Это открывает злоумышленнику возможность скопировать любой файл в системе и использовать его в качестве аттача при переносе issue из одного проекта в другой.

На этом исследователь не остановился и нашел возможность превратить эту «читалку» в полноценную уязвимость типа RCE. Атакующий может прочитать файл secrets.yml, в котором находится токен для подписи cookie. Специально сформированная и подписанная кука позволяет выполнять произвольные команды на сервере.

Стенд

Тестовое окружение для изучения этого бага поднять очень просто, так как у GitLab есть официальный докер-репозиторий. Можно одной командой запустить контейнер с любой версией приложения. Поэтому поднимем последнюю уязвимую версию — 12.9.0.

Задаем пароль админа после первого запуска GitLab

Дальше нам нужно создать два любых проекта.

Создаем два репозитория на тестовом стенде

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

Чтение локальных файлов

Итак, сразу к делу — проблема находится в функции копирования issue.

Создадим в проекте Test новый issue.

Создание нового issue в GitLab

При создании можно описать детали проблемы в формате Markdown, а еще загрузить произвольный файл, например скриншот с ошибкой или лог-файл, чтобы упростить жизнь разработчикам.

Прикрепление файла к описанию возникшей проблемы

Все загруженные файлы складываются на диск в папку /var/opt/gitlab/gitlab-rails/uploads/. За это отвечает класс FileUploader

doc/development/file_storage.md

Сначала генерируется рандомная hex-строка, которая будет именем папки.

app/uploaders/file_uploader.rb

А имя файла используется то, которое передали при загрузке.

app/uploaders/file_uploader.rb

После загрузки аттача ссылка в формате Markdown вставляется в описание проблемы. Сохраним ее.

GitLab позволяет перенести issue из одного проекта в другой, что бывает очень полезно, если ошибка касается и другого продукта того же разработчика.

Эта кнопка перемещает сообщения о проблемах между проектами

После нажатия на кнопку выбираем проект, куда хотим отправить issue.

Выбор проекта для перемещения issue

Во время перемещения в старом проекте issue закрывается и появляется в новом.

Старый issue в новом проекте

Причем аттачи копируются, а не переносятся. То есть для них создаются новые файлы и ссылки на них, соответственно.

Прикрепленные файлы копируются при перемещении issue

Посмотрим в коде, как выполняется перенос. Все роуты, которые касаются issues, можно найти в папке routes в файле issues.rb. Там в том числе есть роут move, который отвечает за перенос. Именно он обрабатывает пользовательский POST-запрос с необходимыми параметрами.

config/routes/issues.rb

Затем мы попадаем в одноименную функцию.

app/controllers/projects/issues_controller.rb

Здесь вызывается Issues::UpdateService.new, в качестве аргументов передаются ID текущего проекта, пользователь, который инициировал перенос, и проект, куда нужно перенести issue. После этого управление переходит к классу UpdateService. Он, в свою очередь, вызывает метод move_issue_to_new_project.

app/services/issues/update_service.rb

app/services/issues/update_service.rb

Следующую часть уже выполняет класс Issues::MoveService — это наследник Issuable::Clone::BaseService.

app/services/issues/move_service.rb

Здесь сначала вызывается метод execute из дочернего, а затем из родительского класса.

app/services/issues/move_service.rb

В родителе нас интересует вызов метода update_new_entity.

app/services/issuable/clone/base_service.rb

После создания нового issue в целевом проекте этот метод выполняет перенос данных из оригинального issue.

app/services/issuable/clone/base_service.rb

За копирование отвечает ContentRewriter.

app/services/issuable/clone/content_rewriter.rb

На данном этапе нам интересен только метод rewrite_description, который копирует содержимое описания ошибки.

app/services/issuable/clone/content_rewriter.rb

Наконец мы добрались до rewrite_content. Здесь и вызывается метод, который дублирует аттачи старого issue в новый. Этим занимается Gitlab::Gfm::UploadsRewriter.

Он парсит содержимое описания issue в поисках шаблона с аттачем.

app/uploaders/file_uploader.rb

lib/gitlab/gfm/uploads_rewriter.rb

И если находит, то копирует этот файл.

lib/gitlab/gfm/uploads_rewriter.rb

app/uploaders/file_uploader.rb

app/uploaders/file_uploader.rb

Как видишь, ни find_file, ни copy_to, ни copy_file никак не проверяют имя файла, а значит, любой файл в системе может легким движением руки превратиться в аттач.

Чтобы это проверить, воспользуемся методом выхода из директории при помощи стандартного ../. Нужно только определиться с количеством ходов наверх. По дефолту полный путь до загружаемых файлов в контейнере GitLab такой, как на скриншоте.

Путь к аттачам GitLab на диске

Полный путь до картинки из моего issue будет выглядеть следующим образом:

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

Попробуем прочитать файл /etc/passwd. Редактируем описание issue и добавляем необходимое количество ../ в пути к файлу. Я рекомендую ставить их побольше, чтобы точно попасть куда нужно.

Path traversal в имени прикрепляемого к issue файла

Теперь сохраняем и переносим файл в другой проект.

Успешная подмена прикрепленного файла через path traversal в GitLab

Появилась возможность скачать файл passwd, и если это сделать, то ты увидишь содержимое /etc/passwd.

Чтение локальных файлов через path traversal в GitLab

Таким образом можно читать все, на что хватает прав у пользователя, от имени которого работает GitLab. В случае с Docker это git.
Тогда возникает другой вопрос: а что же интересного можно прочитать?

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *