要点だけまとめ、読みやすくしたサマリ記事を公開しました。まず概要を知りたい方、あるいはお時間に余裕の無い方におすすめです。
- SSTtechlog 08 S2-054, S2-055 および jackson-databindの脆弱性 CVE-2017-7525, CVE-2017-15095 について | SST 株式会社セキュアスカイ・テクノロジー
2017年12月1日にStruts2のセキュリティアップデートが公開されました。 公開前からJackson(Javaで人気のあるJSONライブラリ)の脆弱性が関連している、という話がメーリングリストに流れており、社内システムやツールでJacksonを利用している筆者も具体的にどのような内容か気にしていました。
実際に公開された内容としては、以下2点のセキュリティ問題が修正されていました。Jacksonのコンポーネントであるjackson-databindの脆弱性が影響しているのはS2-055のみです。
- S2-055 : https://cwiki.apache.org/confluence/display/WW/S2-055
- こちらが jackson-databind のCVE-2017-7525に対応した修正になります。
- Struts側の依存関係でjackson-databindを 2.9.2 にUPしています。これなら、後述のCVE-2017-15095 にも対応しています。
- https://cwiki.apache.org/confluence/display/WW/Version+Notes+2.5.14.1
- S2-054 : https://cwiki.apache.org/confluence/display/WW/S2-054
- こちらはREST pluginでJSON-lib( http://json-lib.sourceforge.net/ )という古いJSONライブラリを使っていたが、DoSの問題が指摘されていたため、Jacksonに変更した修正になります。
REST plugin では、以前からJSON-libを使ったhandlerとJacksonを使ったhandlerが組み込まれていて、利用者側で選べるようになっていたようです。 S2-054ではデフォルトhandlerをJacksonに切り替え、さらにJacksonのバージョンが古かったのを S2-055 で最新にした、というのが今回の修正の全容と思われます。
では一体、CVE-2017-7525はどんな脆弱性なのか?筆者自身が、普段JavaでJSON処理をするときにJacksonを使っていることもあり、12月2, 3の土日を使ってこの問題を調べてみたのが本記事になります。
サンプルコード検証に用いた筆者の環境:
- OS : Windows 10 Pro 64bit
- Java : Oracle JDK 1.8.0_92 64bit
- Groovy : 2.3.1
Adam Caudill氏のblogで、CVE-2017-7525 の解説が公開されています。
筆者自身の言葉でざっくりまとめると、jackson-databindではJSONをJavaのオブジェクトにマッピングする機能(ObjectMapper
クラス)を提供しています。
ここで ObjectMapper.enableDefaultTyping()
を呼ぶことにより、JSON中に独自に埋め込んだクラス名でマッピングすることが可能となります。
「入力JSON中からクラス名を指定可能」という時点で嫌な予感を抱いた方もいると思いますが、まさにその悪い予感が的中したのが CVE-2017-7525 となります。
脆弱性の説明に入る前に、そもそもなぜそのような機能が実装されたのか説明します。
jackson-databind によるdeserializeの基本的な使い方は次のサンプルコードをご確認ください。(本記事ではJacksonのサンプルコードでGroovyを使っています。@Grab
で jackson-databind のバージョンを簡単に切り替えられるのが便利です。)
上記サンプルコードでは単純に"animal" キーがそのまま Animal クラスにマッピング可能です。 では、以下のようなケースではどうでしょうか?
class Zoo {
Animal animal;
}
abstract class Animal {
String name;
protected Animal() { }
}
class Dog extends Animal {
double barkVolume;
Dog() { }
}
class Cat extends Animal {
boolean likesCream;
int lives;
Cat() { }
}
この構成では、"animal"のキーの中身がDogクラスを指す場合と、Catクラスを指す場合の2種類が出てきてしまいます。したがって、どちらのクラスでマッピングするのか追加の情報が必要となります。
これを解決するため、jackson-databindではJSONにマッピングするクラス名を埋め込める独自処理を組み込みました。 例えば以下のように、"animal"キーの中身を配列にしてしまい、最初の要素にクラス名を指定します。
{"animal":["Dog",{"name":"dog1","barkVolume":1.2}]}
これにより ObjectMapper.readValue()
は "animal" キーの中身が Dog クラスだと認識してマッピングを行います。
もちろん、そのままでは "animal" キーの中身がもともと配列だったのか、jackson-databind独自のクラス名情報が含まれたものなのか、判別できません。
これを切り替えるのが ObjectMapper.enableDefaultTyping()
メソッドになります。
他にも @JsonTypeInfo
アノテーションをクラスに定義する方法もあります。詳細は以下のJacksonドキュメントを参照してください。
- JacksonPolymorphicDeserialization
実際に ObjectMapper.enableDefaultTyping()
メソッドを使ったサンプルコードを次に示します。
以上見てきたように、クラス名に続いてそのプロパティをJSONで与えることで、ある程度制限はあるものの、任意のクラスを任意のプロパティで生成することが可能となります。 これを悪用したのがCVE-2017-7525の脆弱性で、きっかけとなったのは恐らく次のレポートと思われます。
- Java Unmarshaller Security - Turning your data into code execution
JacksonなどJavaでよく使われているserialize/deserializeライブラリについて、クラス名などの操作で任意コード実行につながる危険性がレポートされており、実際にどのようなクラスが危険か具体的なクラス名がリストアップされています。
これを受けてのものか分かりませんが、日付的には上記リポジトリの1st commitの直後に、jackson-databind で以下のIssueが立てられ、対応が始まりました。
- Jackson Deserializer security vulnerability
実際にこの脆弱性を突くようなJSONデータとJavaコードはどのようなものでしょうか? このIssueで対応された jackson-databind 2.8.9 のテストコードにヒントがあります :
このテストコードを元に、動作確認できるよう調整したサンプルコードを次に示します。
@Grab
で 2.8.9 を指定して実行すると your jackson version IS SAFE to CVE-2017-7525
と表示されます。これは 2.8.9 の修正によりインスタンス化されるクラス名指定でblacklist検査が追加されたことによります。
ここで @Grab
に 2.8.8 を指定して実行すると、以下のように出力されます。
your jackson version MAY NOT BE SAFE to CVE-2017-7525
com.fasterxml.jackson.databind.JsonMappingException: N/A
at [Source:
{
"id" : 124,
"obj" : [
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
{
"transletBytecodes" : [ "AAIAZQ==" ],
"transletName" : "a.b",
"outputProperties" : { }
}
]
}
; line: 9, column: 28] (through reference chain: Bean1599["obj"]->com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl["outputProperties"])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:277)
(...)
at org.codehaus.groovy.tools.GroovyStarter.main(GroovyStarter.java:128)
Caused by: java.lang.NullPointerException
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$1.run(TemplatesImpl.java:401)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:399)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:507)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.fasterxml.jackson.databind.deser.impl.SetterlessProperty.deserializeAndSet(SetterlessProperty.java:116)
... 30 more
null
出力結果中に at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$1.run(TemplatesImpl.java:401)
という行があります。
このサンプルコードではNullPointerExceptionがthrowされてしまっていますが、メソッド名的に、いかにも何か副作用を含む処理が発生していることが伺えます。
実際にコード実行を成功させるようなJSONを組み立てるには、さらなる調査が必要と思われます。 本記事では一旦ここまでの紹介に留めますが、さらなる調査記事が他のサイトで公開されれば、こちらにも追記していきたいと思います。
ところで肝心のblack listはどこで実装されているのか?ですが、以下のクラスになります。
実は 2.8.9 の時点ではこのblack listに漏れがあったようです。その問題が、CVE-2017-15095 になります。
black list 漏れの改善ですが、まず FasterXML/jackson-databind#1680 で s.add("com.sun.rowset.JdbcRowSetImpl");
が追加されました。
一旦それで 2.9.0 がリリースされた後、さらに FasterXML/jackson-databind#1737 で以下のblack listチェックが追加されてます。
// [databind#1737]; JDK provided
s.add("java.util.logging.FileHandler");
s.add("java.rmi.server.UnicastRemoteObject");
// [databind#1737]; 3rd party
s.add("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor");
s.add("org.springframework.beans.factory.config.PropertyPathFactoryBean");
s.add("com.mchange.v2.c3p0.JndiRefForwardingDataSource");
s.add("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource");
これで 2.8.10 / 2.9.1 がリリースされ、CVE-2017-15095 への対応完了となっています。
2.8.10 でのblack list動作をチェックするテストコード:
このテストコードを元に、動作確認できるよう調整したサンプルコードを次に示します。
対応が完了したとされている 2.8.10 を @Grab
で指定して動かしてみると、your jackson version IS SAFE to CVE-2017-15095
と表示されます。
続いて、black list改善前の 2.8.9 を @Grab
で指定して動かすと、以下が出力されます。
your jackson version MAY NOT BE SAFE to CVE-2017-15095
com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of java.util.logging.FileHandler, problem: \tmp\foobar.txt.lck
at [Source:
{
"v" : [
"java.util.logging.FileHandler",
"/tmp/foobar.txt"
]
}
; line: 5, column: 5] (through reference chain: PolyWrapper["v"])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:277)
(...)
Caused by: java.nio.file.NoSuchFileException: \tmp\foobar.txt.lck
(...)
at java.util.logging.FileHandler.openFiles(FileHandler.java:459)
at java.util.logging.FileHandler.<init>(FileHandler.java:292)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.fasterxml.jackson.databind.introspect.AnnotatedConstructor.call1(AnnotatedConstructor.java:129)
at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:318)
... 31 more
null
対応前のバージョンでは java.util.logging.FileHandler
クラス名がblack listチェックをすり抜け、インスタンス化されることで実際にファイルをオープンを試みていることがわかります。
対応後のバージョンでは black listチェックで捕まり、 JsonMappingException
例外がthrowされています。
なお 2.8.10 時点のblacklistは以下のようになりました。[databind#1737]
のコメントで始まっているところが、CVE-2017-15095に対応した追加のblacklistになります。
以上の結果をまとめると、jackson-databindの脆弱性の影響を受けるのは、以下のような条件が必要となります。
- jackson-databind 2.8.9 / 2.9.0 以下を使用中
- 信頼されないソースから取得したJSONについて、以下のいずれかの処理を行っている。
ObjectMapper.enableDefaultTyping()
を呼んでからdeserializeしている。ObjectMapper.enableDefaultTyping()
は呼んでいないが、クラス宣言で@JsonTypeInfo
アノテーションを使ってマッピングできるようにして、deserializeしている。- アプリケーションコードで使っていなくても、フレームワーク側で
Accept
リクエストヘッダーやURLの拡張子に応じて自動でdeserializeする場合があります。
- classpath中に、Java serialize/deserializeの脆弱性で悪用される可能性のあるクラス("Gadget")を含んでいる。
- マッピング先のJavaクラスのメンバフィールドで、Object型などGadgetクラスを受け入れられるような型を使っている。
- Gadgetに使われるクラスと互換性の無い、アプリケーション固有のBeanクラスなどを型としていれば、実際にインスタンスを生成する前に型チェックのエラーで弾くことができます。(アプリケーション固有のBeanクラスそれ自体にdeserializeの脆弱性が潜んでいた場合を除く)
本当に影響をうけるかどうかについては、ObjectMapper
の使い方/設定状態や@JsonTypeInfo
アノテーションの組み合わせ、さらにマッピング先のクラスのメンバフィールド等など、アプリケーション側のコードに大きく左右される状況のようです。
また、JSON中のキー名が、deserialize先のJavaクラスに存在しない場合、Jacksonでは単に無視されます。 そのため、攻撃を成功させるにはアプリケーションごとのJSONに合わせてカスタマイズが必要となり、複数のアプリケーションで使いまわせるような攻撃コードを作るのは非常に難しいと思われます。
さらに条件4. について、一般的な書き方であればまず、わざわざJavaクラスのフィールドを Object 型にすることは無いと思います。 任意のコード実行が可能になるクラスも、大概はアプリケーションで作り込むBeanクラスとは互換性が無い場合が大半と思われます。
以上から、大規模な攻撃が発生したり、実際にこの脆弱性を悪用して被害を発生させる(=攻撃が成功する)可能性は低いと思われます。
アプリケーション側の対応ですが、条件 3. についてはJDKに含まれるクラスも悪用可能ですので事実上対策できないものと思われます。
そのため、基本的にはjackson-databindを最新バージョンにUPすることが対策となります。
jackson-databindを最新バージョンにUPできない場合は、ObjectMapper.enableDefaultTyping()
の呼び出しや@JsonTypeInfo
アノテーションを削除して、それに依存しないような設計に改修することになります。例えば、カスタムでシリアライザを作成するなどが考えられます。
とはいえリモート呼び出し可能なAPIとして既に使い始めている場合、おいそれとJSONフォーマットを変更できません。
@JsonTypeInfo
アノテーションについて、設定次第ではサブクラスを限定できるなど、プログラマが想定しているクラス名のみを受け付けられるよう設定できるようです。
詳細は以下のドキュメントをご確認ください。
- JacksonPolymorphicDeserialization
jackson-databind 2.8.10 / 2.9.1 は、black list対策によりCVE-2017-7525, CVE-2017-15095 に対応しました。 しかしながら、Struts2のOGNL関連の問題で明らかなようにblack list対策は万全とは言えません。 (筆者個人としては、スキャンツールをかけるだけのいわゆる「スクリプトキディ」のレベルであればある程度実効性のある対策だと思います。)
よって本当に抜本的な対策をするのであれば、ObjectMapper.enableDefaultTyping()
などJSONにクラス情報を埋め込み利用する機能、それ自体を無効化する/使わないことが重要と筆者は考えます。
では、そもそも ObjectMapper.enableDefaultTyping()
が解決しようとしていた問題はどうすれば良いのか?
これについて、筆者自身も正解と言える代案は用意出来ていません。 問題の根っことしては、「マッピングするJavaクラスが曖昧なときに、信頼できないJSONに頼らずに、マッピングできること」だと思います。 それを可能にするアプローチとしては、恐らくカスタムのデシリアライザを作るアプローチがあるのではないか、と筆者は考えます。 カスタムのデシリアライザは、プログラムコード側でdeserializeの最中にJSONを受け取り、生成するObjectを自分で制御することが可能です。
例えば {"animal":{"name":"dog1","barkVolume":1.2}}
が来たら「barkVolume
キーがあるから、これはDogクラスとしてインスタンス化する」と判断させたり、
{"animal":{"name":"cat1","likesCream":true,"lives":10}}
が来たら「likesCream
キーと lives
キーがあるから、これはCatクラスとしてインスタンス化する」とプログラムで判断させることが可能となります。
これを使えば、JSONにわざわざクラス名を埋め込む必要が無くなります。
個人的にはクラス名を埋め込むJSON形式は、他の言語/ライブラリとの相互運用性に支障が出る印象を受けました(他の言語やライブラリで、同様の拡張が可能なものがあれば教えてください)。 他のシステムと相互運用を行う前提であれば、わざわざJacksonの都合にあわせて独自にクラス名を埋め込んでもらうよりは、Jackson側でカスタムのデシリアライザを作成して対応するほうが筋が良いように思います。
他にもいくつか解決策はあると思いますので、読者の方で「こうした解決策もあるのでは」というのがありましたらご意見お寄せいただければ幸いです。
(極端な例ですと、個別のクラスにマッピングせず、全て Map<String, Object>
か List<String, Object>
形式にdeserializeするというやり方もあると思います。)
カスタムのデシリアライザを作るための参考記事を何点か見つけましたので、英語記事になりますがリンクを貼っておきます。
- Jackson: create a custom JSON deserializer with StdDeserializer and JsonToken classes | Dede Blog
- Getting Started with Deserialization in Jackson | Baeldung
- Custom JSON Deserialization with Jackson - DZone Integration
- Building a Custom Jackson Deserializer - The Boy Wonders
jackson-databind の JavaDoc (2.8系, 2.9系):
- https://fasterxml.github.io/jackson-databind/javadoc/2.8/
- https://fasterxml.github.io/jackson-databind/javadoc/2.9/
筆者の方でもカスタムのデシリアライザのサンプルコードを作ってみましたので、ヒントになれば幸いです。
ここまでは Jackson 自体の脆弱性について見てきました。では、実際にそれが Struts2 の REST plugin にどのように影響しているのか? Struts2 に付属している struts2-rest-showcase を使って確認してみました。
Struts REST plugin の使い方は以下を参考にしました。
ところで、CVE-2017-7525 で実際に脆弱となる条件として、以下の条件がありました。
- 信頼されないソースから取得したJSONについて、以下のいずれかの処理を行っている。
ObjectMapper.enableDefaultTyping()
を呼んでからdeserializeしている。ObjectMapper.enableDefaultTyping()
は呼んでいないが、クラス宣言で@JsonTypeInfo
アノテーションを使ってマッピングできるようにして、deserializeしている。
Struts2 REST plugin でこれらの条件に該当するコードがあるか確認したところ、いずれも含まれていないことが確認できました。
実際のところ、対応前の 2.5.14 の時点で、ObjectMapper.enableDefaultTyping()
/ @JsonTypeInfo
のいずれも、REST plugin は元より Struts2 のソースツリー全体をgrepしても使っているところはありませんでした。
Struts2 のソースツリー全体で、Jacksonの ObjectMapper を使っているのは org.apache.struts2.rest.handler.JacksonLibHandler クラスだけです。ソースコードを確認したところ、2.5.14 の時点で、たしかに ObjectMapper.enableDefaultTyping()
は使っていません。
- https://github.com/apache/struts/blob/STRUTS_2_5_14/plugins/rest/src/main/java/org/apache/struts2/rest/handler/JacksonLibHandler.java
- このJavaファイルは、2.5.14.1 でも内容は変わっていません。
このため、CVE-2017-7525 に対して REST plugin それ自体は 2.5.14 の時点でも問題ない状況だったと思われます。
脆弱となるのは、アプリケーション側でJSONにマッピングするクラスのフィールドに @JsonTypeInfo
を設定した場合となります。
よって、以下の struts2-rest-showcase を使った検証では、アプリケーション側に追加したマッピング先のフィールドで @JsonTypeInfo
を設定し、検証しています。
ちなみに、Struts2では JSON plugin というのもあるようです。
- http://struts.apache.org/plugins/json/
- JSON pluginのソースを見てみますと、pom.xml では他のJSONライブラリを依存関係に入れていません。
- どうも、独自にJSON処理を実装しているようです。
- https://github.com/apache/struts/tree/STRUTS_2_5_14/plugins/json
- そのため、jackson-databind の脆弱性は JSON plugin には影響しないと思われます。
struts2-rest-showcase は REST plugin を使って Order クラスのCRUDを実装したサンプルです。ここに、Jacksonの脆弱性のサンプルコードで使った Zoo / Animal / Cat / Dog クラスと、それの CRUD をJSONで処理するための ZooController などを追加してみます。
全体のソースコードは以下をご確認ください。(本記事ではJDK8でビルド・実行を確認)
主な修正点:
- jetty-maven-plugin による listening ポートを 18088 に変更した。(
mvn jetty:run
) - jackson-core, jackson-databind を依存関係に追加した。
- Zoo, Animal(abstract), Dog, Cat クラスを追加した。サービスレイヤーとして ZooService クラスを追加。
- 最低限のCRUDを備えた ZooController を追加。(ViewとなるJSPは省略した)
- struts.xml でjson用のハンドラを JacksonLibHandler に変更した。
- maven-wrapperを組み込み、JDKさえ入っていれば mvnw / mvnw.bat でそのままビルド・実行できるようにした。
ビルドと実行:
- リポジトリを clone 後、rest-showcase ディレクトリにcdし、
mvnw jetty:run
を実行します。(初回実行時はmavenのダウンロードが発生するため、数分~場合によっては10分以上待たされる場合がありますのでご注意ください) - http://localhost:18088/struts2-rest-showcase/ にアクセスし、Orderの一覧が表示されれば成功です。
- 実行を終了するには Ctrl-C で終了できます。
- Javaファイルを修正したら、Ctrl-Cで終了させまた
mvnw jetty:run
を実行してください。
curlコマンドでの動作確認: (local http proxy として localhost:8080 を通す前提)
一覧取得:
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo"
ID指定:
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo/1"
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo/2"
削除:
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo/2" -X DELETE
リポジトリの Zoo.java での animal
フィールドは以下のように @JsonTypeInfo
はコメントアウトされ、abstract class の Animal 型となっています。
//@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY)
public Animal animal;
//public Object animal;
ここで、POSTメソッドでJSONリクエストを送信し、ZooController.create() メソッドを呼び出してみます。
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo" -X POST -H "Content-Type: application/json" -d '{"id":"3","animal":{"name":"dog2","barkVolume":2.3}}'
すると以下のエラーメッセージを含む例外が発生しました。Animalクラスはabstractなため、インスタンスを生成できていません。
Can not construct instance of org.demo.rest.example.Animal: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
そこで animal
フィールドの @JsonTypeInfo
のコメントアウトを外して有効化します。Webアプリケーションは Ctrl-C で中止し、もう一度 mvnw jetty:run
を実行します。
// 次のimportを忘れずに追加
import com.fasterxml.jackson.annotation.JsonTypeInfo;
//...
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY)
public Animal animal;
//public Object animal;
これでJSON中にクラス名を埋め込めるようになります。以下のcurlコマンドで、クラス名を埋め込んだJSONをPOSTしてみます。
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo" -X POST -H "Content-Type: application/json" -d '{"id":"3","animal":["org.demo.rest.example.Dog",{"name":"dog2","barkVolume":2.3}]}'
→ HTTP/1.1 201 Created
が返されます。一覧取得をしてみると、たしかに追加されたことを確認できます。
リポジトリの pom.xml では <parent>
の struts のartifactで、バージョンを 2.5.14 を指定しているため、そのままではCVE-2017-7525に脆弱です。
それを確認するため、以下のcurlコマンドを実行してみます。クラス名とその中身は cve-2017-7525-check.groovy を参考にしていますので、jackson-databind 2.8.8 の時と同じ反応が返されれば、脆弱であると考えられます。
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo" -X POST -H "Content-Type: application/json" -d '{"id":"3","animal":["com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",{"transletBytecodes":["AAIAZQ=="],"transletName":"a.b","outputProperties":{}}]}'
→ 以下のレラーメッセージを含む例外がthrowされました。
java.lang.IllegalArgumentException: Class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl not subtype of [simple type, class org.demo.rest.example.Animal]
animal
フィールドの型が com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl クラスのサブタイプではない Animal クラスのため、IllegalArgumentException が発生してしまったようです。
では、Zoo.java の animal
フィールドを Object 型に修正してもう一度 mvnw jetty:run
で実行してみます。
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY)
//public Animal animal;
public Object animal;
これで先程のcurlコマンドをもう一度実行すると、以下のスタックトレースを含む例外が発生しました。
Caused by: java.lang.NullPointerException
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$1.run(TemplatesImpl.java:401) ~[?:1.8.0_92]
これは cve-2017-7525-check.groovy で検証した時と同じ、脆弱な場合の例外です。
以上より、Struts2 REST plugin 2.5.14 で、 jackson-databind の脆弱性 CVE-2017-7525 の存在を確認できました。
また、@JsonTypeInfo
に加えて Object 型を使う必要があることもわかりました。
以下は筆者個人の見解ですが、REST APIを作成する際のデータ用クラスで、フィールドの型としてわざわざ、Objectクラスやjava deserializeの脆弱性でGadgetとして使えるクラスと互換性のあるクラスを指定することは一般的とは思えません。そのため、実際に攻撃を成功させるのは難しいのではないか、と感じています。
それではバージョン 2.5.14.1 で脆弱性が修正されたか確認してみます。
pom.xml の <parent>
artifactのバージョンを 2.5.14.1 に修正し、 mvnw jetty:run
で再起動し、先ほどと同じcurlコマンドを実行してみます。
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo" -X POST -H "Content-Type: application/json" -d '{"id":"3","animal":["com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",{"transletBytecodes":["AAIAZQ=="],"transletName":"a.b","outputProperties":{}}]}'
→ 以下のエラーメッセージを含む例外が発生しました。
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl`: Illegal type (com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl) to deserialize: prevented for security reasons
これは cve-2017-7525-check.groovy で検証した時と同じ、脆弱性が修正された後の例外です。
Eclipse等のMavenに対応したIDEで開いて、依存性を解決した後のjackson-databindのバージョンを見てみると、たしかに 2.9.2 になっていることが確認できると思います。IDEがない場合は、 mvnw help:effective-pom
で最終的なpomを出力できますので、そこで jackson-databind を検索すれば version 2.9.2 を使用していることを確認できると思います。
CVE-2017-15095 については省略しますが、以上より 2.5.14.1 で jackson-databind の脆弱性に対応できたことを確認できました。
※ 2017-12-08 追記
S2-055についての調査記事 & PoCレポートが公開されました。
- S2-055漏洞环境搭建与分析 | 绿盟科技博客
Google翻訳のお世話になりつつ読んでみますと、発生条件や攻撃の実現度について同じ見解のようです。
実際にrest-showcaseを修正し、電卓が起動したHTTP通信のPoCも示されています。
JSON部分だけ拝借し、まずはJackson単体で試してみたのが次のサンプルコードになります。
ここで 2.8.8 を使うバージョン指定で実行してみると、筆者の環境では以下のように出力されました。
your jackson version MAY NOT BE SAFE to CVE-2017-7525
(...)
Caused by: java.lang.NullPointerException
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$1.run(TemplatesImpl.java:401)
(...)
cve-2017-7525-check.groovy の時と同様、run()メソッドが走ってNullPointerExceptionが発生しました。ただ、電卓は起動しませんでした。
推測になりますが、xalanのTemplatesImplはJavaの中に入ってるクラスなので、Java側で何かしら修正をしている可能性が考えられます。 あるいは、実際の攻撃成功にはもっとナイーブな条件が必要なのかもしれません。
PoC記事の方では検証に使ったJavaのバージョンまでは示されておらず、電卓を動かすところまで辿り着けませんでした。 追加情報が出たら、また検証してみたいと思います。
ここまで S2-055 を発端として主にJacksonの脆弱性 CVE-2017-7525, CVE-2017-15095 について紹介してきました。 では、もう一方の S2-054 についてはどういう状況か、ざっくりと調べた結果をまとめます。
結論から言うと、本記事執筆時点(2017-12-03) では具体的な情報は見つけられませんでした。脆弱性有無を検査できるPoCなども見つけられていません。
S2-054 の情報公開ページでは REST plugin では古いJSON-libライブラリが使われており、改ざんされたJSONによりDoSアタックが可能と説明されています。
The REST Plugin is using an outdated JSON-lib library which is vulnerable and allow perform a DoS attack using malicious request with specially crafted JSON payload.
この問題の対応としてリリースされた 2.5.14.1 のページでは、これに対応するJIRAのチケットとしてWW-4892がリンクされています。
WW-4892 を確認してみたのですが、どこにもDoSやJSON-libの脆弱性について言及がありません。"Description"を読んでも、単にJSON-libが古くてメンテされてないので、デフォルトhandlerをJacksonに変更する、としか書かれてないように読めます。
GitHub側のpullreqは以下のようですが、こちらでも具体的なJSON-libの問題への言及がありません。
そこで、JSON-lib側を見てみることにしました。公式サイトは以下になります。
また2017年現在は、GitHubで管理されているようです。
どちらが最新でしょうか?本記事執筆時点ではGitHub側でのリリースはありません。そこでMaven Centralリポジトリの登録状況を見てみます。 "json-lib" で検索すると、いくつかgroupIdがヒットします。
どのgroupIdが正解か、実際に Struts2 2.5.14.1 の REST plugin のpom.xmlを確認します。
- https://github.com/apache/struts/blob/STRUTS_2_5_14_1/plugins/rest/pom.xml
- → groupId = net.sf.json-lib, artifactId = json-lib でした。
groupId = net.sf.json-lib, artifactId = json-lib のリリースバージョンを見てみると、2010年12月のバージョン 2.4 が最後のリリースです。
sourceforge側のページを確認してみると、やはりこちらも2012年12月のバージョン2.4が最後のリリースです。
実際、GitHub側にはリリースタグこそ打たれていませんが、commitログをたどると 2010年12月にバージョン2.4のリリースというcommitがあります。 また、それ以降はpullreqマージは動いていますが、リリースの動きはありませんでした。
GitHub側のIssueをclosed含め見てみると、DoSにつながるようなタイトルは見当たりません。
sourceforge側のチケットを見てみると、ようやく memory leak 問題のチケットに突き当たりました。いずれもまだ修正されていない模様です。
- Json-lib / Bugs / #124 memory leak in 2.2.2, not fixed correctly in 2.4
- Json-lib / Bugs / #118 Possible memory leak in Tomcat
- → 2.2 までで ThreadLocal を使ったことによるmemory leak問題があって、2.4でSoftReferenceを使って対応しているが、根本的な対応になってないよ、というチケット内容と読み取れました。
ようやくmemory leak問題が残っているらしい、というところまでは辿り着けたのですが、筆者の力量と時間の都合で、この先の調査まではできませんでした。 もし実際にこのパターンのJSONでmemory leakが発生した、あるいはDoSになった、という具体的な情報があれば、ご教示いただけると大変助かります。
Jacksonを使っているOSSは沢山あるため、この脆弱性に影響を受けた他のライブラリ・フレームワークも存在します。 例としてPivotal製品群の Spring Security が影響を受けており、2017年6月に情報公開されています。
- CVE-2017-4995: Jackson Configuration Allows Code Execution with Unknown “Serialization Gadgets” | Security | Pivotal
それ以外の、例えば Spring Framework 本体はどうかというと、少なくとも https://pivotal.io/security/ の方にはJackson由来のアップデート情報は公開されておりません。
だからといって安心することはできません。もともと Spring Framework でのJacksonはかなりカスタマイズが容易な設計になっており、アプリケーション固有の ObjectMapper を生成することも可能です。Jacksonの設定/機能をアプリケーション側でカスタマイズしていないか? @JsonTypeInfo
を使っていないか?など念のため確認したほうが良いでしょう。
jackson-databind のGitHub Issue/リリース情報や、RedHat のbugzilla などの参考リンクを時系列順に整理してみました。 間違っていたら遠慮なく筆者までご指摘・ご連絡ください。
- FasterXML/jackson-databind#1599 にて初期の改修が進む。バージョン管理の都合で、hot-fix扱いで以下のバージョンがリリースされる。
- 2.7.9.1 : 2.7.9 に対するhot-fix
- 2.8.8.1 : 2.8.8 に対するhot-fix
- 他、2.9.0.pr3 もリリースされている。
- その他の改修も含めた 2.8.9 がリリースされる。
- 次のbugzillaでRedHat製品での対応が進む。
- 既にサポートが終わっていた 2.6系 の2.6.7に対して、この問題についてのhot-fixとして 2.6.7.1 がリリースされる。
- RedHatからCVE-2017-7525の情報が公開される。
この時点でのblack-listの一覧は以下になっていた。
s.add("org.apache.commons.collections.functors.InvokerTransformer");
s.add("org.apache.commons.collections.functors.InstantiateTransformer");
s.add("org.apache.commons.collections4.functors.InvokerTransformer");
s.add("org.apache.commons.collections4.functors.InstantiateTransformer");
s.add("org.codehaus.groovy.runtime.ConvertedClosure");
s.add("org.codehaus.groovy.runtime.MethodClosure");
s.add("org.springframework.beans.factory.ObjectFactory");
s.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
s.add("org.apache.xalan.xsltc.trax.TemplatesImpl");
ここで、同月中にblack-list対策の追加修正として 2.9.0 がリリース。
これが追加された:
s.add("com.sun.rowset.JdbcRowSetImpl");
さらに同月中、次のIssueがオープンされる。
→black-listに以下が追加され、これが 2.8.10 / 2.9.1 に取り込まれる。
// [databind#1737]; JDK provided
s.add("java.util.logging.FileHandler");
s.add("java.rmi.server.UnicastRemoteObject");
// [databind#1737]; 3rd party
s.add("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor");
s.add("org.springframework.beans.factory.config.PropertyPathFactoryBean");
s.add("com.mchange.v2.c3p0.JndiRefForwardingDataSource");
s.add("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource");
- #1680, #1737 に対応した 2.8.10 がリリース。
- また8月になり次のIssueがオープンし、CVE-2017-7525 としての対応が包括的にやり取りされている。
- Adam Caudill氏のblogで、CVE-2017-7525 のexploit解説が公開される。
- #1737 に対応した 2.9.1 がリリース。
次のbugzillaで、CVE-2017-7525 修正時点の 2.8.9 / 2.9.0 では対応が不十分であり、新たに CVE-2017-15095 として最新の 2.8.10 / 2.9.1 のblack-listを適用する作業が始まる。
-
RedHatからCVE-2017-15095の情報が公開される。
-
jackson-databind の Issue でも、以下で CVE-2017-15095 への対応状況の質疑応答がやり取りされている。
- FasterXML/jackson-databind#1847
- 2.8.10 / 2.9.1 にて対応がされてるよ、と回答されている。
- クラウド側WAF Scutum の開発者ブログにて解説記事が公開される。
- https://www.scutum.jp/information/waf_tech_blog/2017/12/waf-blog-052.html
- こちらの記事では、2.9.3 のblack list で Spring のクラスに不備があり、緊急で問題になることは無いが、もしかしたらまたアップデートが掛かる可能性があるとのこと。
- また、この脆弱性の責任はそもそもライブラリ側にあったのか?アプリケーション側ではないか?という点についても論じられている。
一昨年~去年くらいからJava の serialize / deserialize 関連の脆弱性情報が増えだした印象があります。 例としてSpring含む Pivotal 製品の脆弱性情報は https://pivotal.io/security/ で公開されているのですが、実際、2017年に入り CVE-2017-4995 に加え以下の脆弱性情報が公開されています。
- https://pivotal.io/security/cve-2017-8045
- Spring AMQP での
org.springframework.amqp.core.Message
のdeserialize問題によるRCE
- Spring AMQP での
- https://pivotal.io/security/cve-2017-8046
- Spring Data REST での PATCH メソッドでJSONの扱いに不備があり、RCE
RCEにつながりやすい脆弱性ということもあり、攻撃者/脆弱性の研究者共に Java の serialize/deserialize に注目を集めている時期なのだと思われます。 ここ数年の間は今後も serialize/deserialize 処理に関わる脆弱性報告が続くと思われます。 とはいえ Jackson もそうですが、開発を効率化してくれる上にリモートAPIによるデータのやり取りが当たり前になった現在の開発で、serialize/deserialize を全く使わない、またはゼロから自作するというのは現実的には不可能、という現場が多いのではないでしょうか。 重要なのは、脆弱性が公開されたらなるべく早くライブラリをアップデートできるようなフットワークの軽い開発体制および文化を作り上げていくことではないか、と筆者個人は思います。
依存しているミドルウェア・ライブラリ・フレームワークの脆弱性とこれからどう向き合っていけば良いのか、メンタル的な部分で思うことがありましたので、以下に感想として書いてみました。
S2-054, 055が公開される、それにはJacksonの脆弱性の影響がある、という情報を目にしたとき、筆者はかなりの衝撃を受けました。 というのも、その数日前に同僚から「JavaでJSONをパースするのにオススメのライブラリはあるか?」と聞かれ、「JacksonがOSSでも広く使われて実績があるし、ググれば沢山記事やQAが見つかるのでオススメですよ」とドヤ顔で答えたばかりだったのです。
筆者自身が、社内のツール開発などでJacksonを利用しており便利さを感じていました。 しかしながら、その時点で筆者はCVE-2017-7525を把握しておらず、Jacksonが広くOSSで使われて利用者が多いことに安心しきっていたのです。
そこに別の同僚が Adam Caudill 氏のblogを見つけてCVE-2017-7525の存在を教えてくれたのですが、自ら利用しているJacksonをドヤ顔で奨めた数日後にJacksonの脆弱性でStruts2がアップデート公開、しかもJacksonの脆弱性自体は数ヶ月前から対応されていたとなれば、セキュリティ業界に身をおいているエンジニアとして、自らが使っているライブラリの脆弱性情報の収集を怠っていたと指摘されても言い逃れようがありません(実際そうだったとしか言えないのですが)。
そのためここ数日の筆者のメンタルコンディションは最悪の状態(血圧・脈拍の上昇、手が震える、悲しくないのに涙が出そうになる、動悸が止まらない、など)で、なんとかするために、とにかくCVE-2017-7525が何なのか、Jacksonは一体どういう状況なのかをまず知ることから始めようと、土日を費やして本記事の調査・執筆に勤しんだ次第です。
この点について振り返ってみますと、やはり開発に専念していると中々、依存ライブラリのアップデート情報に気を配るのは難しいものがあると痛感します。 次から次へとやってくる開発作業の中で、使用するライブラリ全ての機能や、脆弱性など品質面の完全な調査を行うことは現実的に不可能です。 一方で開発に求められる機能はますます多くなり、その全てを自作するのも現実的には不可能です。 どこかで使用するライブラリを「信頼」し、開発を効率化する必要があります。 とはいえ日頃の開発作業の中で使用したツール・ライブラリの全てのアップデート情報をトレースし、セキュリティ問題の修正が含まれていないか目を光らせるのも、これもまた非常に難しいものがあります。 もちろんその課題を解決するために、昨今は使用するツール・ライブラリを登録することでそれらのアップデート情報を配信してくれるサービスがあるのは認識しています。
今回自らのメンタルコンディションの悪化で感じたのは、自分の中で「やっていなかったことに対する自責の念・罪悪感」が相当強い、ということです。 「セキュリティエンジニアであるのに、自ら使ってたライブラリの脆弱性情報を把握していないなんて・・・」というネガティブなレッテル張りを自分に対して行っていたのです。 セキュリティ業界とは関わりの無い一般の開発者でも、 「あの時 Struts2 を提案していなければ・・・」とか「もっとライブラリ/フレームワークのライフサイクル管理をしっかりしなければ(=できていない今の自分/状況が不安)」 などの後悔や不安を心に抱えている人も多いのではないでしょうか。
仮にこれらを「課題」として真正面から「解決」しようとすれば、脆弱性情報を逐一収集し、使用予定のライブラリやフレームワークのソースコードと機能を逐一チェックし、デファクトスタンダードと言えるか入念に調べ、運用開始後もライフサイクル管理をきっちりと行うことになるでしょう。
しかしながら、そのような「石橋を叩いて渡る」やり方は、昨今の多様な開発現場においてそもそも可能でしょうか?
筆者が本記事を書いて思ったことは、「やっていなかったこと/できていなかったこと/気づけなかったこと」を原因や犯人とみなし、それを消す=「できるようにする」ことのみを「課題の解決」とする時代はもう終わったのではないか、ということです。 そのような文化では、完璧な人間出ない限りは、開発者は自らの「できていないこと、しなかったこと、気づけなかったこと」に延々と相対し続けることになります。 完璧な人間がいない以上、よほどメンタルの強い人間でないと保たないのではないかと思います。
ソフトウェアのセキュリティ問題において、実際に被害を発生させる犯人は攻撃者です。状況をマイナスにしているのは、脆弱性を悪用する攻撃者です。
大多数の開発者は、基本的には善意で真面目に開発に取り組んでいるはずです。その時点で、既にプラスの状態なのです。 「ライブラリ/フレームワークのライフサイクル管理をしていない」「使用しているライブラリの脆弱性情報を収集・監視していない」のは、単にやっていないだけで、プラスでもマイナスでも無い状態です。 それなのに攻撃者が存在することで、「やってないこと」が「できなかったこと/気づけなかったこと」としてマイナスになるのは、あまりにも悲しい状況ではないでしょうか?
「石橋を叩いて渡る」価値観について、現代の日本社会や企業文化が影響している点は否めません。 実際にSQLインジェクション等の脆弱性を作り込んでしまい、攻撃者による被害が発生したケースも後を断たず、裁判で開発会社側の責任を問われる事例も発生しています。 業務上の不注意で大きな事故につながるケースもあります。
しかしながら、それら全てを「開発会社/開発者」の問題としてしまうと、全体としてのIT開発が萎縮してしまうように思います。 本当に悪いのは、脆弱性を悪用する攻撃者です。 そう考えると、「やっていない/できていない/気づけていない」開発者や開発会社は、加害者というよりはむしろ被害者と言えるのではないでしょうか? であれば、被害者に「やっていない/できていない/気づけていないお前が悪い」と指摘するのではなく、「こうすればより安全になる、改善できるから一緒に頑張ろう」と、共に歩むことを目的として暖かい手を差し伸べつつ、お互いにそれぞれの専門領域において切磋琢磨し、活かし合うことが重要ではないか、と強く思います。 そうなれば、開発会社/開発者も安心して脆弱性対応に入ることができ、またその後も安心をベースに積極的な開発を展開しやすくなる。 開発現場が安心して明るく、積極的な開発が増えていくことで、結果としてイノベーティブな成果が増え、日本社会がより豊かになっていくのではないでしょうか。
筆者個人の思いとしては、将来的に以下のような考え方が広がっていけば良い、と強く感じました。
- 現場の開発者
- 「脆弱性があるライブラリを使ってしまっていた」、それ自体はマイナスではない。
- マイナスにしているのは、脆弱性を悪用する攻撃者であり、またそれをマイナスと評価する文化そのもの。
- 日々真面目に仕事に取り組んで開発していること自体が既に十分にプラス。
- 脆弱性対応は、マイナスになったのをゼロに戻す作業ではなく、日々の開発成果をより安全なものに改良するためのプラスの作業。
- 開発者をまとめるマネージャ・リーダー、経営層
- 「やっていなかったこと/できていなかったこと/気づけなかったこと」をマイナス評価にしない。そのような価値観は、今後は徐々にフェードアウトしていく。
- 「やっていなかった/できていなかった/気づけなかった」メンバーを犯人扱いするのをやめる。彼らは、そして我々全体が、脆弱性を悪用する攻撃者にとっての被害者であり、その観点では立場は同じ。
- 「完全な施策」で解決しようとするアプローチを止める。
開発者やマネージャ・リーダー、経営層まで巻き込んで「物事の見方・捉え方を変える」という主旨になってしまいますが、それが非常に困難なことも明白です。 そもそも、どう変えれば良いのでしょうか? 筆者自身から正解を示すことは力不足でできないのですが、そこに一つのヒントがあると思います。 つまり、正解を探すことそれ自体を、もう諦めるしか無いのではないか、ということです。 あらゆるソフトウェア開発には何かしらの目的があります。それを安全に、セキュアな方式で実現するための「正解」を探し求めるのは、今や到底不可能なほどに、ソフトウェアの世界は複雑化しているのではないでしょうか。 恐らくそこにあるのは、多数のプレイヤーがそれぞれなりのやり方で試行錯誤し、フィードバックを得た上で相互に情報を交換し、そこからさらに自在に分岐・変化していくような、「変わること」を前提とした開発パラダイムなのではないか、と感じています。
システム開発では一回作ったソフトウェア資産を、数年、長いと数十年かけて使っていきます。 しかし、Web開発をはじめとしてインターネットに繋がったソフトウェアは、数ヶ月~数年で変化する環境にさらされることになります。 5年前の開発で使ったライブラリやフレームワークが、使えなくなっているかもしれないのです。 そうなると、最初に作ったライブラリ/フレームワークを「正解」とみなして、バージョンを「固定」にする開発パラダイムではデメリットの方が多くなります。 外の世界はどんどん変化しているため、使ってるライブラリ/フレームワークに脆弱性が見つかるのが当たり前の世界です。 バージョンを固定してしまうとそうした変化に追従できず、結果として脆弱性対応に非常な精神的/物理的コストを支払うことになります。 よって、これからは「変化すること・変化に追従できること」を前提とした開発パラダイムにシフトしていくことが、長期的にはより良い成果に結びつくのではないか、と強く感じました。
長くなりましたが以上です。
文責 : 坂本 昌彦 (研究開発部所属, 社内で使用するWebアプリケーション診断ツールの開発などに従事)
- Mail : [email protected]
- Twitter : https://twitter.com/msakamoto_sf
- Facebook: https://www.facebook.com/masahiko.sakamoto.75
- GitHub : https://github.com/msakamoto-sf
本記事に関するご意見・お問い合わせは坂本までお願いします。