Apache Groovy Deserialization: A Cunning Exploit Chain to Bypass a Patch

December 19, 2017 | Simon Zuckerbraun

This is the second in our series of Top 5 interesting cases from 2017. Each of these bugs has some element that sets them apart from the approximately 1,000 advisories released by the program this year. Today’s blog examines a remote code execution bug in Apache Groovy that bypasses a previous patch for a similar vulnerability.

In January 2017, the Zero Day Initiative (ZDI) published an advisory for Apache Groovy, ZDI-17-044/CVE-2016-6814. This vulnerability, reported to us in late 2016 by Sam Thomas of Secarma Limited, is a rather deft patch bypass for an earlier vulnerability that was also submitted via the ZDI program.

The original bug

The original bug, discovered by cpnrodzc7 and given the identifier ZDI-15-365/CVE-2015-3253, is a bug in the Groovy programming language. Groovy is implemented in Java and defines various new serializable Java classes. One of these classes, MethodClosure, is particularly problematic from a security standpoint. A MethodClosure instance holds a reference to an object and a method name. When a caller invokes doCall on the MethodClosure, the MethodClosure calls the named method on the specified object. By placing a serialized MethodClosure into a stream, an attacker can cause quite a dangerous object to be formed upon deserialization. Furthermore, it is not difficult to include additional crafted objects in the stream so that the doCall method will be invoked automatically, producing remote code execution.

It follows that it is quite hazardous for MethodClosure to be a serializable class. Even if an application has no intention of passing serialized MethodClosure instances, any attacker who is positioned to insert a serialized message can specify a MethodClosure in the stream, which will be dutifully recreated into a live MethodClosure object by the deserialization mechanism. Logically, the only remedy was to change the implementation of MethodClosure so that it is no longer a serializable type. At this point, the Groovy developers were in a pickle. Changing MethodClosure to be non-serializable would involve modifying the class hierarchy and could be too drastic a change for some existing code. They chose to take a middle path by defining a new static field, MethodClosure.ALLOW_RESOLVE. Provided it is kept in its default state of false, any attempt to deserialize a MethodClosure object will result in an exception throw:

You can view the change on GitHub here.

That ought to take care of the problem... right?

Bogus!

The researcher astutely recognized that this does not, in fact, solve the problem. The heart of the matter is this: even though an exception may be thrown during a deserialization operation, it’s not guaranteed that deserialization immediately terminates and all progress is discarded. Quite the contrary. The process of deserialization is performed in large part by code that is part of the classes being deserialized, and an attacker has wide latitude in choosing those classes. Some classes may even choose to completely ignore exceptions thrown while attempting to deserialize contained objects.

To defeat the patch, Thomas constructed the following gadget chain:

        • The hazardous MethodClosure is wrapped in a KRBError. When the
        MethodClosure throws an UnsupportedOperationException, KRBError
        catches it and instead throws an IOException.
        • The KRBError is wrapped in a BeanContextChildSupport.
        BeanContextChildSupport contains code to handle an IOException and does
        not throw any further exception. Instead, it continues deserializing additional
        child objects.
        • Further on within the same BeanContextChildSupport, there is a serialized
        AnnotationInvocationHandler. At deserialization time, this object invokes the
        MethodClosure, resulting in remote code execution. Somewhat surprisingly, even
        though the readResolve method shown above does not complete successfully, the
        deserialized MethodClosure remains available to be referenced via the handle that
        was assigned to it by the deserialization infrastructure.

The use of AnnotationInvocationHandler to produce automated effects at deserialization time is already well known from the ysoserial Java deserialization payload project.

The serialized objects within the exploit may be visualized as follows:

In this way, the exploit chain bypasses the patch by continuing execution past the exception throw and achieves automatic code execution during deserialization.

Apache patches the patch

To defend against the new attack, the developers of Groovy patched MethodClosure once more. This time, they introduced a custom readObject method for MethodClosure, so that by default MethodClosure refuses to initialize its fields from a serialized stream:

You can view the change on GitHub here.

As before, an attacker might still be able to wrap the object and suppress the exception, thereby obtaining a deserialized MethodClosure. However, the resulting MethodClosure object is harmless because its fields have not been initialized to contain the attacker’s data.

Curiously, the way the readObject method shown above is written, it throws an exception unconditionally. This breaks any legitimate uses of serialization of MethodClosure. Yet the code has remained that way for almost a year. That suggests that there are in fact no legitimate uses for serialization of MethodClosure.

Conclusion

The technique the researcher used for this patch bypass highlights the treacherous nature of deserialization vulnerabilities. The Java deserialization mechanism will create an object of any type that the input stream specifies, provided it is a serializable (or externalizable) type that can be found in the CLASSPATH. Control flow during deserialization proceeds according to the code found within the specified type. This gives an attacker an unusual amount of freedom. Sometimes even an exception throw will be insufficient to stop an exploit chain from achieving remote code execution. When accepting data that may come from an untrusted source, it is recommended to completely avoid general-purpose deserialization mechanisms.

You can find me on Twitter at @HexKitchen, and follow the team for the latest in exploit techniques and security patches. Stay tuned for the next Top 5 bug blog, which will be released tomorrow.