ooooooo_qの日記

脆弱性の話とか

Railsの上で走る

この記事はRuby on Rails Advent Calendar 2019 - Qiitaの11日目です。



この記事を見ている方はRailsアプリケーションの開発をしている方が多いと思います。手元のRailsリポジトリでちょっとbin/rails routesを試してみてください。

出力結果に以下のURLは含まれていたでしょうか?

rails_service_blob GET  /rails/active_storage/blobs/:signed_id/*filename(.:format)                               active_storage/blobs#show
rails_blob_representation GET  /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
rails_disk_service GET  /rails/active_storage/disk/:encoded_key/*filename(.:format)                              active_storage/disk#show
update_rails_disk_service PUT  /rails/active_storage/disk/:encoded_token(.:format)                                      active_storage/disk#update
rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format)                                           active_storage/direct_uploads#create

含まれていた場合はそのRailsのアプリケーションに対して、殆どのケースでRCE(任意コード実行)が可能です。ただし、secret_key_baseの値を知っているならば。*1

これらのURLは一体何なのか?

これらはRails 5.2から入ったActive Storageで必要なURLです。Rails5.2以降であれば、rails newした際にActive Storageも含まれています。

Active Storageの機能を実際に使うためにはrails active_storage:installとDBのmigrationが必要なのですが( Active Storage の概要 - Railsガイド)、このURLはそれらを行わなくてもアクセスが可能です。application.rbでのrequireから"active_storage/engine"を外していなければ、Active Storageを利用する気がない場合でもroutesに含まれています。

今年の夏にRails6がリリースされ、セキュリティのサポートがされているRailsは5.2以上となったこともあり、今後多くのRailsアプリケーションでこれらのURLがアクセス可能な状態になりそうです。

なぜRCEが可能になるのか?

簡単に書けばURLで渡されるパラメータのシリアライズMarshalが使われているためです。

docs.ruby-lang.org

RubyのMarshalは任意のオブジェクトを文字列にしたり(シリアライズ)、そこからまた復元する(デシリアライズ)ことができます。

❯ irb
irb(main):001:0>
irb(main):002:0> class Cat
irb(main):003:1>
irb(main):004:1>   attr_reader :color
irb(main):005:1>
irb(main):006:1>   def initialize(color)
irb(main):007:2>     @color = color
irb(main):008:2>   end
irb(main):009:1>
irb(main):010:1> end

irb(main):011:0> cat = Cat.new('white')
=> #<Cat:0x00007fc4ea922258 @color="white">

# シリアライズ
irb(main):012:0> dump_str = Marshal.dump(cat)
=> "\x04\bo:\bCat\x06:\v@colorI\"\nwhite\x06:\x06ET"

# デシリアライズ
irb(main):013:0> cat_loaded = Marshal.load(dump_str)
=> #<Cat:0x00007fc4ea930f38 @color="white">
irb(main):014:0> cat_loaded.color
=> "white"

Marshal.loadにユーザ入力由来の値が入ると好きなクラスが作ることができてしまうので危険です。これらは脆弱性の種類としてはオブジェクトインジェクションと呼ばれます。

オブジェクトインジェクションからRCEが可能かどうかは読み込まれているクラスやデシリアライズの結果を扱う処理次第です。特にRailsではデシリアライズのタイミングで任意のメソッドを呼べるクラスが存在するため、ERBなどを呼び出してRCEが可能です。

Railsで認証や署名に使われるActiveSupport::MessageVerifierActiveSupport::MessageEncryptorのシリアライザはデフォルトがMarshalになっており、Active StorageのURLにもこれらのクラスが使われています。ただし、シリアライザが実行される前にsecret_key_baseを使った検証が実行されるので、すぐに危険という話ではありません。

という話をRailsに報告したのが以下のhackerone*2のレポートでした。

hackerone.com



少し話が脇にそれますが、Marshalはデータフォーマットのメジャーバージョンが上がったり、Railsのアップグレードで使っていたクラスがなくなったりすると復元できなくなったりするので、扱いに注意が必要です。

Cookie Storeとの違い

以前のCookie StoreではシリアライザにMarshalが使われていて、secret_key_baseが漏れるとRCEが発生することは知っている人は知っているという状態でした。現在はシリアライザをJSONに切り替えるconfigがあるので、Marshalのままになっている場合は切り替えましょう。 https://railsguides.jp/upgrading_ruby_on_rails.html#cookies%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%B6

Active StorageのURLとCookie Storeとの違いとしてシリアライザに切り替えるconfigがないことと、HTTPでGETのURLが送信されれば攻撃可能であるという点があります。 cookieはログを残していることが少ないため攻撃されたことに気づきにくいですが、GETのURLであればログに残っているので攻撃されたかどうかは判断しやすそうです。一方、このGETリクエストではヘッダへの細工もなく必要なのはURLだけであるため、インターネットに公開されていないサーバに対しても攻撃が可能です。サーバと同じネットワーク内のブラウザなどから直接リクエストが送信されれば良いので、ユーザに対してCSRFを仕掛ける、メールを送って細工した画像のURLをメーラーで閲覧させるなど経路は色々考えられます。

secret_key_baseが漏洩するパターン

そもそも知ってる

credentials.yml.encなどで管理を行っている場合、開発者であれば知ることができることが多いと思います。

[CVE-2019-5420] Possible Remote Code Execution Exploit in Rails Development Mode

https://groups.google.com/forum/#!topic/rubyonrails-security/IsQKvDqZdKw

hackeroneのレポートにも書かれていますが、Rails5.2.2.1未満では開発モードのsecret_key_baseはアプリケーションの名前から計算することが可能であるため、アプリケーションの名前を知っている開発サーバには攻撃可能という状態でした。現在はdevelopmentモードで動かしている場合はランダムな値になるため安全なようです。(ただし、明示的にsecret_key_baseを渡している場合を除きます)

Directory Traversal

Rails の CVE-2019-5418 は RCE (Remote code execution) です · GitHub

CVE-2019-5420と同時に公開されたCVE-2019-5418は任意のパスのファイルが閲覧できてしまうDirectory Traversalでした。そのため、Active Storage側のものと合わせてRCEできる組み合わせになっていました。

エラー経由

hackerone.com

blog.harshjaiswal.com

productionでは使うべきではないエラーの詳細を表示する機能が生きていたことで、わざとエラーを起こすことでsecret_key_baseが見えてしまったようです。

サムネイル経由

これまで脆弱性を調査してきた中で、画像がアップロードされたときに作るサムネイルの中に任意のファイルの中身が表示されてしまう脆弱性を何度か報告したことがあります。imagemagickやghostscriptなどの問題です。

ImageTragickを始めとしたこれらの問題は知識が必要なうえに色々対応が難しいです。SaaSでサムネイルを作ってくれるサービスを使えるのであれば使ったほうが楽だとは思います。(ところでActive StorageではサムネイルをRailsサーバ側で作っており、なかなか危なそうに見えます。)



オブジェクトインジェクションについて他の事例

memcacheへSSRF経由で

blog.orange.tw

記事の中の4つめがGitHub EnterpriseでSSRF経由でのオブジェクトインジェクションの事例です。memcacheにruby側からSSRFで細工したキャッシュを入れて、ruby側でそれ読み込むとRCEとなったそうです。 現在はruby側にもmemcache側にもSSRF対策が入っています。(しかし、SSRFはこれはこれでかなりややこしいので実際に安全なのかどうかは状況次第の部分が多いですね……)

YAML.load

YAMLもMarshalと同じ問題を抱えており、任意のクラスからRCEが実行可能になる場合があります。以下の記事はrubygems.orgでの事例です。

justi.cz

gemが登録時にメタ情報のYAMLをそのまま読み込んでいるため発生する問題だったようです。

Marshalと違い、YAMLではYAML.safe_loadを使うと復元可能なクラスを制限することができるため、ある程度安全になります。 docs.ruby-lang.org

JSON

JSON.loadも同様に任意のクラスのオブジェクトが復元できる…わけではなくjson_createが宣言されたクラスのみが復元できます*3。しかしJSON.parseではそもそもクラスは復元されないので、JSON.parseを使えばオブジェクトインジェクションの問題は発生しません。

Oj

honoki.net

JSONを扱うgemの一つ、Ojではruby標準のJSONとは違いOj.loadで任意のクラスが復元できてしまうため発生した事例です。

github.com

Rack::Session::Cookie*4

Rails側のCookieStoreとは違いRack::Session::CookieのシリアライザはMarshalのままのようです。

github.com

更にこちらではsecretなしでも起動できるようです。が警告がでます。


対策

secret_key_baseMarshalなどが使われている場所を把握する

CVE-2019-5420自体にはActive Storageの話が出てこないので、Cookie Storeだけの問題だと思われていそうなケースを何度か見かけました(Cookie storeではなくdeviseを使えばよいなど)。現在のRailsのサイトではCookieStoreでの話が中心に記述されているため、他のところで使われていることは気づかれにくそうではあります。

Active StorageやCookie Store以外の場所ですと、MessageVerifierはSigned Global IDでも使われています。 https://github.com/rails/globalid/blob/master/lib/global_id/verifier.rb#L5

Signed Global ID自体がどこで使われているというとAction Textで使うようです(Rails 6: Action Textのファイルアップロードを分解調査する(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社)。 MessageVerifierMessageEncryptorのデフォルトのシリアライザはMarshalから変わっていないため、Railsでなにか機能が増えるたびに同じ問題が発生しますね。

他にもgemの中で使っている可能性はあるため、実際のどのような影響があるかはアプリケーション次第になります。

jobを扱うgemでは、jobの引数をDBのレコードにYAMLシリアライズして保存するものもいくつかあるようなので、任意のテーブルに保存ができるSQL injectionが可能な場合にRCEにつながることもあるかもしれません。*5

リアライザを安全なものに切り替えていく

キャッシュ以外の用途であれば、大体の場合はシリアライザはJSONに切り替えることができると思います。逆に問題がある場合はRubyRailsのバージョンアップ時に復元に失敗する可能性があります。

hackeroneに書いたようにAcitve StorageのシリアライザもJSONに切り替えてそんなに困らないとは思うのですが、Railsのupgrade時に影響が出ないかは…どうなんでしょう。*6

secret_key_baseなどの鍵が漏洩したときに、鍵のrotationが直ぐにできる実装にする

秘密なものが秘密でなくなる、ということはよくある話*7ですので、鍵の交換ができるような実装に事前にしておいたほうが良いでしょう。

*1:後述のようにシリアライザを変更して対策していた場合などを除く

*2:https://hackerone.com/ 脆弱性報告のハンドリングをするサービスの一つ

*3:https://twitter.com/bulkneets/status/1109346078460006402

*4:1/5追記

*5:そんなSQL injectionができる時点でかなり不味いですが

*6:https://hackerone.com/reports/713407 をみるとhackeroneではJSONに切り替えているように見えます

*7:ロバの耳