Overview

In this article we discuss a recent deserialization vulnerability we found in Relution (CVE-2023-48178), a mobile device management product that is popular among multinational German corporations. CVE-2023-48178 can potentially lead to remote code execution and complete compromise of the MDM application and clients managed by the solution. The deserialization vulnerability exists in a component of the application used for inter-cluster communication within multi-cluster deployments.

The underlying vulnerability itself is rather straightforward, but our docker image deployment ran on Java 17. We ran into some unique challenges when trying to exploit the vulnerability due to the new Java 17 (and Java 16) restrictions on using reflection to access internal classes within the Java Development Kit (JDK). Unfortunately, this includes classes like the TemplatesImpl class that is commonly leveraged in many gadget chains to achieve remote code execution. Ultimately, the new Java 17 restrictions partially limited exploitation options with the currently available public gadget chains.

Relution Architecture Overview

The Relution MDM application architecture is pretty straightforward, with two primary components (see Figure A). The first component is the client applications that Relution manages. Relution supports managing various devices, from mobile devices running Android and iOS to laptop and desktop computers running Microsoft Windows, macOS, and Google Chromebooks.

The second component is the management layer implemented by Relution. The application leverages a monolithic architecture built primarily on Java Spring with a RESTful interface with the JGroups library leveraged for inter-cluster communication. The application also uses multiple backend databases, including both relational and NoSQL database backends. Relution may also communicate with other backend services, such as LDAP for authenticating end-users or an external mail-server for sending emails.

Figure 1: A diagram taken from the official Relution documentation outlines the architecture of the application when deployed on-premises.

Figure 1: A diagram taken from the official Relution documentation outlines the architecture of the application when deployed on-premises.

Performing a Bottom-Up Code Review

We conducted code review with a bottom-up methodology during the Relution analysis. We first audited the application code for dangerous sink functions such as ObjectInputStream.readObject or runtime.Exec. After identifying a sink function, we attempted to map the inputs to that sink to attacker-controlled data sources. The tracing process can involve static application security testing tools to perform automated code analysis, but it can also include simple tools like grep and other command-line text-searching utilities. Another technique is using dependency analysis tools to find dependencies containing known security vulnerabilities and then identifying the sources that invoke the vulnerable functionality.

Identifying the Deserialization Vulnerability

The deserialization vulnerability exists in the component used for inter-cluster communication between instances of the Relution application. The component uses the JGroups library and is enabled by default. According to the Relution documentation, the cluster communication uses port 7800 and “need to be opened on the firewall for incoming and outgoing connections from/to the Internet”. (Side note: while the documentation states that port 7800 needs to be accessible from the internet, our scans indicated that none were actually exposed. The exploitation vector required is a local network connection where port 7800 is accessible, like from an internal network.)

We identified a ObjectInputStream.readObject function call during our source review where the “handle” function passes a message object to the readObject function. After further source code review we determined that the handler (shown in Figure 2) was invoked as part of a JGroups listener service (see Figure 3).  It seemed likely at this point that the message object could contain attacker-controlled user input.

Figure 2: A bottom-up analysis of the decompiled source code for the Relution application yielded a function ClusterService.handle which appeared to deserialize an input message potentially containing attacker-controlled data.

Figure 2: A bottom-up analysis of the decompiled source code for the Relution application yielded a function ClusterService.handle which appeared to deserialize an input message potentially containing attacker-controlled data.

Figure 3: The constructor for the ClusterService class uses the JGroups library to create a listener with a RequestHandler set to the ClusterService.handle function.

Figure 3: The constructor for the ClusterService class uses the JGroups library to create a listener with a RequestHandler set to the ClusterService.handle function.

We investigated further to determine if the JGroups ClusterService service was enabled by default or if certain settings are required for it to be enabled. Our initial analysis suggested that the ClusterService would be enabled regardless of the configuration leveraged by the end-user. We did observe that if cluster support is disabled in the application configuration file a log warning message is printed, but the service is still started (see Figure 4).

Figure 4: Our review of the application source code indicated that the cluster service was still activated even when cluster support was disabled within the application.

Figure 4: Our review of the application source code indicated that the cluster service was still activated even when cluster support was disabled within the application.

Java 17 and an Initial Exploitation Attempt

