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
でやるんじゃないよ、って思うけど。踏まないこと祈ってる。