https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement
Spring Shell がかわいそうなのでSpring4Shellって呼び方はしてほしくないなーと。でもCVEの番号なんて覚えれないので、通称は欲しい。この名前で広まっちゃったから検索に使ってるけど、、、
起こりうる問題とか実務ですべき対応とかはSpringの公式アナウンスを読んでください。 なんかおかわりもあるっぽいので、SpringBoot使ってるなら 2.6.6 -> 2.6.7 と更新してくことになりそ。
中身の話します。発生直後とかは攻撃の助けになっちゃうので言及は控えめにした方がいい気もするけど、そろそろいいかなって……攻撃者はもう情報持ってるだろうし。。
Springの対応内容
コミット で「なるほどねー」と言えるなら、以降は読む必要ありません。
この CachedIntrospectionResults の History を見ると、ひとつ前の変更は 2020-08-29 で2年弱触られておらず、過去10年で30コミット程度。そんな変更が活発なクラスでもないです。
動かして確認してみよう
CachedIntrospectionResults の変更箇所は private なコンストラクタで、使用箇所はパッケージプライベートな static メソッド forClass(Class) だけ。このメソッドは BeanUtils と BeanWrapperImpl で使用されます。
使いやすい BeanUtils を使用して試してみましょう。
BeanUtils と言われると「ああ、getter/setter呼んだりBeanのプロパティをがさっとコピーしたりするあれね」とか「 BeanUtils とか BeanUtil とかいろんなパッケージにあるよなー」とか思い当たる方も多いでしょう。
動作確認コード
今回問題になっていたリクエストは class.module.classLoader.resources.context... というものでした。
で BeanUtils はJavaBeansを扱うもので、 java.beans.PropertyDescriptor が使われています。
PropertyDescriptor はリフレクションで少し踏み込んだことをしたことがなければ見たこともないかもしれませんが、別に知らなくても問題ありません。「あーJavaBeansのパッケージにプロパティをいじるクラスがあるんだなぁ」くらいで十分すぎると思います。興味あったら触ってみるのもいいと思いますが。
JavaBeansについては以下のエントリを見ておくと心穏やかに生きられると思います。
さて、やってみましょう。
BeanUtilsを(介して対象のCachedIntrospectionResultsを)使ってプロパティ名に対するPropertyDescriptorを取得PropertyDescriptor#getReadMethod()で getterを取得- getterを使ってインスタンスを取得
- インスタンスに対して1に戻って繰り返し
import org.springframework.beans.BeanUtils; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; public class CVE202222965Test { public static void main(String... args) throws Exception { Object instance = new CVE202222965Test(); for (String propertyName : "class.module.classLoader.definedPackages".split("\\.")) { System.out.println("-- " + propertyName); PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(instance.getClass(), propertyName); System.out.printf("%s に対する %s のPropertyDescriptor: %s%n", instance.getClass(), propertyName, propertyDescriptor); if (propertyDescriptor == null) { System.out.println("とれなかった(セーフ)"); return; } Method readMethod = propertyDescriptor.getReadMethod(); instance = readMethod.invoke(instance); System.out.println("PropertyDescriptorで取れた値: <<" + instance + ">>"); if (instance == null) { System.out.println("とれなかった(セーフ)"); return; } } System.out.println("ClassLoaderから取れちゃった: " + instance); } }
Java9以降で該当するバージョンの spring-beans を使うと最後まで行きます。こんな感じ。
-- class class cve.CVE202222965Test に対する class のPropertyDescriptor: org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=class] PropertyDescriptorで取れた値: <<class cve.CVE202222965Test>> -- module class java.lang.Class に対する module のPropertyDescriptor: org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=module] PropertyDescriptorで取れた値: <<unnamed module @30cb5b99>> -- classLoader class java.lang.Module に対する classLoader のPropertyDescriptor: org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=classLoader] PropertyDescriptorで取れた値: <<jdk.internal.loader.ClassLoaders$AppClassLoader@42110406>> -- definedPackages class jdk.internal.loader.ClassLoaders$AppClassLoader に対する definedPackages のPropertyDescriptor: org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=definedPackages] PropertyDescriptorで取れた値: <<[Ljava.lang.Package;@548e7350>> ClassLoaderが取れちゃった: [Ljava.lang.Package;@548e7350
Java8以前はgetModuleメソッドがないので取れないし、対応後のspring-beansはmoduleが取れないようになってます。もちろんclassLoaderも取れない。
"class.module.classLoader.definedPackages" の最後の definedPackages は ClassLoader の持っている public なgetterならなんでも良いです。
PropertyDescriptor 経由のアクセスなので乱暴なメソッド呼び出しはできませんが、今回の脆弱性は DataBinder なのでsetterを呼び出しちゃう。で、setter経由でTomcatのあいつを狙ったらいけちゃう、と言うのが示されちゃった内容。
探し回ったらTomcat以外でもなんやかんや見つかるでしょうね。
こんな感じでTomcatにwar作ってデプロイしたりDataBinderを使ったりしなくても、事象の原因部分だけは動かせます。
直感的にはとにかく ClassLoader に手が届くのが気持ち悪いのですが、これ "class.module.classLoader が class.classLoader だと だいぶ昔(2010-03)から取れない んですよね。
今回の対応前から ("classLoader".equals(pd.getName()) で continue してるので、元々 CachedIntrospectionResults は ClassLoader を扱わせるつもりはなかったわけで。
動きを変えて遊んでみる
- プロパティのたどりかたをクラスを読みながら変える
"class.module.classLoader.definedPackages"をいじってみてどこまで手が届くんだっけ?とやってみる。
- 起点のクラスを変える
- たとえば
new CVE202222965Test()としてるのをnew Object()とか、クラスローダーが違うクラスを使ってみる。
- たとえば
- setterを呼んでみる
……攻撃者がやってることなんですが、こう言うことしてるとJSLとかフレームワークとかの構造が少し見えるようになってくるんじゃないかなと。遊びで得られる知見ってどこで役に立つかわかりませんが、なんらかの糧にはなります。
思うところ
別段 ClassLoader に手が届いたからと言って、 ClassLoader の機能を使ってるわけではないです。初期情報を見た時は「 ClassLoader に無理矢理クラスを読み込ませてるのかな?」と思ったりしましたが、濡れ衣でした。「任意のコードを実行できるプロパティに手が届いてしまうケースが見つかった」ですかね。
今回の方法はgetterを辿ってsetterを呼び出せるなら何にでも適用できるもので、 ClassLoader は踏み台にされただけ。 ClassLoader は多くのインスタンスに手が届きやすいのでよく狙われるんですけども。
じゃぁ問題はどこにあるんだって言うと、意識的に実装してないプロパティにアクセスできるところなんじゃないかなぁ。
具体的には getClass() メソッドで Classクラスのインスタンスが取れる点。要するにすべてのインスタンスは Object を実装するがゆえに Object のメソッドをすべて持っている(けれどそれが意識から漏れがち)というところが。 Object のメソッドって業務コードではだいたい直接は使わないんですよね。 toString equals hashCode あたりは意識するものの、別段 Object のメソッドである必要はないと言うか。他のメソッドはもっと意識されてないでしょうし。そういえば Java9でdeprecatedになっていた finalize がJava18の JEP 421: Deprecate Finalization for Removal で削除に一歩進みましたね。さすがに finalize くらいになると削除に対してもすごく慎重な進め方をしている模様。
話を戻して、 BeanUtils を使う場面で Object クラスのメソッドを呼び出したいと思うことはない、と言うかシンプルに考えれば対象クラスに実装していないsetter/getterを呼び出したいとは思っていなさそうなんですが、現実問題としてBeanと呼ばれるものは共通プロパティを基底Beanと呼ばれるものに定義してたりするので、ユーティリティとしては親クラスも辿って呼び出す必要はあるんですよねぇ。
対応内容を見てると「なんで getClass() の呼び出し自体止めないんだろ」とか思うところですが、 Class クラス自体を使用されたら大穴は空いたままだし、 class プロパティはなんだかんだで使われてるんだろうなぁ。。
そういえば InitBinderのフィールド指定
「 @InitBinder でdisallowFieldを指定する」がバージョンアップするまでの対応として挙げられています。「そんな機能知らなかった」と言う話を聞いたりしたので、haljikさんのこちらを紹介。
意図しない改竄に対して安全
「動けばいい」なら面倒なだけなんですが、こういうこと起こるとホワイトリスト方式って強いなぁと……
まとめると
と言うことで、変更内容は以下。
- 変更前
Classクラスのプロパティ名classLoaderとprotectionDomainは扱わせない。(ブラックリスト)
- 変更後
だいぶ厳しくなってます。
Java9で生えた getModule メソッドが public でなかったり get から始まっていなければ、と言う感じでしょうか。私がメンテナなら「まじかー……」とか言ってると思います。
普段よく触っているコードならともかく、ほとんど更新しないコードですしね。
ともかく、今後は BeanUtils で ClassLoader なプロパティと Class クラスのほとんどのプロパティは無視されるようになります。
万一使っていたら「バージョンアップしたら動かなくなった」とかなります。 そんなもの BeanUtils でやるんじゃないよ、って思うけど。踏まないこと祈ってる。