After some reverse engineering of the JGroups packet structure and protocol we were able to construct a JGroups BytesMessage that would trigger the handle function. The submitted message would then be deserialized by the application. We quickly identified that the CommonBeanutils1 gadget chain existed within the class path for the application and generated a ysoserial payload that would provide us with a reverse shell.

Unfortunately, after sending the ysoserial generated payload to the application we observed an error message stating that it “cannot access com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl (in module java.xml) because module java.xml does not export com.sun.org.apache.xalan.internal.xsltc.trax”. This error results from the usage of Java 17 which is required as of version 5.18.0 of Relution published on June 9th, 2023 (see Figure 5).

Figure 5: Our analysis of the changelog published by Relution indicated that versions below Java 17 were deprecated in version 5.18.0 published on June 6th, 2023.

Figure 5: Our analysis of the changelog published by Relution indicated that versions below Java 17 were deprecated in version 5.18.0 published on June 6th, 2023.

We quickly reviewed the new Java 17 restrictions and learned that the CommonBeanutils1 gadget chain from ysoserial does not work anymore (by default). Our deserialization attempt failed and we were not able to run the exploit. We decided to dig deeper into the root cause.

There are several blog posts that outline the recent changes to Java 17 (and Java 16) which we found helpful when performing our research into the new exploit mitigations. Oracle published an excellent article titled A Peek into Java 17: Encapsulating the Java Runtime Internals. The primary change in Java 17 revolves around restricting access to internal classes within the Java runtime through reflection when the module does not explicitly authorize external access to those classes through the module’s definition.

Hans-Martin Münch published an excellent article titled Look Mama, no TemplatesImpl which describes his attempt to use TemplatesImpl within com.sun.org.apache.xalan.internal.xsltc.trax with the CommonBeanutils1 gadget chain on Java 17. He provides useful information including the same error message we observed when attempting to use the CommonBeanutils1 gadget chain with TemplatesImpl. He outlines a bypass by showing how to modify the payload to invoke the getConnection function with a malicious JDBC connection string in third party drivers like H2 or PostgreSQL.

The technique of leveraging third-party code in JDBC drivers bypasses the restrictions implemented in Java 17 (and Java 16) by targeting “getters” within dependencies leveraged by the application. We were unable to identify a viable vector for achieving remote code execution using the JDBC PostgreSQL driver (and the H2 JDBC driver was not shipped with the Relution application). We performed a manual code review of the MSSQL JDBC driver shipped with Relution looking for primitives we could leverage for remote code execution through the getConnection function without success.

Fortunately, while we were unable to identify a remote code execution primitive using JDBC drivers, we now had a much better understanding of how the CommonsBeanutils1 gadget chain was used alongside TemplatesImpl to achieve remote code execution. We also confirmed we could still use the CommonsBeanutils1 gadget chain to invoke arbitrary getters.

Understanding the CommonsBeanutils1 Gadget Chain

As mentioned previously, the CommonsBeanutils1 gadget chain allows an attacker to invoke arbitrary “getters” on a serializable class object. This is accomplished by leveraging a serialized PriorityQueue containing an attacker-controlled serialized version of the BeanComparator class with a customized property value named “property”. However, during our research we were unable to obtain a clear explanation as to how this primitive which allowed us to invoke arbitrary “getters” actually worked. Because we were unable to find a satisfactory explanation online, we decided to include a section in this article detailing how this mechanism works.

To begin, let’s examine the source code of the CommonsBeanutils1 ysoserial module to gain a better understanding of how the payload is constructed. Figure 6 shows the code used to construct a CommonsBeanutils1 payload using TemplatesImpl by invoking getOutputProperties. First, we observe on line 20 that the payload generator invokes “Gadgets.createTemplatesImpl” with the “command” argument passed to the “getObject” function. On line 22 a new BeanComparator class is created and a PriorityQueue class is created on line 25. The queue is populated with some filler values on line 27 and line 28. On line 31, the comparator we created previously has the “property” field set to “outputProperties” which results in getOutputProperties being invoked whenever the comparator.Compare() function is invoked. On line 34 – 36 the queue members mentioned previously are overwritten with the generated TemplatesImpl object. The priority queue object is then returned and serialized into a payload to be leveraged against the victim system.

