Railsの上で走る
(2021/9/27追記: この記事の内容に色々追加した物をzennへ載せました Deserialization on 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
が使われているためです。
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::MessageVerifier
とActiveSupport::MessageEncryptor
のシリアライザはデフォルトがMarshalになっており、Active StorageのURLにもこれらのクラスが使われています。ただし、シリアライザが実行される前にsecret_key_base
を使った検証が実行されるので、すぐに危険という話ではありません。
という話をRailsに報告したのが以下のhackerone*2のレポートでした。
少し話が脇にそれますが、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できる組み合わせになっていました。
エラー経由
productionでは使うべきではないエラーの詳細を表示する機能が生きていたことで、わざとエラーを起こすことでsecret_key_base
が見えてしまったようです。
サムネイル経由
これまで脆弱性を調査してきた中で、画像がアップロードされたときに作るサムネイルの中に任意のファイルの中身が表示されてしまう脆弱性を何度か報告したことがあります。imagemagickやghostscriptなどの問題です。
ImageTragickを始めとしたこれらの問題は知識が必要なうえに色々対応が難しいです。SaaSでサムネイルを作ってくれるサービスを使えるのであれば使ったほうが楽だとは思います。(ところでActive StorageではサムネイルをRailsサーバ側で作っており、なかなか危なそうに見えます。)
オブジェクトインジェクションについて他の事例
memcacheへSSRF経由で
記事の中の4つめがGitHub EnterpriseでSSRF経由でのオブジェクトインジェクションの事例です。memcacheにruby側からSSRFで細工したキャッシュを入れて、ruby側でそれ読み込むとRCEとなったそうです。 現在はruby側にもmemcache側にもSSRF対策が入っています。(しかし、SSRFはこれはこれでかなりややこしいので実際に安全なのかどうかは状況次第の部分が多いですね……)
YAML.load
YAMLもMarshalと同じ問題を抱えており、任意のクラスからRCEが実行可能になる場合があります。以下の記事はrubygems.orgでの事例です。
gemが登録時にメタ情報のYAMLをそのまま読み込んでいるため発生する問題だったようです。
Marshalと違い、YAMLではYAML.safe_load
を使うと復元可能なクラスを制限することができるため、ある程度安全になります。
docs.ruby-lang.org
JSON
JSON.load
も同様に任意のクラスのオブジェクトが復元できる…わけではなくjson_create
が宣言されたクラスのみが復元できます*3。しかしJSON.parse
ではそもそもクラスは復元されないので、JSON.parse
を使えばオブジェクトインジェクションの問題は発生しません。
Oj
JSONを扱うgemの一つ、Ojではruby標準のJSONとは違いOj.load
で任意のクラスが復元できてしまうため発生した事例です。
Rack::Session::Cookie*4
Rails側のCookieStoreとは違いRack::Session::Cookie
のシリアライザはMarshalのままのようです。
更にこちらではsecretなしでも起動できるようです。が警告がでます。
対策
secret_key_base
やMarshal
などが使われている場所を把握する
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 by BPS株式会社)。
MessageVerifier
とMessageEncryptor
のデフォルトのシリアライザはMarshalから変わっていないため、Railsでなにか機能が増えるたびに同じ問題が発生しますね。
他にもgemの中で使っている可能性はあるため、実際のどのような影響があるかはアプリケーション次第になります。
jobを扱うgemでは、jobの引数をDBのレコードにYAMLでシリアライズして保存するものもいくつかあるようなので、任意のテーブルに保存ができるSQL injectionが可能な場合にRCEにつながることもあるかもしれません。*5
シリアライザを安全なものに切り替えていく
キャッシュ以外の用途であれば、大体の場合はシリアライザはJSONに切り替えることができると思います。逆に問題がある場合はRubyやRailsのバージョンアップ時に復元に失敗する可能性があります。
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:ロバの耳