Return of the Rhino — Analysis of MozillaRhino gadgetchain (also the writeup of HITB linkextractor)
Last week, HITB A&D CTF was held both online & onside.
I didn’t participate in, but from my colleague, i knew that there was at least a challenge with deserialization bug.
The challenge i focused on is “linkextractor”, a challenge with the java deserialization bug!
While most solutions for this didn’t achieve RCE, it abused some bug in the source code to leak the flag.
I’m not sure if this is intended solution or not, but it worths a few lines sharing about this!
- HITB SECCONF “linkextractor” writeup
- MozillaRhino revisited, rework, renew
- Limitiation of deserialization in JDK 17+
According to the Dockerfile, the entrypoint for this challenge is a fat jar file “linkextractor.jar”, which was compiled before putting in the docker.
(I’ve added some debugging env for remote debugging)
Checking the pom.xml file, there was only “io.javalin” as the dependency
But after importing to IntellIJ, i realized that there was more libraries. These libraries were imported as bundle of “javalin-bundle”.
Back to the main source code, the web server is created with Javalin, It will take the Cookie “user” and call to parseUserCookie() at every requests:
The method “parseUserCookie”:
We can clearly see the input data is base64 decode and deserialized by calling ObjectInputStream.readObject()
But it’s not easy to trigger the bug by putting any random gadgetchain into the Base64 data.
Take a look back at the Program.main() method, there is also a call to ObjectInputFilter.Config.setSerialFilter to filter the serialized Class,
This was done by the EntitiesObjectInputFilter class:
This class override the ObjectInputFilter.checkInput(), according to above code, if the class is not null and the serialization depth is equals to 1, it will perform the check. And if the className is not started with “ctf.linkextractor.entities.”, it will reject the deserialization!
Take this as an example, there are 3 serializable fields in User.class,
While deserializing an Object of User class
- At the first time of the check, you can see that the filterInfo.depth == 1, and the className == “ctf.linkextractor.entities.User”, so it will get passed the check.
- At the second time of the check, the className == “java.util.HashSet” (the “pages” field in the User class), but the filterInfo.depth == 2, the check will be skipped and the return result is UNDECIDED
According to the code in ObjectInputStream.filterCheck(), it will throw the exception only when the Status == REJECTED
Which mean from there, we can deserialize everything with the User class as the parent of the deserialize data!
##The Trojan horse
We’ve spotted the bug but where can we wrap the real gadgetchain data?
Take a look closer at the User class, there was no field with type == Object, so we cannot directly wrap the gadgetchain into it.
But remember that we still have the field with type == HashSet
In the past, there was many gadgetchain also wrapped by the HashSet class, for example: CommonsCollections6
Let’s me explain a bit about this,
In the HashSet class, each elements are stored as a key of a HashMap, like this:
You can see that the element is put as a key of the map, with the value is just a empty Object().
The “map” field in HashSet class has transient modifier, so basically, it would not be carried during the serialize/deserialize process.
So in the HashSet writeObject/readObject, it will perform some extra step to store/restore it.
While being deserialized, the method readObject() will create a new map, which may be an instance of LinkedHashMap or HashMap. Then call to ObjectInputStream.readObject() to restore the element and cast to the original Type, it’s Page class here.
The problem is ObjectInputStream.readObject() doesn’t care about which type will be deserialized, it will deserialize whatever inside!
By abusing this “feature”, we can wrap any gadgetchain inside the User class!
At this point, we have a arbitrary deserialization bug, but a big question ahead, which gadgetchain can be used?
By inspecting the libraries, i see that the Mozilla Rhino is also included in this bundle,
There are 2 working (worked) gadgetchains for this library:
MozillaRhino1 & 2
After changing the dependency to Mozilla Rhino 220.127.116.11 (which is used in this challenge), these gadgetchains don’t work anymore.
- The problem with MozillaRhino1:
This gadgetchain abuse the use of BadAttributeValueExpException.readObject() -> toString() to trigger deserialization, but this challenge is using latest JDK (18), in new JDK BadAttributeValueExpException.readObject() doesn’t call to toString() anymore:
- The problem with MozillaRhino2
This gadgetchain doesn’t use BadAttributeValueException anymore, most part of this gadgetchain relied on the Mozilla Rhino library,
But there was some changes in new version of rhino, there’s no method accessSlot() in new version.
It can easily be fixed by using the alternative method “createSlot”, it has the same functional as the “accessSlot” method
It all looks good, but while using the payload generated from MozillaRhino2, there was a fatal exception thrown:
This gadgetchain is using the com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties() to trigger the command execution.
But in new version of JDK (from JDK 17), many changes were made to hardening the java runtime.
Above exception is one of these, a class from this module cannot access the class from the other module if that module doesn’t export to it.
Below is the code before the exception, with the method = TemplatesImpl.getOuputProperties()
In short, all gadgetchain relied on the TemplatesImpl will not work on new JDK.
And this cannot be bypassed, we must use another way to achieve code execution, without the usage of TemplatesImpl.
To do this, we need to understand the MozillaRhino gadgetchain deeper!
Luckily, the MozillaRhino1 was analyzed very detailed here (unluckily, i found this after finishing the chain :( )
It can be visualized like this:
Both of these gadgetchain’s main idea are the usage of ScriptableObject.getProperty()
This method will call to obj.get() AKA Scriptable.get(),
Then all of them will lead to Slot.getValue()
Here is an extend method of Slot.getValue(), GetterSlot.getValue():
There are 2 remarkable branches, if the “getter” is an instance of MemberBox, it will directly invoke this “getter” method.
Both branches are necessary for the gadgetchain,
At first branch, MemberBox is a Serializable class
Because the Method is not serializable, while being deserialized, readObject() reconstruct the Method by using the readMember()
While GetterSlot.getValue() call to MemberBox.invoke()
The wrapped getter method will be invoked, as in the MozillaRhino1, it invoked the method Context.enter() first.
This step is necessary because the trigger point is in the second branch, at this place, if the Context == null, it will abort the chains.
In the second branch, getter will be an instance of Function, in MozillaRhino1, getter is an instance of NativeJavaMethod.
It’s quite flexible here, the NativeJavaMethod.call() allow us to directly call to any java method.
In MozillaRhino1, it will call TemplatesImpl.newTransformer() to get code executing.
But our problem is not able to access TemplatesImpl class,
There are two way to achieve code execution here:
- Find a method with no argument to trigger RCE, with the parent Class is serializable
- Find another suspicious Function.call() method
I’ve successful achieved code execution by using the second way!
There’re many implementation of method Function.call(), after looking around, i focused on this one: NativeScript
NativeScript.call() make a call to Script.exec()
We’ve a working example like this:
The Script created by Context.compileString() will be defined as a new class, so it can’t be used with NativeScript for serialization.
Lucky for me, there was another implementation of Script, that didn’t define as new class: InterpretedFunction
It can be constructed by specific a compiler while calling to Context.compileString()
As the result, we have a serializable Script:
Craft everything into a new gadgetchain, we will have the source code look like this:
After generating the payload, it will cause an exception:
The main reason for this is the parentScope of NativeScript (which is Environment for now) doesn’t contain the “java” package in the top level packages.
In the previous example, the Script gets executed because its parent scope is init by Context.initStandardObjects()
Context.initStandardObjects() will add others top level packages like java, javax, org, … to the scope.
That explained why the code got executed in the example!
There is an idea to set the parent scope of NativeScript in the gadgetchain by using Context.initStandardObjects().
But after generating gadgetchain, the payload size is too large, about ~20kb for just the Scope.
That happened because while using Context.initStandardObjects(), many other unnecessary top packages are also added to the Scope => made the size grow up.
This can be solved by manually adding the top level package into the scope, like this:
(Don’t be lazy ¯\_(ツ)_/¯)
The generated payload size is just about ~5kb, small enough to send via HTTP header
That’s all for now,
I’ve added new gadgetchain — MozillaRhino3, which support all Rhino version from 1.7R2 to 1.7.13,
You can check it out here:
Add MozillaRhino3 supported version 1.7R2 -> 1.7.13 by testanull · Pull Request #192 ·…
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes…
The payload generator of linkextractor:
Thanks for reading!!