Figure 6: The source code of the CommonsBeanutils1 class within YSOSerial which is responsible for generating a CommonsBeanutils1 payload which runs an attacker-controlled command using the TemplatesImpl class.

Figure 6: The source code of the CommonsBeanutils1 class within YSOSerial which is responsible for generating a CommonsBeanutils1 payload which runs an attacker-controlled command using the TemplatesImpl class.

For additional context, the “Gadgets.createTemplatesImpl” function returns a TemplatesImpl object with compiled bytecode which runs the command specified within the “command” variable using the “java.lang.Runtime.getRuntime.exec() function (see Figure 7). This bytecode is compiled and executed when the getOutputProperties function is invoked within the deserialization chain. Since this section is focused on the CommonsBeanutils1 gadget chain we won’t dive into the internal implementation of TemplatesImpl to discuss the mechanisms by which an attacker-controlled bytecode is executed when getOutputProperties is invoked.

Figure 7: The createTemplatesImpl class which compiles an attacker-controlled class which runs a command using the Runtime.Exec() function within Java by setting the _bytecodes field within the TemplatesImpl class using Reflection.

Figure 7: The createTemplatesImpl class which compiles an attacker-controlled class which runs a command using the Runtime.Exec() function within Java by setting the _bytecodes field within the TemplatesImpl class using Reflection.

Now that we understand how the payload is built, let’s take a brief look at the execution chain when the payload is deserialized. When the payload is deserialized the PriorityQueue.readObject function is invoked (see Figure 8). This function begins by reading the properties of the Priority queue and storing them into the queue class member variable. The function then ends by calling the heapify function.

Figure 8: The PriorityQueue.readObject function works by reading the serialized PriorityQueue members and then running the heapify function to construct the heap structure used to represent the PriorityQueue.

Figure 8: The PriorityQueue.readObject function works by reading the serialized PriorityQueue members and then running the heapify function to construct the heap structure used to represent the PriorityQueue.

The heapify function is responsible for building the heap structure used by the priority queue (see Figure 9). If a custom comparator is used the heapify function will invoke siftDownUsingComparator which then invokes the compare function on the custom BeanCompartor we specified in our deserialization payload (see Figure 10).

Figure 9: The heapify function invokes siftDownUsingComparator when a custom comparator is configured on the PriorityQueue object.

Figure 9: The heapify function invokes siftDownUsingComparator when a custom comparator is configured on the PriorityQueue object.

Figure 10: The siftDownUsingComparator function invokes the compare method on our custom BeanComparator object we configured on the PriorityQueue object during payload generation.

Figure 10: The siftDownUsingComparator function invokes the compare method on our custom BeanComparator object we configured on the PriorityQueue object during payload generation.

The question now is – “What happens within the compare function of BeanComparator?”. We can examine the source code of the compare function in BeanComparator to answer this question. We observe that the property value that we set previously in our payload generator is used in a call to “PropertyUtils.getProperty”. Ultimately, since we set the property value to “outputProperties” the call to “PropertyUtils.getProperty” ultimately translates this into a call to “getOutputProperties”. The “compare” function then leverages reflection to invoke that function within the deserialized TemplatesImpl object we specified as a member of our PriorityQueue (see Figure 11). At this point, the call to “getOutputProperties” then triggers the execution of the attacker-controlled bytecode we specified within the serialized TemplatesImpl object. This ultimately results in a call to the Runtime.Exec() function which runs an attacker-controlled command on the victim system.

Figure 11: The implementation of the compare function within the BeanComparator class in the Commons Beanutils library.

Figure 11: The implementation of the compare function within the BeanComparator class in the Commons Beanutils library.

At this point, we now understand how the CommonsBeanutils1 library can be leveraged within a gadget chain to invoke arbitrary “getters” within serializable Java classes. To summarize the process documented previously this involves:

  1. An attacker creates a PriorityQueue object with a custom BeanComparator. The custom BeanComparator is configured to read a specific property by invoking a “getter” using reflection to compare objects within the PriorityQueue using the custom comparator.
  2. An attacker-controlled serialized object corresponding to the class with the “getter” the attacker wants to invoke is placed within the PriorityQueue object twice.
  3. When the PriorityQueue object is deserialized the “readObject” function is invoked which results in the custom comparator being invoked on the objects within the queue in order to construct the heap structure used internally by the PriorityQueue at runtime.

