BruceFan's Blog

Stay hungry, stay foolish

0%

Java反序列化漏洞(一)基础知识

要理解Java反序列化漏洞需要一些Java的相关知识:
1.使用Java反射机制
2.利用sun.reflect.annotation.AnnotationInvocationHandler类
3.调用TransformedMap类的decorate方法时,参数一的Map对象需要put进”value”与非空的值
4.AnnotationInvocationHandler类的实例化参数一需要为java.lang.annotation.Retention类
下面先介绍基础知识,再利用类TransformedMap与AnnotationInvocationHandler分析漏洞。

Java序列化与反序列化

Java序列化:使用ObjectOutputStream.writeObject方法可对实现了Serializable接口的对象进行序列化,序列化的数据可存储在文件中,或通过网络传输。
Java反序列化:使用ObjectInputStream.readObject方法可对序列化的数据进行反序列化。当实现了Serializable接口的对象被反序列化时,该对象的readObject方法会被调用。
String实现了Serializable接口,可进行序列化。以下测试代码对String类的对象进行序列化,将序列化的数据保存在文件中,再从文件读取序列化的数据进行反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Basic {
public static void main(String[] args) throws IOException, ClassNotFoundException {
String test = "tttest"; // 需要序列化的对象
// 将序列化数据写入文件
FileOutputStream fos = new FileOutputStream("test.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(test);
os.close();
// 从文件读取序列化数据
FileInputStream fis = new FileInputStream("test.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
String stringFromData = (String)ois.readObject();
ois.close();
System.out.println(stringFromData);
}
}

Java序列化数据的magic number

java.io.ObjectStreamConstants类中定义了STREAM_MAGIC与STREAM_VERSION,查看JDK1.5、1.6、1.7、1.8的ObjectStreamConstants类,STREAM_MAGIC值均为0xaced,STREAM_VERSION值均为5。即0xaced为Java对象序列化流的魔数,0x0005为Java对象序列化的版本号,Java对象序列化数据的前4个字节为”AC ED 00 05”。
查看上一步生成的文件:

对自定义类的序列化与反序列化

下面的测试代码为SerializeMyClass类,在其中定义了内部类MyObject。MyObject类实现了Serializable接口,SerializeMyClass类会对MyObject类的对象进行序列化,将序列化的数据保存在文件中,再从文件读取序列化的数据进行反序列化。

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
package test;

public class SerializeMyClass {
public static void main(String[] args) throws Exception {
// 需要序列化的对象
MyObject myObj = new MyObject("tttest");

FileOutputStream fos = new FileOutputStream("test-myobj.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(myObj); // 将序列化数据写到文件
os.close();

FileInputStream fis = new FileInputStream("test-myobj.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
MyObject objectFromData = (MyObject) ois.readObject(); // 从文件读取序列化数据
ois.close();

System.out.println(objectFromData.getName());
}
}

class MyObject implements Serializable {
private static final long serialVersionUID = 1L;
private String name = "initial";
public MyObject() {
this.name = "no name-default";
System.out.println("MyObject(String name) " + name);
}
public MyObject(String name) {
this.name = name;
System.out.println("MyObject(String name) " + name);
}
public String getName() {
return this.name;
}
// 重写了Serializable接口的readObject方法
private void readObject(ObjectInputStream ois) throws IOException,
ClassNotFoundException {
ois.defaultReadObject();
System.out.println("MyObject-readObject!!!! " + this.name);
this.name = this.name + "!";
}
}

程序输出:

1
2
3
MyObject(String name) tttest
MyObject-readObject!!!! tttest
tttest!

可以看到MyObject类实现的Serializable接口的readObject方法会被调用,且对象被序列化再反序列化后,对其值没有影响。

Java反射机制

之前写过一篇Java反射机制的文章,不过只是一些理论类的东西,跟文档差不多,这里实际使用一下。
(1)调用FileOutputStream类写文件时,常用代码如下:

1
2
FileOutputStream fos = new FileOutputStream("1.txt");
fos.write("abc".getBytes());

若需要使用Java反射机制调用FileOutputStream类写文件,且只允许调用Class.getMethod与Method.invoke方法,需要使用如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ReflectBasic {
public static void main(String[] args) throws NoSuchMethodException, SecurityException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException {
Object object = FileOutputStream.class;
Class cls = object.getClass(); // class java.lang.Class
/* getMethod第二个参数是一个Class数组,数组中是获取的method的参数类型,
* getConstructor的参数类型是Class数组,数组中是构造函数的参数类型
*/
Method method = cls.getMethod("getConstructor", new Class[] { Class[].class });
// invoke第二个参数是一个Object数组,数组中是调用的方法的参数
object = method.invoke(object, new Object[] { new Class[] { String.class } });
cls = object.getClass();
// newInstance的参数类型是Object数组
method = cls.getMethod("newInstance", new Class[] { Object[].class });
object = method.invoke(object, new Object[] { new Object[] { "./1.txt" }});
cls = object.getClass();
method = cls.getMethod("write", new Class[] { byte[].class });
object = method.invoke(object, new Object[] {"abc".getBytes()});
}
}

(2)使用Java反射机制调用Runtime类执行程序,常用代码如下:

1
2
Runtime runtime = Runtime.getRuntime();
runtime.exec("calc");

如果使用Java反射机制调用Runtime类执行程序,且只允许调用Class.getMethod与Method.invoke方法,上述代码需修改为如下形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Object object = Runtime.class;
Class cls = object.getClass();
/* 此时cls的类型为Class而不是Runtime,只能先获取getMethod方法并调用,
* 以获取Runtime类的getRuntime方法,无法直接获取getRuntime方法 */
Method method = cls.getMethod("getMethod", new Class[] { String.class, Class[].class });
object = method.invoke(object, new Object[] { "getRuntime", new Class[0] });
System.out.println("-1- " + object.getClass());

// 此时cls为Method,对应getRuntime方法,获取invoke方法并执行
cls = object.getClass();
method = cls.getMethod("invoke", new Class[] { Object.class, Object[].class });
object = method.invoke(object, new Object[] { null, new Object[0] });

// 此时cls为Runtime,对应Runtime.getRuntime()的结果,可调用exec方法
cls = object.getClass();
method = cls.getMethod("exec", new Class[] { String.class });
object = method.invoke(object, new Object[] { "/usr/bin/open /Applications/Notes.app" });

Java反射机制与序列化

当需要操作无法直接访问的类时,需要使用Java的反射机制。即对无法直接访问的类进行序列化时,需要使用Java的反射机制。
以下测试代码为testreflection.TestReflection类,与前文中的test.MyObject类不在同一个包中,在TestReflection类中对MyObject类进行序列化时,需要使用Java的反射机制。

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 testreflection;

public class TestReflection {
public static void main(String args[]) throws Exception {
doSerialize("tttest");
doSerialize(null);
}

private static void doSerialize(String data) throws Exception {
Class cls = Class.forName("test.MyObject");
Constructor ctor = null;
Object instance = null;
if (data != null)
ctor = cls.getDeclaredConstructor(new Class[] { String.class });
else
ctor = cls.getDeclaredConstructor();

ctor.setAccessible(true);

System.out.println("-before newInstance-");
if (data != null)
instance = ctor.newInstance(data);
else
instance = ctor.newInstance();
System.out.println("-after newInstance-");

ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteOut);
out.writeObject(instance);
out.flush();
out.close();

System.out.println("byteOut.toByteArray().length:"+byteOut.toByteArray().length);
ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
ObjectInputStream ois = new ObjectInputStream(byteIn);
// 可以触发自定义类的readObject方法
Object object = (Object)ois.readObject();
System.out.println("object:" + object.getClass());
}
}

程序执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
-before newInstance-
MyObject(String name) tttest
-after newInstance-
byteOut.toByteArray().length:71
MyObject-readObject!!!! tttest
object:class test.MyObject
-before newInstance-
MyObject(String name) no name-default
-after newInstance-
byteOut.toByteArray().length:80
MyObject-readObject!!!! no name-default
object:class test.MyObject

基础知识就介绍到这里,后面会再进行漏洞分析的学习。
reference
JAVA反序列化漏洞完整过程分析与调试