Demystify a Java gadget chain to exploit insecure deserialization
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.
We need to use property oriented programming
to build a RCE gadget
from the scratch.
Component / Classes used to build the gadget
- Default JRE System Libraries: HashMap
commons-collections-3.2.2.jar
:- ChainedTransformer
- LazyMap
- TiedMapEntry
- HashBag
commons-lang3-3.7.jar
mockito-all-1.9.5.jar
Make sure to add all those external libraries into Java class PATH (i.e Pycharm File -> Project Structure -> Libraries)
Pieces of the Puzzle
Command Execution using Runtime
object directly
We can use the Java Runtime
object and its exec()
method to execute any system
commands.
- for example, running the
mate-calculator
in linux.
Command Execution using Reflection API and Runtime
object
- Java
Reflection API
is used toexamine
ormodify
thebehavior
of methods, classes, interfaces atruntime
. - Through reflection API, we can invoke any method at
runtime
viainvoke()
function.- Here, we are trying to invoke
getRuntime()
method to get aRuntime
object.
- Here, we are trying to invoke
Before directly jump into the Constant Transformer
and InvokerTransformer
, first understand the Transformer
class and the transform()
method.
Concept of Transformer
- It transforms an input object to an output object through
transform()
method. - It doesn’t change the input object.
- It is mainly used for:
type conversion
,extracting
parts of an object.
For example, we can create a class MyReverse
by implementing the Transformer
interface and transform()
method.
- Here, in
transform()
method, we specify how to reverse aString
type object.
When we call the transform()
method via passing the argument of a String
type object, it reverses the string.
The return type of the transform()
method is Object
therefore it can return any type of object.
Command Execution using ConstantTransformer
In contrast to the Transformer
class, it always returns the same object
that specified during initialization
.
- If we Initialize a
ConstantTransformar
withRuntime.class
and can call thetransform()
method withany object
(for example,HashSet
), we will always get theRuntime.class
type object.
Concept of InvokerTransformer
- During initialization, it takes a
method name
with optional parameters. - On
transform
, it calls that method for the object provided with the parameters.
Command Execution combining Transformer, ConstantTransformer and InvokerTransformer
We can chain all three types of transformers and perform RCE.
Command Execution using ChainedTransformer
ChainedTransformer
is an array of transformers. By carefully maintaining their execution order we can achieve the same result but with minimum amount of code.
Concept HashMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
- HashMap class contains
values
based on thekey
. - It contains only unique
keys
. - It maintains no order.
- It returns
null
if there’sno
value is present for the requestedkey
.
Concept LazyMap
- A type of
Map
whichcreates
avalue
if there’sno
value is present for therequested
key. - This
generation
is done through atransformation
(i.e transformer.transform()
method) on the requestedKey
. - When the request key is not found then
lazyMap.get("invalid_key")
callstransformer.transform("invalid_key")
to generate the key on the fly.
Command Execution by combining ChainedTransformer and LazyMap
Concept TiedMapEntry
- This can be used to
enable
aMap
entry tomake changes
on theunderlying
map. - Key point to remember
tiedmapentry.hashcode()
method callstiedmapentry.getValue()
method then intern it finally callslazymap.get(this.key)
method.
Concept HashBag
- A Collection that
counts
the number of times an objectappears
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 underlyingHashMap
table entry. - Key Point to remember
hashbag.add(tiedmapentry)
method callstiedmapentry.hashcode()
method.
Assembling the pieces
- Create a
TiedMapEntry
with a underlyinglazyMap
and key isString
-> ‘invalid_key’. - Then
add
thetiedmapentry
Object into aHashBag
instance. hashbag.add(tiedmapentry)
->tiedmapentry.hashcode()
->lazymap.get(this.key)
->chainedtransformer.transform(key)
->Runtime.getRuntime().exec("/usr/bin/gnome-calculator");
Challenges
- Our primary objective for during the deserialization process is to ensure that
TiedMapEntry.hashcode()
gets invoked first time only. - However, when we attempt to serialize the HashBag object to generate the payload or .ser file, the method lazymap.get(“invalid_key”) is invoked once. This causes the underlying HashMap to be updated with the entry
invalid_key:derived_value
. - As a result, during the deserialization process, when
TiedMapEntry.hashcode()
triggers the calllazymap.get(this.key)
, theChainedTransformer.transform(key)
method will not be executed. This is because the LazyMap no longer needs to derive the value for the key using thetransform()
method—the underlying HashMap already contains the precomputed entry.
Therefore, we need to find a way to prevent the
TiedMapEntry.hashcode()
method from being invoked while creating the exploit payload.
Resolve using mockito
- Create a
HashBag
instance and add anyObject
into it. - This will invoke Object’s
hashcode()
method and based on the hashcode / index, the underlyingHashBag's
=>HashMap
table entry will be updated withkey = Object
andvalue / count = 1
. - Now using
mokito
library, modify thatHashBag's
=>HashMap’s
first
entry inmemory
.- Replace that
Object
withTiedMapEntry
Object. - We have added only one entry due to that we are modifying the
first
entry.
- Replace that
- As you can observe, till this point,
TiedMapEntry.hashcode()
is not called anywhere. - Serialize this
HashBag
Object.
An exception might still occur while creating the serialized object, as serialization support is now disabled by default for org.apache.commons.collections.functors.InvokerTransformer
.
Therefore enable the support in both producer and consumer programs to serialize / deserialize the object without any exception
1
2
3
4
System.setProperty(
"org.apache.commons.collections.enableUnsafeSerialization",
"true");
Prepare Final Exploit
Before finalizing the exploit payload, we need to include support for the Mockito library.
Java’s strong encapsulation introduced in Java 9+, which restricts reflective access to certain internal Java classes and fields by default. This is especially relevant when using libraries or tools that attempt to access private or internal fields of classes like
HashMap
.
- By adding the
--add-opens
option, we can explicitly open the necessary package (java.util
) for reflection. In IntelliJ IDEA -> Run -> Edit Configurations -> In VM options field, add the following
1
--add-opens java.base/java.util=ALL-UNNAMED
It was observed that the calculator did not open during object creation because TiedMapEntry.hashcode()
was not invoked at any point.
Test Exploit
When the client deserialization code attempts to process the .ser file, the calculator opens.