The interesting thing about this gadget chain is that on Java 16 and Java 17 everything works until we hit the point where the “getter” on TemplateImpl is invoked as this is done using reflection and access to non-exported classes within internal Java JDK modules is restricted in these versions of Java. We also noticed that on Java 17, the “–add-opens” JDK command line flag can be added to allow access to the internal Java classes. The two Github issues on the ysoserial project, 176 and 203, explain how to generate and run a ysoserial payload on JDK 17. We have noticed that occasionally the software startup scripts wrapping the JDK options will contain the “–add-opens” flag. If the wrapper script happens to allow the class required for the gadget chain, the chain might still work on JDK 17.

Library-Level Hardening: Exploitation Using InvokerTransformer

At this point, we also observed that the Relution application shipped with Apache Commons 4 Version 4.4 (commons-collections4-4.4.jar). We attempted to use common deserialization payloads for Apache Commons 4 version 4.0 such as the CommonsCollections2 payload unsuccessfully.

We observed an error message indicating that serialization support for InvokerTransformer was disabled by default in this version of Apache Commons Collections (see Figure 12). We performed some analysis and learned that new versions of Apache Commons Collections 4 include restrictions that by default prevent the deserialization of InvokerTransformer objects.

Figure 12: We received an error message indicating that serialization of the InvokerTransformer object was disabled within the Apache Commons Collections library by default as a mechanism to break common deserialization attack chains.

Figure 12: We received an error message indicating that serialization of the InvokerTransformer object was disabled within the Apache Commons Collections library by default as a mechanism to break common deserialization attack chains.

However, assuming that enableUnsafeSerialization was enabled or an older version of Apache Commons Collections was leveraged we did discover that the InvokerTransformer object would have allowed us to achieve remote code execution without using TemplatesImpl or any internal Java runtime classes. This would have been possible as InvokerTransformer can be used to invoke the Runtime.Exec function with an arbitrary shell command (see Figure 13). In this case, Java 16 and Java 17 would permit access to the Runtime.Exec function as the java.base module definition exports the java.lang.Runtime class (see Figure 14). For the curious, HackTricks has an excellent article titled CommonsCollection1 Payload – Java Transformers to Runtime exec() and Thread Sleep which describes how the CommonsCollection1 deserialization gadget chain works.

Figure 13: The InvokerTransformer object can be leveraged to invoke the Runtime.Exec function without leveraging internal non-exported Java classes such as TemplatesImpl.

Figure 13: The InvokerTransformer object can be leveraged to invoke the Runtime.Exec function without leveraging internal non-exported Java classes such as TemplatesImpl.

Figure 14: The java.base module exports the java.lang.runtime class within its module definition file.

Figure 14: The java.base module exports the java.lang.runtime class within its module definition file.

Hunting for Custom Deserialization Gadget Chains

At this point, we decided to hunt for custom gadget chains that we could leverage using the ability to invoke arbitrary “getters” within serializable Java objects (with the exception of internal Java JDK classes). We identified some interesting prior research by Ian Haken called Automated Discovery of Deserialization Gadget Chains. As part of his presentation, Ian also released an open source tool called gadgetinspector which automates the process of identifying custom gadget chains within an application.

Hugo Vincent has also published several articles on finding custom deserialization gadget chains titled Finding Gadgets Like It’s 2015: Part 1, Finding Gadgets Like It’s 2015: Part 2, and Finding Gadgets Like It’s 2022 that we found useful when performing our research into custom gadget chain identification. At this point, we identified a gadget chain using gadgetinspector which allowed for an arbitrary file write on the system. However, we later realized that this corresponds to the existing AspectJWeaver gadget chain within ysoserial.

Understanding the AspectJWeaver Deserialization Chain

As mentioned previously, we quickly determined that the output from gadgetinspector corresponded to an existing gadget chain within ysoserial called AspectJWeaver. This deserialization gadget chain uses a combination of gadget chains from the AspectJ library (e.g. SimpleCache and StorableCachingMap), Apache Common Collections 4 (e.g. TiedMapEntry and LazyMap), and internal serializable Java runtime classes (e.g., HashSet and HashMap). Figure 15 shows the deserialization gadget chain used in the AspectJWeaver payload generator.

