技術者ブログ
クラウド型WAF「Scutum(スキュータム)」の開発者/エンジニアによるブログです。
金床“Kanatoko”をはじめとする株式会社ビットフォレストの技術チームが、“WAFを支える技術”をテーマに幅広く、不定期に更新中!
SpringのRCE脆弱性(CVE-2022-22965)について
はじめに
Log4jやStruts2など、Java製ソフトウェアにおいてリモートからの任意のコード実行(RCE)の脆弱性が目立つ時代になってしまっていますが、これにさらにSpringも加わってきました。この記事では特にCVE-2022-22965に焦点を当て、技術的な視点からの解説を行ってみます。
なぜJavaアプリでRCEとなるのか?
Javaの(特にウェブアプリケーションで)RCEとなるパターンはいくつか知られており、以前こちらの記事にまとめました。今回のCVE-2022-22965はこの記事の「3. クラスローダを操作できてしまうパターン」のパターンになります。
なぜクラスローダを操作できるのか?
そもそも「クラスローダの操作」とは何を意味しているのでしょうか。この文脈では、Javaのプロセス内のクラスローダ系のクラスのインスタンスの、getterやsetterのメソッドを攻撃者が実行できることを「操作できる」と表現しています。
SpringやStruts等のいくつかのJava製のウェブアプリケーション用フレームワークでは、HTTPリクエストに含まれるパラメータが自動的にJavaプロセス内で特定のオブジェクトに対するgetterやsetterのメソッドチェーンに変換される仕組みが存在します。例えばHTTPリクエストのパラメータがid=123の場合、Javaプロセス内でobject.setId(123)のように自動的に変換されます(このobjectがそもそも何なのか?はウェブアプリケーションが事前に定義しておくことになります)。パラメータがドットで繋げられている場合、例えばuser.name=tomの場合、object.getUser().setName("tom")のようにgetterとsetterが繋げられていきます。getterによって取得されるオブジェクトは起点となるインスタンス(この例ではobject)とは異なるクラスのインスタンスである可能性があり、そこにあるいくつかのgetterのうち攻撃者に都合が良いものをどんどん繋げていくことで、クラスローダにたどり着く場合があります。
Springでの基本的なウェブフォームを作るこちらの例では、上記のobjectに該当するのは下記のようなコードのGreetingクラスです。
package com.example.handlingformsubmission; public class Greeting { private long id; private String content; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } }extendsやimplementsといった単語はなく、非常にシンプルなクラスです。無害そうなgetter/setterしか存在していませんが、この場合でも攻撃者はgetterを使ってクラスローダにたどり着けます。この理由は、SpringがGreetingクラス(を含む全Javaのクラス)の暗黙の基底クラスである Objectクラスのgetterとsetterについても、HTTPリクエストのパラメータ経由で呼び出してしまう仕様だからです。
ObjectクラスにはgetClass()というgetterが存在します。このメソッドはClassのインスタンスを返します。そしてClassクラスにはgetClassLoader()というgetterが存在します。このため、class.classLoaderとすることで、Greetingクラスのオブジェクトから、クラスローダにたどり着けてしまうのです。
厳密には最近のSpringでは上記のようなclass.classLoaderでは攻撃はできないのですが(理由は後述)、ここでは原理をシンプルに示すためにclass.classLoaderで説明しました。
なぜクラスローダを操作したいのか?
前項で説明したように攻撃者はfoo.bar.baz...とすることでgetFoo().getBar().getBaz()...とgetterで取得したオブジェクトのさらに別のgetterを使って…という形でJavaプロセス内のさまざまなクラスのインスタンスに到達できます。到達可能な数多くのクラスのうち、なぜクラスローダが特に攻撃で利用されるのでしょうか。この理由はJavaのアプリケーションサーバとしてシェアが高いTomcatのクラスローダの1つであるWebappClassLoaderがRCE可能となるオブジェクトへ到達するgetterを持っているからです。つまりいわゆるJavaのクラスローダだから狙われているわけではなく、たまたまTomcatのクラスローダが攻撃に利用可能だから、というのが理由です。また、前項で説明したように基底クラスであるObjectからClassを経由してClassLoaderに至るという汎用性の高さも理由の1つでしょう。
WebappClassLoaderからgetterを繋いでいき、最終的にAccessLogValveクラスのインスタンスを操作することができます。これによってアクセスログに書き込んだ内容をJSPとして解釈させることでRCEが行われます。この非常に賢く巧妙な攻撃手法は、過去にStruts2に同じようにクラスローダを操作できる脆弱性が見つかった際に発明されたものだと思います。
こちらの記事にあるように2010年に今回と殆ど同じSpringの脆弱性であるCVE-2010-1622が見つかったときには、まだ誰もTomcatのWebappClassLoaderからAccessLogValveへ繋ぐ手法を見つけていませんでした。そのためこのときはリモートのjarファイルのURLをクラスローダに設定するという攻撃ベクターがRCEの根拠とされましたが、これはAccessLogValveを使う手法に比べ現実的に攻撃を成功させるのは難しそうです。
なぜJava9以上のみが脆弱なのか?
今回の脆弱性では「Java9以上で動作させていると脆弱である」という珍しい条件がありました。これは何故なのでしょうか。
実は今回の脆弱性は前項で軽く触れた、12年前の脆弱性であるCVE-2010-1622と本質的にまるっきり同じものです。ここまでに説明した「getterを繋いでさまざまなインスタンスに到達できてしまう」というSpringの仕様そのものが非常に危険であり、基本的にclass.classLoaderとすることでTomcatのWebappClassLoaderに到達できる可能性があります。これを防ぐため、12年前にこちらのコミットで対策が行われました。
この修正は「プロパティ(getter/setterでアクセスできる対象)の調査対象のクラス(コード内のbeanClass)がClassである場合には、getClassLoader()にはアクセスさせない」という非常に狭い範囲に限定したブラックリスト的な対策でした。つまりClassクラスのgetClassLoader()以外にクラスローダを取得できるgetterがあれば、そこが抜け道になる可能性を持っていたわけです。そしてJava9が登場した際に、まさに抜け道が誕生していました。Classクラスに新たにgetModule()メソッドが追加され、ModuleクラスにはgetClassLoader()メソッドが存在したのです。そのためclass.module.classLoaderとすることで12年前に封じ込めたはずの脆弱性が蘇ってしまったというわけです。
このSpringの脆弱性の修正やWAF等の防御ロジックでも共通して言えることですが、ブラックリスト的に、つまり禁止したいものに注目して対策する場合、そのリストをメンテナンスしていく必要があります。今回の例のようにプログラミング言語自体に機能が追加されることでブラックリストの内容が不十分となる場合もありますし、こちらの記事に書いたようにRDBMSに機能が追加されることでWAFをバイパスしてSQLインジェクション攻撃ができるようになったりします。
可能な場合にはもちろんホワイトリスト的なアプローチを採るのが良いでしょう。
今回はどのように修正されたか?
先述したようにSpringでは12年前には非常にピンポイントなブラックリスト的対策が行われました。今回はどのように修正されたのでしょうか。かつてStruts2はひたすらブラックリストにキーワードを追加していくという泥縄的な修正を繰り返し、似たような脆弱性を何度も繰り返しました。もし今回Springの修正が単に「ブラックリストにさらにgetModule()を追加する」のようなものだったらそれと同じことになってしまいます。そのような視点から今回の修正コードを確認してみました。
修正のコミットはこちらです。さすがに単純にgetModule()をブラックリストに追加するようなことは行われていませんでした。修正は大きくわけて二点あります。まず一点目として、Classクラスについてはプロパティをホワイトリスト的に抽出する仕様に変更されました。getFooName()のような、いかにも無害そうな「Name」系のプロパティのみが許可されるようになりました。これによってgetName()やgetPackageName()、getCanonicalName()などのみが許可されるようになり、今回の「Javaの仕様変更でgetModuleが爆誕していたのでやられてしまった」みたいなことは繰り返さずに済みそうです。脆弱性対策という観点では悪くないと思います。しかし一方でこれまで利用可能だった多くのgetter/setterが利用できない形になりました。つまりこの修正によって仕様ががらっと変わっているわけです。今まではかなり自由にアクセスを許可していました。その仕様に基づいて開発されたアプリケーションが、修正後のバージョン以降では動かなくなる可能性があるのでは?と思いました。
次に二点目として、Classクラスに限定せず、プロパティ調査対象の全てのクラスについて、ClassLoaderかProtectionDomainから派生したクラスの型のプロパティへのアクセスが禁止されました。こちらはこの二種類をピンポイントで狙い撃ちしたブラックリスト的アプローチです。TomcatのWebappClassLoaderへのアクセスにうんざりしたので、とにかくクラスローダ系には一切アクセスさせないぞ!という意思を感じます。しかしこちらも対策としては良いですが、そもそものソフトウェアとしての仕様の変更となってしまっているわけです。
脆弱性についてはまずまず固めの対策ができている反面、ソフトウェアとしての仕様が大きく変わっていると感じました。この説明は当然あるべきだと思ったのでRelease Note的なドキュメントをチェックしましたが、なんとどこにも記述されていませんでした。
そして案の定、「バージョンアップしたら今まで動いていたものが動かなくなった」という報告がされています。脆弱性対応ということで若干慌てた対応となっており、ドタバタしている印象を受けます。
今後も同じような脆弱性は出るか?
攻撃者がかなり自由にgetter/setterを呼び出せるという作りそのものは変わっていないので、今後も常にSpringとその上で動作するウェブアプリケーションには危険がつきまといます。ここまで解説したようにRCEを可能にしたのはAccessLogValveクラスであり、今回ブラックリストへ追加された2つのクラスには含まれていません。そのため、クラスローダを経由せず、何か別のgetterを繋いでAccessLogValveにアクセスされてしまえば、同じようにRCEが起こりえます。
とはいえ全てのインスタンスから(さまざまな環境やウェブアプリケーションで共通して)実行可能なのはObjectクラスやClassクラスのgetter/setterに限られ、ここがかなり固めに変更されたので、CVE-2022-22965やCVE-2010-1622のように「Springを使っている多くの環境で攻撃が可能になる」というパターンでのRCEが出てくる可能性は現実的には非常に低くなったかなと思います。
今回の修正以後のSpringでは、getter/setterを繋いで何か攻撃を行うためにはウェブアプリケーション内部のクラスの具体的な実装を知る必要が出てくるでしょう。そのため、Spring上で動作することを前提にしたOSSのウェブアプリケーションにおいて、RCEかどうかはわかりませんが、何かしら脆弱性が出る可能性はそこそこあると思います。setterでかなり自由にデータを書き換えできてしまうので、そこから色々な事が起こり得るでしょう。多くの業務アプリケーションのように ソースがクローズである場合、外部から攻撃を行うのはかなり難しいと思います。
なぜScutumはゼロデイで防御できたのか?
最後に少し視点を変え、WAFサービス提供者として今回の脆弱性を見てみます。とても幸いなことに、このCVE-2022-22965(と同時期に見つかったCVE-2022-22963)については、Scutumでは特別に追加の対応を行わなくても最初から攻撃を止めることができていました。これは、TomcatのWebappClassLoaderを経由してAccessLogValveを操作しRCEする、という攻撃方法そのものが過去のStruts2において発見されたものと非常に似たものだからです。Scutumでは新たな脆弱性へ対応を行う際、できるだけ汎用的に、かつHTTPリクエストの広い部分をカバーするようにしています。一般的にWAFでは検知の適用範囲を広げると誤検知の可能性が上がってしまうため、影響範囲をできるだけ狭めて(シグネチャ等の)検知ロジックが投入されることが多いです。しかしその場合には「似た別の脆弱性が出ると、また追加でシグネチャを投入しなければいけない」という問題が起こり、今回のようなゼロデイ攻撃には間に合わず、やられてしまう可能性が高くなります。
今回はSpring開発元からのオフィシャルの情報発信やパッチよりも前に攻撃が始まるいわゆるゼロデイ攻撃でした。Scutumでは最初の攻撃は日本時間の3月30日の22時頃に観測しています。本格的に話題に登り始めた翌日の3月31日には攻撃は多数観測されるようになっていました。CVE-2022-22965に関するオフィシャルの情報が最初に出たのは日本時間の3月31日の夜20時頃のようです。WAFがゼロデイ攻撃を止められないものであった場合、このオフィシャル情報が出てから仮に12時間後に追加のシグネチャを準備したとしても、おそらく間に合わないでしょう。Scutumで観測した範囲では、この「12時間後」にあたる4月1日の朝8時までに、CVE-2022-22965に対する攻撃が700件程観測されました。
まとめと個人的見解
今回はSpringのCVE-2022-22965について、それが12年前の脆弱性が蘇ったものであるという点などについて技術的な解説をしてみました。個人的にはStruts2と同じく、外部からgetterやsetterをかなり自由に呼べてしまうという根本的な設計思想そのものが非常に危険だと考えており、自分や自社の開発でSpringを利用することはありえないと考えています。
あるクラスがgetterやsetterで「公開」しているのは基本的にJavaプログラム内のアクセスの話であり、HTTP経由でインターネットの向こう側からプロパティをセットしてくれ、と考えているわけではありません。TomcatのAccessLogValveを開発した人は間違いなくそう考えているだろうと思います。長いJavaの(特にエンタープライズ寄りの)開発の歴史においてJava Beansという概念があり、その流れからこのようなプロパティアクセスがウェブフレームワークに入り込んだのだろうと推測しますが、普通に常識的に冷静に考えてセキュリティ的にあり得ないのではというのが個人的な見解です。
Spring の脆弱性に関するWAF「Scutum」の対応
Spring Frameworkのリモートコード実行の脆弱性を利用した攻撃(CVE-2022-22965)への対応Spring Cloud Function の脆弱性を利用した攻撃(CVE-2022-22963)への対応