Невозможно отучить людей изучать самые ненужные предметы.
Введение в CSS
Преимущества стилей
Добавления стилей
Типы носителей
Базовый синтаксис
Значения стилевых свойств
Селекторы тегов
Классы
CSS3
Надо знать обо всем понемножку, но все о немногом.
Идентификаторы
Контекстные селекторы
Соседние селекторы
Дочерние селекторы
Селекторы атрибутов
Универсальный селектор
Псевдоклассы
Псевдоэлементы
Кто умеет, тот делает. Кто не умеет, тот учит. Кто не умеет учить - становится деканом. (Т. Мартин)
Группирование
Наследование
Каскадирование
Валидация
Идентификаторы и классы
Написание эффективного кода
Вёрстка
Изображения
Текст
Цвет
Линии и рамки
Углы
Списки
Ссылки
Дизайны сайтов
Формы
Таблицы
CSS3
HTML5
Блог для вебмастеров
Новости мира Интернет
Сайтостроение
Ремонт и советы
Все новости
Справочник от А до Я
HTML, CSS, JavaScript
Афоризмы о учёбе
Статьи об афоризмах
Все Афоризмы
Помогли мы вам |
Автор эксплоита, который мы разберем, — исследователь и разработчик из Австрии Уильям vakzz Боулинг (William Bowling). Он обнаружил, что класс UploadsRewriter
РїСЂРё определенных условиях никак РЅРµ проверяет путь РґРѕ файла. Рто открывает злоумышленнику возможность скопировать любой файл РІ системе Рё использовать его РІ качестве аттача РїСЂРё переносе issue РёР· РѕРґРЅРѕРіРѕ проекта РІ РґСЂСѓРіРѕР№.
На этом исследователь не остановился и нашел возможность превратить эту «читалку» в полноценную уязвимость типа RCE. Атакующий может прочитать файл secrets.yml
, в котором находится токен для подписи cookie. Специально сформированная и подписанная кука позволяет выполнять произвольные команды на сервере.
Уязвимость относится к типу path traversal и получила номер CVE-2020-10977. Уязвимы версии GitLab EE/CE начиная с 8.5 и 12.9. Компания GitLab в рамках программы bug bounty выплатила за этот баг 20 тысяч долларов.
Тестовое окружение для изучения этого бага поднять очень просто, так как у 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.
В русской версии интерфейса issue перевели как «обсуждение», но мне кажется, что по смыслу ближе термин «баг», «ошибка» или «проблема», ведь именно их чаще всего и описывают в issue. Я буду использовать то английское написание, то различные вариации русского, так что не удивляйся.
Создадим в проекте Test
новый issue.
При создании можно описать детали проблемы в формате Markdown, а еще загрузить произвольный файл, например скриншот с ошибкой или лог-файл, чтобы упростить жизнь разработчикам.
Все загруженные файлы складываются на диск в папку /var/opt/gitlab/gitlab-rails/uploads/
. За это отвечает класс FileUploader
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-строка, которая будет именем папки.
011: class FileUploader < GitlabUploader
...
019:
VALID_SECRET_PATTERN = %r{Ah{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
А имя файла используется то, которое передали при загрузке.
212: def secure_url
213: File.join('/uploads', @secret, filename)
214: end
После загрузки аттача ссылка в формате Markdown вставляется в описание проблемы. Сохраним ее.
GitLab позволяет перенести issue из одного проекта в другой, что бывает очень полезно, если ошибка касается и другого продукта того же разработчика.
После нажатия на кнопку выбираем проект, куда хотим отправить issue.
Во время перемещения в старом проекте issue закрывается и появляется в новом.
Причем аттачи копируются, а не переносятся. То есть для них создаются новые файлы и ссылки на них, соответственно.
Посмотрим в коде, как выполняется перенос. Все роуты, которые касаются issues, можно найти в папке routes
в файле issues.rb
. Там в том числе есть роут move
, который отвечает за перенос. Именно он обрабатывает пользовательский POST-запрос с необходимыми параметрами.
5: resources :issues, concerns: :awardable, constraints: { id: /d+/ } do
6:
member do
...
9:
post :move
Затем мы попадаем в одноименную функцию.
123: def move
124: params.require(:move_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
.
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
097: def move_issue_to_new_project(issue)
098: target_project = params.delete(:target_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
.
3: module Issues
4: class MoveService < Issuable::Clone::BaseService
Здесь сначала вызывается метод execute из дочернего, а затем из родительского класса.
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
.
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.
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
.
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
, который копирует содержимое описания ошибки.
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 в поисках шаблона с аттачем.
11: class FileUploader < GitlabUploader
...
17:
MARKDOWN_PATTERN = %r{!?[.*?](/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?))}.freeze
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.try(:exists?)
27:
28:
klass = target_parent.is_a?(Namespace) ? NamespaceFileUploader : FileUploader
29:
moved = klass.copy_to(file, target_parent)
60: def find_file(project, secret, file)
61: uploader = FileUploader.new(project, secret: secret)
62: uploader.retrieve_from_store!(file)
63: uploader
64: end
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
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.
Тогда возникает другой вопрос: а что же интересного можно прочитать?
|
|