freeeの開発情報ポータルサイト

freee PSIRTの風景:Dependabot alertの調査

こんにちは、PSIRTのWaTTsonです。

昨年の夏頃に、「Dependabot alertをSlackに通知して、トリアージ運用に役立てる仕組みを作ってみた」という記事を投稿しました:

developers.freee.co.jp

ここでは、新しく報告されたDependabot alertをSlackに通知し、Jiraチケットを作成してPSIRTメンバーをアサインし、トリアージを行って各開発チームのチャンネルにメッセージを送信する、という仕組みについて説明しました。 今回は、この中でPSIRTメンバーがトリアージをする時にどういう風なことをしているのかを書いてみたいと思います。

過去に私自身にアサインされた事例の中から1つ、具体例を挙げて見てみましょう。執筆に時間をかけてしまったせいでちょっと古い例ですが、2024年3月頃アラートが上がったにRDoc RCE vulnerability with .rdoc_options (GHSA-592j-995h-p23j/CVE-2024-27281)を見てみます:

github.com

担当としてアサインされたら、とりあえずはアラートが上がっているリポジトリを確認した上で、どういうアラートなのかGitHub Advisory Databaseを見に行きます。

まずはDescriptionを読んでいきます:

RDoc RCE vulnerability with .rdoc_options

An issue was discovered in RDoc 6.3.3 through 6.6.2, as distributed in Ruby 3.x through 3.3.0.

When parsing .rdoc_options (used for configuration in RDoc) as a YAML file, object injection and resultant remote code execution are possible because there are no restrictions on the classes that can be restored.

When loading the documentation cache, object injection and resultant remote code execution are also possible if there were a crafted cache.

We recommend to update the RDoc gem to version 6.6.3.1 or later. In order to ensure compatibility with bundled version in older Ruby series, you may update as follows instead:

For Ruby 3.0 users: Update to rdoc 6.3.4.1

For Ruby 3.1 users: Update to rdoc 6.4.1.1

For Ruby 3.2 users: Update to rdoc 6.5.1.1

You can use gem update rdoc to update it. If you are using bundler, please add gem "rdoc", ">= 6.6.3.1" to your Gemfile.

Note: 6.3.4, 6.4.1, 6.5.1 and 6.6.3 have a incorrect fix. We recommend to upgrade 6.3.4.1, 6.4.1.1, 6.5.1.1 and 6.6.3.1 instead of them.

対象になっているのは、RubyのRDocのようです。RDocの設定ファイルとして使われている.rdoc_optionsをYAMLとしてパースする際に、object injectionによって任意コード実行ができる、ということが書かれています。


Descriptionの下にあるReferencesの中に、RDocの修正コミットのリンクがいくつか並んでいるので、その中身を見てみます。例えば一番上のruby/rdoc@1254b00を見てみます。

差分はいろいろと書かれていますが、

File.open file, 'rb' do |io|
  Marshal.load io.read
end

のように書かれている箇所を

marshal_load(file)

の形に置き換える変更が行われているようです。置き換えた先のmarshal_load()の定義を見てみます。

private
def marshal_load(file)
  File.open(file, 'rb') {|io| Marshal.load(io, MarshalFilter)}
end

Marshal.loadでファイルを読み込む際に、第2引数にMarshalFilterを渡すような処理が書かれているようです。MarshalFilterの定義は別に書いてあります:

MarshalFilter = proc do |obj|
  case obj
  when true, false, nil, Array, Class, Encoding, Hash, Integer, String, Symbol, RDoc::Text
  else
    unless obj.class.name.start_with("RDoc::")
      raise TypeError, "not permitted class: #{obj.class.name}"
    end
  end
  obj
end
private_constant :MarshalFilter

RubyのMarshal.loadの第2引数はどういう風に扱われるものでしょうか?公式のレファレンスを見てみます:

docs.ruby-lang.org

第2引数はprocとなっていて、以下のように説明があります:

load(port, proc = nil) -> object

port からマーシャルデータを読み込んで、元のオブジェクトと同じ状態をもつオブジェクトを生成します。 proc として手続きオブジェクトが与えられた場合には読み込んだオブジェクトを引数にその手続きを呼び出します。

つまり、単にマーシャルデータを読み込んだ後に、それに対して第2引数として渡したMarshalFilterを実行する、という形になります。

MarshalFilterで何をやっているかを見てみると、読み込んだオブジェクトがtrue, false, nil, Array, Class, Encoding, Hash, Integer, String, Symbol, RDoc::Textのいずれかに該当せず、かつ"RDoc::"の形式でもない時にTypeErrorをraiseする、という処理が書かれています。つまり、端的に言えば、想定外のclassのオブジェクトが渡されたときのエラーハンドリングを追加した、というのがこのcommitの趣旨といって良さそうです。