Figure 15: The deserialization gadget chain used by the AspectJWeaver payload generator in ysoserial.

 

Figure 15: The deserialization gadget chain used by the AspectJWeaver payload generator in ysoserial.

To further our understanding of Java deserialization gadget chains, let’s dig into the particulars of how this chain works. We will start at the very end when StorableCachingMap.writeToPath is invoked and trace back to the original call to HashSet.readObject, which kicks off the entire deserialization gadget chain.

StorableCachingMap is the first class used within the gadget chain. StorableCachingMap is an internal class within the SimpleCache class within the AspectJ library. StorableCachingMap has a function called writeToPath which takes an input key and a byte array. It uses the input key to construct a file path of a location to write to disk. It then writes the data specified within the bytes argument to the location specified using the previously mentioned key variable (see Figure 16 and Figure 17).

Figure 16: The StorableCachingMap class is a private internal class within the SimpleCache class.

Figure 16: The StorableCachingMap class is a private internal class within the SimpleCache class.

Figure 17: The writeToPath function takes a key which is used in the construction of an output file path and an array of bytes written to the target file path.

Figure 17: The writeToPath function takes a key which is used in the construction of an output file path and an array of bytes written to the target file path.

If we then traverse up one more level in the gadget chain we see that StorableCachingMap.put invokes the writeToPath function assuming a conditional that the value byte array doesn’t match the value in the SAME_BYTES variable which has a value of “IDEM” (see Figure 18). If we can find a way to invoke StorableCachingMap.put with an attacker controlled key and value object this should result in an arbitrary write.

Figure 18: The StorableCachingMap.put function invokes the writeToPath function using a key and array of bytes passed to the put function as arguments.

 

Figure 18: The StorableCachingMap.put function invokes the writeToPath function using a key and array of bytes passed to the put function as arguments.

Next, let’s look at how the gadget chain invokes StorableCachingMap.put using the LazyMap.get function. This is quite interesting as the get function in LazyMap first checks if its internal map field, in this case a StorableCachingMap object, does not contain an entry corresponding to the key argument passed to the function (see Figure 19). If the map doesn’t contain this entry it adds it to it’s internal map entry by first deriving a corresponding value for the input key using a local class member variable called factory of type Transformer by invoking the factory.transform function (see Figure 20).

Figure 19: The LazyMap.get function takes an input key and runs a transform to derive a value from the key. It then invokes map.put with the passed key and derived value.

Figure 19: The LazyMap.get function takes an input key and runs a transform to derive a value from the key. It then invokes map.put with the passed key and derived value.

Figure 20: The LazyMap function uses a Transformer data type to derive a value from a key through the factory member field.

Figure 20: The LazyMap function uses a Transformer data type to derive a value from a key through the factory member field.

In Apache Commons Collections, a Transformer is an interface which provides a framework for transforming elements within collections. It provides a single method called transform which is responsible for applying a specific transformation to a given input element. Transformers allow developers to create reusable customizable transformation rules.

One type of transformer that exists in the Apache Commons Collections library is the ConstantTransformer which implements the Transformer interface. This transformer always returns a constant value regardless of the input passed to the transformer. Since we control the factory element in our payload we can set the “factory” field to an object of type ConstantTransformer, which always returns a specific attacker-controlled value (see Figure 21). This would get us half of the way there as we can now control the value passed to StorableCachingMap.put using our ConstantTransformer, but we still need to figure out how we can gain control of the key value passed to the LazyMap.get function.

Figure 21: The ConstantTransformer class implements the Transformer interface and always returns a constant attacker-controlled value regardless of the input key.

Figure 21: The ConstantTransformer class implements the Transformer interface and always returns a constant attacker-controlled value regardless of the input key.

If we traverse yet another level higher in the deserialization gadget chain we observe that LazyMap.get is invoked by TiedMapEntry.getValue. We observe that TiedMapEntry.getValue simply invokes the “get” function on the “map” variable while passing a “key” value (see Figure 22).

Figure 22: The TiedMapEntry.getValue function leverages the internal map field and invokes the get function on that class member using another internal member called key as an argument.

Figure 22: The TiedMapEntry.getValue function leverages the internal map field and invokes the get function on that class member using another internal member called key as an argument.

