はじめに
前回はApache Commons Collectionsライブラリの脆弱性について、問題の詳細と攻撃の仕組みを解説した。ポイントを整理しておく。
-
Commons Collectionsライブラリには、Gadget Chainが生成可能でシリアライズ可能なクラスが存在する。攻撃者は前記クラスおよびデシリアライズ時に前記クラスを利用しているクラスを悪用し、細工したオブジェクトをデシリアライズさせることで、任意のコードを実行することができる。同様の問題がすでにSpringやGroovyでも見つかっており、他にも何らかのライブラリなどで上記条件を満たすクラスが存在する場合は、新しい攻撃手法が公開される可能性も考えられる。
- 本問題は、本質的にはライブラリの利用者側がデシリアライズ処理をセキュアに実装していないことに起因するため、ライブラリの修正プログラムを適用するだけでは、根本的な対処とならない。
上記を踏まえ、今回はより一般的なJavaのデシリアライズの問題について解説する。
シリアライズ・デシリアライズの詳細
前回は概要について解説したが、デシリアライズの問題と対策への理解を深めるために、シリアライズ・デシリアライズの詳細について、もう少し踏み込んで解説する。
シリアライズ
ObjectOutputStream.writeObject()を実行すると、以下の流れでシリアライズが行われる。
GoodClass2クラスのオブジェクトを参照しているGoodClass1クラスのオブジェクトをシリアライズする場合を例に挙げて説明する。
① クラス名の書き込み完了後、ObjectOutputStream.annotateClass()が実行される。デフォルトの動作では何も処理を行わないが、サブクラスでオーバーライドすることで、クラス情報の書き込みをカスタマイズできる。
② オブジェクトの情報(フィールド)が書き込まれる。シリアライズ対象クラスでwriteObjectメソッドが実装されている場合は、当該メソッドが実行される。
③ シリアライズ対象オブジェクトが参照しているオブジェクトについて、上記①②が実行される。デフォルトの動作では、transient/staticを除く全てのフィールドがシリアライズ対象となる。
デシリアライズ
ObjectInputStream.readObject()を実行すると、以下の流れでデシリアライズが行われる。
① クラス名の読み出し後、ObjectInputStream.resolveClass()が実行され、該当クラスをロードする。サブクラスでオーバーライドすることで、クラスのロード処理をカスタマイズすることができる。ロードしようとするクラスのserialVersionUID(注1)とシリアライズオブジェクトが保持するserialVersionUIDの比較が行われ、異なる場合は例外が発生する。
② オブジェクトの情報(フィールド)が読みだされる。シリアライズ対象クラスでreadObjectメソッドが実装されている場合は、該当メソッドが実行される。
③ シリアライズ対象オブジェクトが参照しているオブジェクトについて上記①②が実行される。
注1
オブジェクトがどのクラスを元に復元されたのかを識別するためのID。
以下はシリアライズデータ(バイト列)の例だ。シリアライズデータ固有のバイト列や、クラス名、serialVersionUID、フィールドなどが読み取れる。
public class GoodClass1 implements Serializable { private static final long serialVersionUID = 5754104541168320730L; // 16進数では「4FDAAF97F8CCC0DA」 private GoodClass2 goodClass2 = null; // 参照オブジェクト GoodClass1(){ goodClass2 = new GoodClass2(); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); } } public class GoodClass2 implements Serializable { private static final long serialVersionUID = 5754104541168320731L; private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); } }
以上を踏まえて、デシリアライズ時に発生し得るセキュリティ問題について見ていこう。