
Struts2の脆弱性S2-045, S2-055 および Jackson の脆弱性 CVE-2017-7525, CVE-2017-15095 の調査報告
要点だけまとめ、読みやすくしたサマリ記事を公開しました。まず概要を知りたい方、あるいはお時間に余裕の無い方におすすめです。
2017年12月1日にStruts2のセキュリティアップデートが公開されました。 公開前からJackson(Javaで人気のあるJSONライブラリ)の脆弱性が関連している、という話がメーリングリストに流れており、社内システムやツールでJacksonを利用している筆者も具体的にどのような内容か気にしていました。
実際に公開された内容としては、以下2点のセキュリティ問題が修正されていました。Jacksonのコンポーネントであるjackson-databindの脆弱性が影響しているのはS2-055のみです。
REST plugin では、以前からJSON-libを使ったhandlerとJacksonを使ったhandlerが組み込まれていて、利用者側で選べるようになっていたようです。 S2-054ではデフォルトhandlerをJacksonに切り替え、さらにJacksonのバージョンが古かったのを S2-055 で最新にした、というのが今回の修正の全容と思われます。
では一体、CVE-2017-7525はどんな脆弱性なのか?筆者自身が、普段JavaでJSON処理をするときにJacksonを使っていることもあり、12月2, 3の土日を使ってこの問題を調べてみたのが本記事になります。
サンプルコード検証に用いた筆者の環境:
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ドキュメントを参照してください。
実際に ObjectMapper.enableDefaultTyping() メソッドを使ったサンプルコードを次に示します。
以上見てきたように、クラス名に続いてそのプロパティをJSONで与えることで、ある程度制限はあるものの、任意のクラスを任意のプロパティで生成することが可能となります。 これを悪用したのがCVE-2017-7525の脆弱性で、きっかけとなったのは恐らく次のレポートと思われます。
JacksonなどJavaでよく使われているserialize/deserializeライブラリについて、クラス名などの操作で任意コード実行につながる危険性がレポートされており、実際にどのようなクラスが危険か具体的なクラス名がリストアップされています。
これを受けてのものか分かりませんが、日付的には上記リポジトリの1st commitの直後に、jackson-databind で以下のIssueが立てられ、対応が始まりました。
実際にこの脆弱性を突くような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 漏れの改善ですが、まず https://github.com/FasterXML/jackson-databind/issues/1680 で s.add("com.sun.rowset.JdbcRowSetImpl"); が追加されました。
一旦それで 2.9.0 がリリースされた後、さらに https://github.com/FasterXML/jackson-databind/issues/1737 で以下のblack listチェックが追加されてます。
// [databind#1737]; JDK provided
s.add("java.util.logging.FileHandler");
s.add("java.rmi.server.UnicastRemoteObject");