If we dig into the source of these variables, we observe that these are both internal class members of the TiedMapEntry class (see Figure 23). As such, both of these values are attacker-controlled variables. We also observe that TiedMapEntry.hashCode invokes TiedMapEntry.getValue (see Figure 24). At this point, we have full control of both the key and the value that will be passed to StorableCachingMap.writeToPath. However, we need to identify a mechanism to invoke TiedMapEnty.hashCode within our gadget chain to trigger this entire process.

Figure 23: We confirmed that the map and key values are both local class members within the TiedMapEntry class by examining the constructor for the class.

Figure 23: We confirmed that the map and key values are both local class members within the TiedMapEntry class by examining the constructor for the class.

Figure 24: The first step performed by the LazyMap.hashCode function involves invoking TiedMapEntry.hashCode.

Figure 24: The first step performed by the LazyMap.hashCode function involves invoking TiedMapEntry.hashCode.

If we traverse one more level up the gadget chain we observe that the HashMap function’s hash function will invoke key.hashCode on an input key object passed to the hash function (see Figure 25). We also observe that the call to the HashMap.hash function is triggered by a call to HashMap.put with a given input key and value object (see Figure 26).

Figure 25: The HashMap.hash function uses key.hashCode to derive a hash value from the input key object.

Figure 25: The HashMap.hash function uses key.hashCode to derive a hash value from the input key object.

Figure 26: To add a new entry to a HashMap the “put” function must derive a hash from the input key object by invoking the hash function which is just a wrapper around key.hashCode.

Figure 26: To add a new entry to a HashMap the “put” function must derive a hash from the input key object by invoking the hash function which is just a wrapper around key.hashCode.

We have now reached the initial trigger point for the deserialization gadget chain which is HashSet.readObject. When an object of type HashSet is deserialized the HashSet.readObject function is invoked by the deserialization code within Java (see Figure 27). It begins by initializing an object of type HashMap and assigning it to an internal class member called map (see Figure 28).

Figure 27: The HashSet.readObject function instantiates a new HashMap and then invokes HashMap.put on the newly instantiated HashMap to initialize it using the member values specified within the serialized object.

Figure 27: The HashSet.readObject function instantiates a new HashMap and then invokes HashMap.put on the newly instantiated HashMap to initialize it using the member values specified within the serialized object.

Figure 28: The HashSet class is implemented by leveraging an internal HashMap object to store the values inserted into the HashSet.

Figure 28: The HashSet class is implemented by leveraging an internal HashMap object to store the values inserted into the HashSet.

Next, HashSet.readObject loops through and reads all of the member objects that exist within the HashSet and deserializes them through a call to the readObject function on an input ObjectInputStream object while invoking the map.put function to add those deserialized member variables to the newly created HashMap object.

We can now conclude that it is possible to trigger a call to TiedMapEntry.hashCode by creating a serialized HashSet object containing a single malicious TiedMapEntry object as an element in the HashSet. When map.put is called TiedMapEntry.hashCode will be invoked as part of adding that object to the internal HashMap used by the HashSet object to store member variables. This mechanism now provides us with a complete gadget chain leading to arbitrary file write with attacker-controlled data on the Relution server.

