Code White have already an impressive publication record on Java Deserialization. This post is dedicated to a vulnerability in SAP NetWeaver Java. We could reach remote code execution through the p4 protocol and the Jdk7u21 gadget with certain engines and certain versions of the SAP JVM.
We would like to emphasize the big threat unauthenticated RCE poses to a SAP NetWeaver Java. An attacker with a remote shell can read out the secure storage, access the database, create a local NetWeaver user with administrative privileges, in other words, fully compromise the host. Unfortunately, this list is far from being complete. An SAP landscape is usually a network of tightly
connected servers and services. It wouldn’t be unusual that the database of the server stores technical users with high privileges for other SAP systems, be it NetWeaver ABAP or others. Once the attacker gets hold of credentials for those users she can extend her foothold in the organization and eventually compromise the entire SAP landscape.
We tested our exploit successfully on 7.20, 7.30 and 7.40 machines, for detailed version numbers see below. When contacted, SAP Product Security Response told us they published 3 notes (see [7], [8] and [9]) about updates fixing the problems (already in June 2013) with SAP JVM versions 1.5.0_086, 1.6.0_052 and 1.7.0_009 (we tested on earlier versions, see below). In addition SAP have recently adopted JDK JEP 290 (a Java enhancement that allows to filter incoming serialized data). However, neither do these three notes mention Java Deserialization nor is it obvious to the reader they relate to security in any other way.
Due to missing access to the SAP Service Marketplace we’re unable to make any statement about the aforementioned SAP JVM versions. We could only analyze the latest available SAP JVM from tools.hana.ondemand.com (see [6]) which contained a fix for the problem.
Details
In his RuhrSec Infiltrate 2016 talk, Code White’s former employee Matthias Kaiser already talked about SAP NetWeaver Java being vulnerable [2]. The work described here is completely independent of his research.
The natural entry point in this area is the p4 protocol. We found a p4 test client on SAP Collaboration Network and sniffed the traffic. One doesn’t need to wait long until a serialized object is sent over the wire:
00000000 76 31 v1 00000002 18 23 70 23 34 4e 6f 6e 65 3a 31 32 37 2e 30 2e .#p#4Non e:127.0. 00000012 31 2e 31 3a 35 39 32 35 36 1.1:5925 6
00000000 76 31 19 23 70 23 34 4e 6f 6e 65 3a 31 30 2e 30 v1.#p#4N one:10.0 00000010 2e 31 2e 31 38 34 3a 35 30 30 30 34 .1.184:5 0004
The highlighted part is just the java.lang.String object “ClientIDPropagator”.
Now our plan was to replace this serialized object by a ysoserial payload. Therefore, we needed to
find out how the length of such a message block is encoded.
When we look at offset 0000005E, for instance, the 00 00 75 00 looks like 2 header null bytes and
then a length in little endian format. Hex 75 is 117, but the total length of the last block is 8*16+3 =
131. If one looks at the blocks the client sent before (at offset 0000001B and 0000003A) one can
easily spot that the real length of the block is always 14 more than what is actually sent. This lead to
the first conclusion: a message block consists of 2 null bytes, 2 bytes length of the payload in little
endian format, then 10 bytes of some (not understood) header information, then the payload:
When running the test client several times and by spotting the messages carefully enough one can see that the payload and header aren’t static: They use 2 4-bytes words sent in the second reply from
the server:
That was enough to set up a first small python program: Send the corresponding byte arrays in the right order, read the replies from the network, set the 4 byte words accordingly and replace “ClientIDPropagator” by the ysoserial Jdk7u21 gadget.
Unfortunately, this didn’t work out at first. A bit later we realized that SAP NetWeaver Java obviously didn’t serialize with the plain vanilla Java ObjectOutputStream but with a custom serializer. After twisting and tweaking a bit we were finally successful. Details are left to the reader ;-)
To demonstrate how dangerous this is we have published a disarmed exploit on github [5]. Instead of using a payload that writes a simple file to the current directory (e.g. cw98653.txt with contents "VULNERABLE"), like we did, an attacker can also add bytecode that runs Runtime.getRuntime().exec("rm -rf *") or establish a remote shell on the system and thereby compromise the system or in the worst case even parts of the SAP landscape.
We could successfully verify this exploit on the following systems:
SAP Application Server Java 7.20 with SAPJVM 1.6.0_07 (build 007)
SAP Application Server Java 7.30 with SAPJVM 1.6.0_23 (build 034)
SAP Application Server Java 7.40 with SAPJVM 1.6.0_43 (build 048)
After SAP Product Security’s response, we downloaded SAPJVM 1.6.0_141 build 99 from [6] and indeed, the AnnotationInvocationHandler, which is at the core of theJdk7u21 gadget exploits, was patched. So, with that version, the JdkGadget cannot be used anymore for exploitation.
However, since staying up-to-date with modern software product release cycles is a big challenge for customers and the corresponding SAP notes do not explicitely bring the reader’s attention to a severe security vulnerability, we’d like to raise awareness that not updating the SAP JVM can expose their SAP systems to serious threats.
In Q4 2017 I was pentesting a customer. Shortly before, I had studied json attacks when I stumbled over an internet-facing B2B-portal-type-of-product written in Java they were using (I cannot disclose more details due to responsible disclosure). After a while, I found that one of the server responses sent a serialized Java object, so I downloaded the source code and found a way to make the server deserialize untrusted input. Unfortunately, there was no appropriate gadget available. However, they are using groovy-2.4.5 so when I saw [1] end of december on twitter, I knew I could pwn the target if I succeeded to write a gadget for groovy-2.4.5. This led to this blog post which is based on work by Sam Thomas [2], Wouter Coekaerts [3] and Alvaro Muñoz (pwntester) [4].
Be careful when you fix your readObject() implementation...
We'll start by exploring a popular mistake some developers made during the first mitigation attempts, after the first custom gadgets surfaced after the initial discovery of a vulnerability. Let's check out an example, the Jdk7u21 gadget. A brief recap of what it does: It makes use of a hashcode collision that occurs when a specially crafted instance of java.util.LinkedHashSet is deserialized (you need a string with hashcode 0 for this). It uses a java.lang.reflect.Proxy to create a proxied instance of the interface javax.xml.transform.Templates, with sun.reflect.annotation.AnnotationInvocationHandler as InvocationHandler. Ultimately, in an attempt to determine equality of the provided 2 objects the invocation handler calls all argument-less methods of the provided TemplatesImpl class which yields code execution through the malicious byte code inside the TemplatesImpl instance. For further details, check out what the methods AnnotationInvocationHandler.equalsImpl() and TemplatesImpl.newTransletInstance() do (and check out the links related to this gadget).
The following diagram, taken from [5], depicts a graphical overview of the architecture of the gadget.
So far, so well known.
In recent Java runtimes, there are in total 3 fixes inside AnnotationInvocationHandler which break this gadget (see epilogue). But let's start with the first and most obvious bug. The code below is from AnnotationInvocationHandler in Java version 1.7.0_21:
There is a try/catch around an attempt to get the proxied annotation type. But the proxied interface javax.xml.transform.Templates is not an annotation. This constitutes a clear case of potentially dangerous input that would need to be dealt with. However, instead of throwing an exception there is only a return statement inside the catch-branch. Fortunately for the attacker, the instance of the class is already fit for purpose and does not need the rest of the readObject() method in order to be able to do its malicious work. So the "return" is problematic and would have to be replaced by a throw new Exception of some sort.
Let's check how this method looks like in Java runtime 1.7.0_80:
Ok, so problem fixed? Well, yes and no. On the one hand, the use of the exception in the catch-clause will break the gadget which currently ships with ysoserial. On the other hand, this fix is a perfect example of the popular mistake I'm talking about. Wouter Coekaerts (see [3]) came up with an idea how to bypass such "fixes" and Alvaro Muñoz (see [4]) provided a gadget for JRE8u20 which utilizes this technique (in case you're wondering why there is no gadget for jdk1.7.0_80: 2 out of the total 3 fixes mentioned above are already incorporated into this version of the class. Even though it is possible to bypass fix number one, fix number two would definitely stop the attack).
Let's check out how this bypass works in detail.
A little theory
Let's recap what the Java (De-)Serialization does and what the readObject() method is good for. Let's take the example of java.util.HashMap. An instance of it contains data (key/value pairs) and structural information (something derived from the data) that allows logarithmic access times to your data. When serializing an instance of java.util.HashMap it would not be wise to fully serialize its internal representation. Instead it is completely sufficient to only serialize the data that is required to reconstruct its original state: Metadata (loadfactor, size, ...) followed by the key/value pairs as flat list. Let's have a look at the code:
As you can see, the method starts with a call to defaultReadObject. After that, the instance attributes loadFactor and threshold are initialized and can be used. The key/value pairs are located at the end of the serialized stream. Since the key/value pairs are contained as an unstructured flat list in the stream calling putVal(key,value) basically restores the internal structure, what allows to efficiently use them later on.
In general, it is fair to assume that many readObject() methods look like this:
Coming back to AnnotationInvocationHandler, we can see that its method readObject follows this pattern. Since the problem was located in the custom code section of the method, the fix was also applied there. In both versions, ObjectInputStream.defaultReadObject() is the first instruction. Now let's discuss why this is a problem and how the bypass works.
Handcrafted Gadgets
At work we frequently use ysoserial gadgets. I suppose that many readers are probably familiar with the ysoserial payloads and how these are created. A lot of Java reflection, a couple of fancy helper classes doing stuff like setting Fields, creating Proxy and Constructor instances. With "Handcrafted Gadgets" I meant gadgets of a different kind. Gadgets which cannot be created in the fashion ysoserial does (which is: create an instance of a Java object and serialize it). The gadgets I'm talking about are created by compiling a serialization stream manually, token by token. The result is something that can be deserialized but does not represent a legal Java class instance. If you would like to see an example, check out Alvaro's JRE8_20 gadget [4]. But let me not get ahead of myself, let's take a step back and focus on the problem I mentioned at the end of the last paragraph. The problem is that if the developer does not take care when fixing the readObject method, there might be a way to bypass that fix. The JRE8_20 gadget is an example of such a bypass. The original idea was, as already mentioned in the introduction, first described by Wouter Coekaerts [2]. It can be summarized as follows:
Idea
The fundamental insight is the fact that many classes are at least partly functional when the default attributes have been instantiated and propagated by the ObjectInputStream.defaultReadObject() method call. This is the case for AnnotationInvocationHandler (in older Java versions, more recent versions don't call this method anymore). The attacker does not need the readObject to successfully terminate, an object instance where the method ObjectInputStream.defaultReadObject() has executed is perfectly okay. However, it is definitely not okay from an attacker's perspective if readObject throws an exception, since, eventually this will break deserialization of the gadget completely. The second very important detail is the fact that if it is possible to suppress somehow the InvalidObjectException (to stick with the AnnotationInvocationHandler example) then it is possible to access the instance of AnnotationInvocationHandler later through references. During the deserialization process ObjectInputStream keeps a cache of various sorts of objects. When AnnotationInvocationHandler.readObject is called an instance of the object is available in that cache.
This brings the number of necessary steps to write the gadget down to two. Firstly, store the AnnotationInvocationHandler in the cache by somehow wrapping it such that the exception is suppressed. Secondly, build the original gadget, but replace the AnnotationInvocationHandler in it by a reference to the object located in the cache.
Now let's step through the detailed technical explanation.
If one thinks about object serialization and the fact that you can nest objects recursively it is clear that something like references must exist. Think about the following construct:
Here, the attribute a of class instance c points to an existing instance already serialized before and the serialized stream must reflect this somehow. When you look at a serialized binary stream you can immediately see the references: The hex representation usually looks like this:
71 00 7E AB CD
where AB CD is a short value which represents the array index of the referenced object in the cache. You can easily spot references in the byte stream since hex 71 is "q" and hex 7E is "~":
Wouter Coekaerts found the class java.beans.beancontext.BeanContextSupport. At some point during deserialization it does the following:
continue in the catch-branch, exactly what we need. So if we can build a serialized stream with an AnnotationInvocationHandler as first child of an instance of BeanContextSupport during deserialization we will end up in the catch (IOException ioe) branch and deserialization will continue.
Let's test this out. I will build a serialized stream with an illegal AnnotationInvocationHandler in it ("illegal" means that the type attribute is not an annotation) and we will see that the stream deserializes properly without throwing an exception. Here is what the structure of this stream will look like:
Once done, the deserialized object is a HashMap with one key/value pair, key is an instance of BeanContextSupport, value is "whatever".
Click here to see the code on github.com
You need to build Alvaro's project [6] to get the jar file necessary for building this:
kai@CodeVM:~/eworkspace/deser$ javac -cp /home/kai/JRE8u20_RCE_Gadget/target/JRE8Exploit-1.0-SNAPSHOT.jar BCSSerializationTest.java
kai@CodeVM:~/eworkspace/deser$ java -cp .:/home/kai/JRE8u20_RCE_Gadget/target/JRE8Exploit-1.0-SNAPSHOT.jar BCSSerializationTest > 4blogpost
Writing java.lang.Class at offset 1048
Done writing java.lang.Class at offset 1094
Writing java.util.HashMap at offset 1094
Done writing java.util.HashMap at offset 1172
Adjusting reference from: 6 to: 8
Adjusting reference from: 6 to: 8
Adjusting reference from: 8 to: 10
Adjusting reference from: 9 to: 11
Adjusting reference from: 6 to: 8
Adjusting reference from: 14 to: 16
Adjusting reference from: 14 to: 16
Adjusting reference from: 14 to: 16
Adjusting reference from: 14 to: 16
Adjusting reference from: 17 to: 19
Adjusting reference from: 17 to: 19
kai@CodeVM:~/eworkspace/deser$
A little program that deserializes the created file and prints out the resulting object shows us this:
This concludes the first part, we successfully wrapped an instance of AnnotationInvocationHandler inside another class such that deserialization completes successfully.
Now we need to make that instance accessible. First we need to get hold of the cache. In order to do this, we need to debug. We set a breakpoint at the highlighted line in java.util.HashMap:
Then start the deserializer program and step into readObject:
When we open it we can see that number 24 is what we were looking for.
Here is one more interesting thing: If you deserialize with an older patch level of the Java Runtime, the object is initialized as can be seen in the sceenshot below:
If you use a more recent patch level like Java 1.7.0_151 you will see that the attributes memberValues and type are null. This is the effect of the third improvement in the class I've been talking about before. More recent versions don't call defaultReadObject at all, anymore. Instead, they first check if type is an annotation type and only after that they populate the default fields.
Let's do one more little exercise. In the program above in line 150, change
As you can see, the entry in the handles table can easily be referenced.
Now we'll leave the Jdk7u21 gadget and AnnotationInvocationHandler and build a gadget for groovy 2.4.5 using the techniques outlined above.
A deserialization gadget for groovy-2.4.5
Based on an idea of Sam Thomas (see [2]).
The original gadget for version 2.3.9 looks like this:
Trigger is readObject of our beloved AnnotationInvocationHandler, it will call entrySet of the memberValues hash map, which is a proxy class with invocation handler of type org.codehaus.groovy.runtime.ConvertedClosure. Now every invocation of ConvertedClosure will be delegated to doCall of the nested instance of MethodClosure which is a wrapper of the call to the groovy function execute. The OS command that will be executed is provided as member attribute to MethodClosure.
After the original gadget for version 2.3.9 showed up MethodClosure was fixed by adding a method readResolve to the class org.codehaus.groovy.runtime.MethodClosure:
If the global constant ALLOW_RESOLVE is not set to true an UnsupportedOperationException is supposed to break the deserialization. Basically, this means that an instance of MethodClosure cannot be deserialized anymore unless one explicitely enables it.
Let's quickly analyze MethodClosure: The class does not have a readObject method and readResolve is called after the default built-in deserialization. So when readResolve throws the exception the situation is almost identical to the one explained in the above paragraphs: An instance of MethodClosure is already in the handle table. But there is one important difference: AnnotationInvocationHandler throws an InvalidObjectException which is a child of IOException whereas readResolve throws an UnsupportedOperationException, which is a child of RuntimeException. BeanContextSupport, however, only catches IOException and ClassCastException. So the identical approach as outlined above would not work: The exception would not be caught. Fortunately, in late 2016 Sam Thomas found the class sun.security.krb5.KRBError which in its readObject method transforms every type of exception into IOException:
This means if we put KRBError in between BeanContextSupport and MethodClosure the UnsupportedOperationException will be translated into IOException which is ultimately caught inside the readChildren method of BeanContextSupport.
So our wrapper construct looks like this:
Some readers might be confused by the fact that you can nest an object of type MethodClosure inside a KRBError. Looking at the code and interface of the latter, there is no indication that this is possible. But it is important to keep in mind that what we are concerned with here are not Java objects! We are dealing with a byte stream that is deserialized. If you look again at the readObject method of KRBError you can see that this class calls ObjectInputStream.readObject() right away. So here, every serialized Java object will do fine. Only the cast to byte array will throw a ClassCastException, but remember: An exception will be thrown already before that and this is perfectly fine with the design of our exploit.
Now it is time to put the pieces together. The complete exploit consists of a hash map with one key/value pair, the BeanContextSupport is the key, the groovy gadget is the value. [1] suggests putting the BeanContextSupport inside the AnnotationInvocationHandler but it has certain advantages for debugging to use the hash map. Final structure looks like this:
The final exploit can be found on github.com.
I had mentioned 3 improvements in AnnotationInvocationHandler but I only provided one code snippet. For the sake of completeness, here are the two:
The second fix in jdk1.7.0_80 which already breaks the jdk gadget is a check in equalsImpl:
The highlighted check will filter out the methods getOutputProperties and newTransformer of TemplatesImpl because they are not considered annotation methods, and getMemberMethods returns an empty array so the methods of TemplatesImpl are never called and nothing happens.
The third fix which you can find for example in version 1.7.0_151 finally fixes readObject:
As one can see, only the 2 last calls actually set the member attributes type and memberValues. defeaultReadObject is not used at all. Before, the type check for the annotation class is performed. If it fails, an InvalidObjectException is thrown and type and memberValues remain null.
This blog post describes the research on SAP J2EE Engine 7.50 I did between
October 2020 and January 2021. The first part describes how I set off to find a
pure SAP deserialization gadget, which would allow to leverage SAP's P4 protocol
for exploitation, and how that led me, by sheer coincidence, to an entirely
unrelated, yet critical vulnerability, which is outlined in part two.
The reader is assumed to be familiar with Java Deserialization and should have a
basic understanding of Remote Method Invocation (RMI) in Java.
Prologue
It was in 2016 when I first started to look into the topic of Java Exploitation, or,
more precisely: into exploitation of unsafe deserialization of Java objects.
Because of my professional history, it made sense to have a look at an SAP
product that was written in Java. Naturally, the P4 protocol of SAP NetWeaver
Java caught my attention since it is an RMI-like protocol for remote
administration, similar to Oracle WebLogic's T3. In May 2017, I published a
blog post about an exploit that was getting RCE by using the Jdk7u21 gadget. At
that point, SAP had already provided a fix long ago. Since then, the subject
has not left me alone. While there were new deserialization
gadgets for Oracle's Java server product almost every month, it surprised me no one ever heard of
an SAP deserialization gadget with comparable impact. Even
more so, since everybody who knows SAP software knows the vast amount of code
they ship with each of their products. It seemed very improbable to me that
they would be absolutely immune against the most prominent bug class in the
Java world of the past six years. In October 2020 I finally found the time and
energy to set off for a new hunt. To my great disappointment, the search was in
the end not successful. A gadget that yields RCE similar to the ones from the
famous ysoserial project is still not in sight. However in January, I found a
completely unprotected RMI call that in the end yielded administrative access
to the J2EE Engine. Besides the fact that it can be invoked through P4 it has
nothing in common with the deserialization topic. Even though a mere chance
find, it is still highly critical and allows to compromise the security of the underlying
J2EE server.
The bug was filed as CVE-2021-21481. On march 9th 2021, SAP provided a
fix. SAP note 3224022 describes the details.
P4 and JNDI
Listing 1 shows a small program that connects to a SAP J2EE server using P4:
The only hint that this code has something to do with a proprietary protocol
called P4 is the URL that starts with P4://. Other than that, everything is
encapsulated by P4 RMI calls (for those who want to refresh their memory about
JNDI).
Furthermore, it is not obvious that what is going on behind the scenes has
something to do with RMI. However, if you inspect more closely the types of the
involved Java objects, you'll find that keysMngr is of type
com.sun.proxy.$Proxy (implementing interface KeystoreManagerWrapper) and
keysMngr.getKeystore() is a plain vanilla RMI-call. The argument (the name
of the keystore to be instantiated) will be serialized and sent to the server
which will return a serialized keystore object (in this case it won't because
there is no keystore "whatever"). Also not obvious is that the instantiation
of the InitialContext requires various RMI calls in the background, for
example the instantiation of a RemoteLoginContext object that will allow to
process the login with the provided credentials.
Each of these RMI calls would in theory be a sink to send a
deserialization gadget to. In the exploit I mentioned above, one of the first
calls inside new InitialContext() was used to send the Jdk7u21 gadget
(instead of a java.lang.String object, by the way).
Now, since the Jdk7u21 gadget is not available anymore and I was looking for a
gadget consisting merely of SAP classes, I had to struggle with a very annoying
limitation: The classloader segmentation. SAP J2EE knows various types of
software components: interfaces, services, libraries and applications (which
can consist of web applications and EJBs). When you deploy a component, you
have to declare the dependencies to other components your component relies
upon. Usually, web applications depend on 2-3 services and libraries which will
have a couple of dependencies to other services and libraries, as well. At the
bottom of this dependency chain are the core components.
Now, the limitation I was talking about is the fact that the dependency
management greatly affects which classes a component can see: It can precisely
see all classes of all components it relies upon (plus of course JDK classes)
but not more. If your class ships as part of the keystore service above, it
will only be able to resolve classes from components the keystore service
declares as dependencies.
Figure 1: dependencies of the keystore service with all child and parent classloaders
This has dramatic consequences for gadget development. Suppose you found a
gadget whose classes come from components X, Y and Z but there are no
dependencies between these components and in addition, there is no component
which depends on all of them. Then, no matter in which classloader context your
gadget will be deserialized, at least one of X, Y or Z will be missing in the
classpath and the deserialization will end up in a ClassNotFoundException.
By using a similar approach to the one described in the GadgetProbe
project I found out that at the
point the Jdk7u21 gadget was deserialized in the above mentioned exploit, there
were only about 160 non-JDK classes visible that implement
java.io.Serializable. Not ideal for building an exploit.
Going back to listing 1, in case we send a gadget instead of the string
"whatever", we can tell from figure 1 that classes from ten components (the
ones listed beneath "Direct parent loaders") will be in the class path.
Code that sends an arbitrary serializable object instead of the string
"whatever" could e.g. look like this (instead of keysMgr.getKeystore()):
If there was a gadget, one could send it with out.writeObject().
With this approach, the critical mass of accessible serializable classes can be
significantly increased. The telnet interface of SAP J2EE provides useful
information about the services and their dependencies.
Regardless of the classloader challenge, I was eager to get an overview of how
many serializable classes existed in the server. The number of classes in the
core layer, services and libraries amounts to roughly 100,000, and this does
not even count application code. I quickly realized that I needed something
smarter than the analysis features of Eclipse to handle such volumes. So I
developed my own tool which analyses Java bytecode using the OW2 ASM
Framwork. It writes object and interface inheritance
dependencies, methods, method calls and attributes to a SQLite DB. It turned
out that out of the 100,000 classes, about 16,000 implemented
java.io.Serializable. The RDBMS approach was pretty handy since it allowed
build complex queries like
Give classes which are Serializable and Cloneable which implement private void readObject(java.io.ObjectInputStream) and whose toString() method exists and has more than five calls to distinct other methods
This question translates to
The work on this tool and also the process of constantly inventing new and
original queries to find potentially interesting classes was great fun.
Unfortunately, it was also in vain. There is a library, which almost allowed
to build a wonderful chain from a toString() call to the ubiquitous
TemplatesImpl.getOutputProperties(), but the API provided by the library is
so very complex and undocumented that, after two months, I gave up in total frustration.
There were some more small findings which don't really deserve to
be mentioned. However, I'd like to elaborate on one more thing before I'll
start part two of the blog post, that covers the real vulnerability.
One of the first interesting classes I discovered performs a JNDI lookup with
an attacker controlled URL in private void readObject(java.io.ObjectInputStream). What would have been a direct hit four
years ago could at least have been a respectable success in 2020. Remember:
Oracle JRE finally switched off remote classloading when resolving LDAP
references in 2019 in version JRE 1.8.0_191. Had this been exploitable, it
would have opened up an attack avenue at least for systems with outdated JRE.
My SAP J2EE was running on top of a JRE version 1.8.0_51 from 2015, so the JNDI
injection should have worked, but, to my great surprise, it didn't.
The reason can be found in the method getObjectInstance of javax.naming.spi.DirectoryManager:
The hightlighted call to getObjectFactoryFromReference is where an attacker needs to get to. The method resolves the JNDI reference using an URLClassLoader and an attacker-supplied codebase. However, as one can easily see, if getObjectFactoryBuilder() returns a non-null object the code returns in either of the two branches of the following if-clause and the call to getObjectFactoryFromReference below is never reached.
And that is exactly what happens. SAP J2EE registers an ObjectFactoryBuilder of type com.sap.engine.system.naming.provider.ObjectFactoryBuilderImpl. This class will try to find a factory class based on the factoryName-attribute and completely ignore the codebase-attribute of the JNDI reference.
Bottom line is that JNDI injection might never have worked in SAP J2EE, which would eliminate one of the most important attack primitives in the context of Java Deserialization attacks.
CVE-2021-21481
After digressing about how I searched for deserialization gadgets, I'd like to
cover the real vulnerability now, which has absolutely nothing to do with Java
Deserialization. It is a plain vanilla instance of CWE-749: Exposed Dangerous
Method or Function. Let's go back to Listing 1. We can see that the JNDI
context allows to query interfaces by name, in our example we were querying the
KeyStoreManager interface by the name "keystore". On several occasions, I had
already tried to find an available rich client for SAP J2EE Engine
administration that uses P4. Every time I was unsuccessful, I believed such a
client did not officially exist, or at least was not at everyone's disposal.
However, whenever you install a SAP J2EE Engine, the P4 port is enabled by
default and listening on the same network interface as the HTTP(s) services.
Because I was totally focussing on Deserialization, for a long time I
was oblivious how much information one can glean through the JNDI context. E.g.
it is trivial to get all bindings:
The list() call allows to simply iterate through all bindings:
Interesting items are proxy objects and the _Stub objects. E.g. the proxy for
messaging.system.MonitorBean can be cast to
com.sap.engine.messaging.app.MonitorHI.
During debugging of the server, I had already encountered the class
JUpgradeIF_Stub, long before I executed the call from Listing 5. The class
has a method openCfg(String path) and it was not difficult to establish that the
server version of the call didn't perform any authorization check. This one
definitively looked fishy to me, but since I wasn't looking for unprotected RMI
calls I put the finding into the box with the label "check on a rainy sunday
afternoon when the kids are busy with someone else".
But then, eventually, I did check it. It didn't take long to realize that I
had found a huge problem. Compare Listing 6.
The configuration settings of SAP J2EE Engine are organized in a hierarchical
structure. The location of an object can be specified by a path, pretty much
like a path of a file in the file system. The above code gets a reference to
the JUpgradeIF_Stub by querying the JNDI context with name
"MigrationService", gets an instance of a Configuration object by a call to
openCfg() and then walks down the path to the leaf node. The element found there
can be exported to an archive that is stored in the file system of the server
(call to export(String path)). If carefully chosen, the local path on the
server will point to a root folder of a web application. There, download.zip
can simply be downloaded through HTTP. If you want to check for yourself, the
UME configuration is stored at
cluster_config/system/custom_global/cfg/services/com.sap.security.core.ume.service/properties.
You'd probably say "hey! I need to be Administrator to do that! Where's the
harm?". Right, I thought so, too. But neither do you need to be Administrator,
nor do you even have to be authenticated. The following code works perfectly
fine:
So does the enumeration using ctxt.list() from Listing 5. The fact that authentication is
not needed at this point is not new at all by the way, compare CVE-2017-5372.
However, you will get a permission exception when calling
keysMngr.getKeystore() (because getKeystore() does have a permission
check). But JUpgradeIF.openCfg() was missing the check until SAP fixed it.
At this point, even without SAP specific knowledge an attacker can cause
significant harm. E.g. flood the server's file system with archives causing a
resource exhaustion DoS condition.
With a little insider knowledge one can get admin access. In the configuration
tree, there is a keystore called TicketKeystore. Its cryptographic key pair
is used to sign SAP Logon Tickets. If you steal the keystore, you can issue a
ticket for the Administrator user and log on with full admin rights. There
are also various other keystores, e.g. for XML signatures and the like (let
alone the fact that there is tons of stuff in this store. No one probably
knows all the security sensitive things you can get access to ...)
This information should be sufficient to the understanding of CVE-2021-21481.
The exact location of the keystores in the configuration and the relative local
path in order to download the archive by HTTP are left as an exercise to the
reader.