GitHub Advisory DatabaseのReferenceには、commitのリンク以外にもいくつかのリンクが並んでいて、その中にruby-lang.orgのページ CVE-2024-27281: RCE vulnerability with .rdoc_options in RDocへのリンクがあります。これはRubyが公式にnewsとして出している脆弱性情報ですが、こちらにはcreditとして

Thanks to ooooooo_q for discovering this issue.

と書かれているのが見えます。ooooooo_qのリンク先はHackerOneのユーザーページとなっているようです。

プロフィールの中にX(旧twitter)のリンクがあるので、そちらを辿って経緯を調べてみます。すると、@rubylangorgの脆弱性情報の投稿をリポストした上で、

「デシリアライズ本を書いていたときに調べたものはこれですべて公開されたはず。多分」と投稿されています:

ここで言及されている「デシリアライズ本」というのは、おそらくZennで公開されている『Deserialization on Rails』というものでしょう:

zenn.dev

2021年にこの本を執筆した際に見つけた脆弱性がHackerOne上で議論されていて、その対応の最後の1ピースになったのが今回の脆弱性、という経緯と思われます:

hackerone.com

『Deserialization on Rails』の中ではMarshal.loadについても言及されていて、

Marshal.loadの引数としてユーザ由来の値が渡されると、任意のインスンタンスのオブジェクトを作ることができてしまうので危険な状態になります。脆弱性の種類としてはObject injection、Insecure Deserializationなどと呼ばれます。

オブジェクトインジェクションの危険性は定義されているクラスや、デシリアライズ後のオブジェクトへの操作によって変わります。特にRails(ActiveSupport)ではデシリアライズすることで、デシリアライズされたオブジェクトの任意のメソッドを呼べるクラスが存在するため、RCEが容易でした。また後の章で紹介するGadget chainによりほとんどのRubyの環境ではRCEが可能です。

という風に説明が書かれています。


このアラートは、GitHub Advisory Databaseではseverity: HIGHと書かれていますが、CVSSでの評価が記載されていません。これはNVDの方でも「NVD assessment not yet provided.」と書かれていて評価されていないところですが、他のソースを調べてみると、例えばRed Hatで以下のような評価がなされています:

access.redhat.com

CVSS v3 Base Score: 4.5 Attack Vector: Local Attack Complexity: High Privileges Required: None User Interaction: Required Scope: Unchanged Confidentiality Impact: Low Integrity Impact: Low Availability Impact: Low

攻撃を成立させるには読み込まれる.rdoc_optionsに細工をでき、かつそこでrdocを動作させる必要があるので、Attack Vectorがローカルとして評価されています。特権レベルが不要というところだけ評価が高めですが、他は基本的に低めのスコアリングとなっていて、これを使って実際に攻撃を成功させ、かつ広範囲に影響を与えさせるのは少し難しい側面がありそうです。CVSSスコアが4.5だと、他のGitHub Advisory Databaseの項目ではseverityがmediumくらいに評価されているものが多そうな印象で、NVDなどの評価がない分ちょっと高めに扱われているかも、という雰囲気も感じられます。


ということで、このアラートの内容をおおよそ把握したので、実際にfreeeのリポジトリでDependabot alertで上がったものについて検討していきます。

freeeでは比較的多くのプロダクトでRuby on Railsをバックエンドの実装に用いています。今回対象になっているRDocは、Railsのdoc関係の依存で入っているので、Railsのバージョンによって一緒に入っていることがあるようです。とはいえ、外部からの入力が.rdoc_optionsに渡され、その上でrdocが実行されるような環境はRailsの通常の利用では考えづらく、もしこの条件が実現されるとするとシステムのかなり深いところまで侵入されたケースになるでしょう。成功したら任意コード実行ができるとはいえ、攻撃の成立可能性がそこまで高くないと言えそうです。

ということで、該当しているリポジトリを担当する各開発チームには、ここまで調べた脆弱性の概要と修正の温度感について、ざっくりまとめてメッセージを送りました。ここでは、前回のブログ記事で紹介したような、各チームのslackチャンネルに一斉にメッセージを送信する仕組みを使っています。

なお、このときは調査しながら色々とメモを取っていたので、ついでにこの調べた内容を社内ブログにも投稿してみました。今回の記事の内容は主にこの社内ブログで書いたものを整形して作っています。


開発チームに内容を伝えられる程度に理解すれば十分なので、いつもここまで詳しく調べている、というわけでもないですが、気が向いたときはこういう風にちょっと深掘りしてみたりもしています。開発チームからも説明がまとまっていると分かりやすい、という意見をもらったりしますし、セキュリティ専業のチームでやっているとあまりコードを読む機会が取れなかったりすることもあるので、こういう調査をやっていると息抜きになったりもして楽しいところです。

というわけで、PSIRTの業務の中から、Dependabot alertを見て開発チームに対応を促すまでの一幕を紹介してみました。freee Developers Hubには他にも色々とセキュリティの話も投稿しているので、ぜひぜひそちらもご覧ください!