At this point, we have a pretty clear understanding of how the AspectJWeaver deserialization gadget chain works. To summarize below, the following steps occur during the deserialization of our payload to achieve an arbitrary file write:

  1. The HashSet.readObject function is invoked upon payload deserialization, instantiating a new HashMap object. The readObject function then deserializes the members of the HashSet object and adds them to the newly instantiated HashMap object using the HashMap.put() function.
  2. During the execution of the HashSet.readObject function our attacker-controlled TiedMapEntry object, an entry within the serialized HashSet object, is deserialized and passed to the HashMap.put function.
  3. The HashMap.put function needs to calculate the passed object’s hash to add the entry to the HashMap object. As a result, the HashMap.put function invokes the hashCode function on our TiedMapEntry object.
  4. Within TiedMapEntry.hashCode, the function begins by invoking an internal function getValue, which invokes the “get” function on a class member called “map” of abstract type “Map”. In this case, “map” is an attacker-controlled LazyMap object. LazyMap.get is passed an attacker-controlled string key, also a class member of TiedMapEntry. The key member is essentially the file path we want to write to on disk.
  5. The LazyMap.get function first checks if its internal map structure does not contain the key value mentioned previously. If the key does not exist, it invokes the “transform” function on an internal class member called “factory” of type “Transformer,” which is also an attacker-controlled value. In this case, the map is of type SimpleCache$StoreableCachingMap, and the attacker controls the values within this map so they can ensure this check always passes. Since the attacker also controls the “factory” field in “LazyMap” they can leverage an object of type “ConstantTransformer” which always returns a fixed value for any key specified in the invoked “transform” function. This provides the attacker with full control of the return value when the “transform” function is invoked on the factory object. After invoking “factory.transform”, the LazyMap.get function then invokes “put” on the “map” member field of type “StoreableCachingMap”. The arguments passed in are the key passed to “LazyMap.get” along with the values returned from the call to the transform function.
  6. Internally “StoreableCachingMap.put” invokes “StoreableCachingMap.writeToPath” using the passed key and value. It uses the key value to construct a path to write to on disk and leverages the passed value as the contents to write to the file on disk.

This gadget chain is quite unorthodox as four-levels of serialized map objects: a HashMap, TiedMapEntry, LazyMap,and StorableCachingMap, are all used within the gadget chain to perform the arbitrary write operation. The gadget chain also uses gadgets from two separate libraries, AspectJ and Apache Commons Collections, along with the exported Java JDK classes HashMap and HashSet. All the different libraries make the resulting chain quite complex, but also very useful in Java 16 and Java 17 environments.

Leveraging the Arbitrary Write for Keystroke Logging

We then used the AspectJWeaver gadget chain to overwrite the frontend code for the Relution login page with a keylogger. The keylogger was used to record the username and password leveraged by end-users to authenticate to the application (see Figure 29). At this point, we concluded that we had demonstrated an appropriate level of impact to make the issue worth reporting and decided to document the issue and move onto another research target.

Figure 29: We used the arbitrary write capability to backdoor the frontend login panel of the Relution application. The backdoor allowed us to harvest credentials for the login interface using malicious JavaScript code.

Figure 29: We used the arbitrary write capability to backdoor the frontend login panel of the Relution application. The backdoor allowed us to harvest credentials for the login interface using malicious JavaScript code.

We do believe it would also be possible to achieve remote code execution using the arbitrary write capability, but it would require more effort. Other user deployed instances that are running on older versions of Java (pre Java 16) would be vulnerable to remote code execution. Example ways to leverage the arbitrary file write to gain remote code execution would be overwriting startup scripts in “/opt/relution/bin” or writing to authorized_keys to gain SSH access to the system. Exploitation would be somewhat dependent on the privileges assigned to the Relution service account and how the application was installed. The Relution application does not use JSP files so we did not identify any scenarios where we could write a malicious JSP file to the webroot of the application to achieve remote code execution.

Investigating the Deserialization Vulnerability’s Impact

We were unable to identify any instances of Relution on the Internet where the JGroups service used for inter-cluster communication was exposed to the Internet. Because of this we believe the primary impact of this vulnerability would arise from an attacker with internal network access exploiting the issue to elevate privileges within an internal network environment.

Conclusion

In this article, we discussed how we leverage a bottom-up source code auditing methodology to identify a Java deserialization vulnerability in the Relution mobile device management (MDM) solution. MITRE assigned the vulnerability the id CVE-2023-48178. CVE-2023-48178 allowed for system compromise through a Java deserialization vulnerability in the inter-cluster communication component deployed within the system.

The interesting thing about the vulnerability we identified in this case, is that we spent significantly more time hunting down deserialization gadget chains to prove exploitability than we did actually finding the vulnerability. The modifications made to Java 17, combined with Relution properly keeping dependencies updated, especially in the case of Apache Commons Collections, did significantly hinder our ability to identify exploitable gadget chains. Thomas and I really enjoyed working on this project and reverse engineering various common gadget chains leveraged within different serialization payloads while hunting for custom gadget chains was quite fun as well.

Proactive vulnerability research allows us to identify critical security vulnerabilities in applications within a client’s attack surface before an attacker has a chance to exploit them. Chariot, by monitoring and categorizing external assets, helps us identify applications that warrant further review by our team.