首先我们介绍下序列化和反序列化的概念:
-
序列化:把Java对象转换为字节序列的过程。 -
反序列化:把字节序列恢复为Java对象的过程。
对象的序列化主要有两种用途:
-
把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(持久化对象) -
在网络上传送对象的字节序列。(网络传输对象)
任何一门成熟的语言,在网络上传递信息,都会使用到一些格式化的数据结构,比如
YAML
JSON
XML
像阿里的FastJson就是简单化处理JSON格式的数据的。
02 序列化前提
在Java中,如果一个对象要想实现序列化,必须要实现下面两个接口之一:
-
Serializable
接口 -
Externalizable
接口
Java 通过对象输入输出流来实现序列化和反序列化:
-
java.io.ObjectOutputStream
类的writeObject()
方法可以实现序列化; -
java.io.ObjectInputStream
类的readObject()
方法用于实现反序列化。
序列化和反序列化示例:
public class SerializeDemo01 {
enum Sex {
MALE,
FEMALE
}
static class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name = null;
private Integer age = null;
private Sex sex;
public Person() { }
public Person(String name, Integer age, Sex sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
@Override
public String toString() {
return "Person{" + "name='" + name + ''' + ", age=" + age + ", sex=" + sex + '}';
}
}
/**
* 序列化
*/
private static void serialize(String filename) throws IOException {
File f = new File(filename); // 定义保存路径
OutputStream out = new FileOutputStream(f); // 文件输出流
ObjectOutputStream oos = new ObjectOutputStream(out); // 对象输出流
oos.writeObject(new Person("Jack", 30, Sex.MALE)); // 保存对象
oos.close();
out.close();
}
/**
* 反序列化
*/
private static void deserialize(String filename) throws IOException, ClassNotFoundException {
File f = new File(filename); // 定义保存路径
InputStream in = new FileInputStream(f); // 文件输入流
ObjectInputStream ois = new ObjectInputStream(in); // 对象输入流
Object obj = ois.readObject(); // 读取对象
ois.close();
in.close();
System.out.println(obj);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
final String filename = "~/Donwloads/s.dat";
serialize(filename);
deserialize(filename);
}
}
其中代码最关键的地方就是
ObjectInputStream ois = new ObjectInputStream(in); // 对象输入流
Object obj = ois.readObject(); // 读取对象
这两行其实就包括了整个反序列化的流程。
我们来看下生成的s.dat文件,用vim打开之后输入:%!xxd
就可以查看文件的十六进制模式了
一段数据如果以aced开头,那么他就是这一段java序列化的16进制
我们可以用到一个工具SerializationDumper 来查看我们序列化生成的字节流 https://github.com/NickstaDB/SerializationDumper
我们把十六进制提取出来命令运行后
java -jar /Users/fengxuan/Downloads/SerializationDumper-v1.13.jar aced000573720023636f6d2e6576616c7368656c6c2e5365724170706c69636174696f6e24506572736f6e00000000000000010200034c00036167657400134c6a6176612f6c616e672f496e74656765723b4c00046e616d657400124c6a6176612f6c616e672f537472696e673b4c00037365787400224c636f6d2f6576616c7368656c6c2f5365724170706c69636174696f6e245365783b7870737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b02000078700000001e7400044a61636b7e720020636f6d2e6576616c7368656c6c2e5365724170706c69636174696f6e2453657800000000000000001200007872000e6a6176612e6c616e672e456e756d000000000000000012000078707400044d414c450a
得到一下数据
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 35 - 0x00 23
Value - com.evalshell.SerApplication$Person - 0x636f6d2e6576616c7368656c6c2e5365724170706c69636174696f6e24506572736f6e
serialVersionUID - 0x00 00 00 00 00 00 00 01
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 3 - 0x00 03
Fields
0:
Object - L - 0x4c
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 19 - 0x00 13
Value - Ljava/lang/Integer; - 0x4c6a6176612f6c616e672f496e74656765723b
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - name - 0x6e616d65
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 02
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
2:
Object - L - 0x4c
fieldName
Length - 3 - 0x00 03
Value - sex - 0x736578
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 34 - 0x00 22
Value - Lcom/evalshell/SerApplication$Sex; - 0x4c636f6d2f6576616c7368656c6c2f5365724170706c69636174696f6e245365783b
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 04
classdata
com.evalshell.SerApplication$Person
values
age
(object)
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 17 - 0x00 11
Value - java.lang.Integer - 0x6a6176612e6c616e672e496e7465676572
serialVersionUID - 0x12 e2 a0 a4 f7 81 87 38
newHandle 0x00 7e 00 05
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 1 - 0x00 01
Fields
0:
Int - I - 0x49
fieldName
Length - 5 - 0x00 05
Value - value - 0x76616c7565
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_CLASSDESC - 0x72
className
Length - 16 - 0x00 10
Value - java.lang.Number - 0x6a6176612e6c616e672e4e756d626572
serialVersionUID - 0x86 ac 95 1d 0b 94 e0 8b
newHandle 0x00 7e 00 06
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 07
classdata
java.lang.Number
values
java.lang.Integer
values
value
(int)30 - 0x00 00 00 1e
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 08
Length - 4 - 0x00 04
Value - Jack - 0x4a61636b
sex
(object)
TC_ENUM - 0x7e
TC_CLASSDESC - 0x72
className
Length - 32 - 0x00 20
Value - com.evalshell.SerApplication$Sex - 0x636f6d2e6576616c7368656c6c2e5365724170706c69636174696f6e24536578
serialVersionUID - 0x00 00 00 00 00 00 00 00
newHandle 0x00 7e 00 09
classDescFlags - 0x12 - SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_CLASSDESC - 0x72
className
Length - 14 - 0x00 0e
Value - java.lang.Enum - 0x6a6176612e6c616e672e456e756d
serialVersionUID - 0x00 00 00 00 00 00 00 00
newHandle 0x00 7e 00 0a
classDescFlags - 0x12 - SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 0b
TC_STRING - 0x74
newHandle 0x00 7e 00 0c
Length - 4 - 0x00 04
Value - MALE - 0x4d414c45
Invalid content element type 0x0a
这里有两个关键的标识
TC_OBJECT:标记后面的数据为Object对象
TC_CLASSDESC:类描述符标识,表示一个类中的所有信息
当我们实例化ObjectInputStream
后,首先调用的是ObjectInputStream
的构造方法。
ObjectInputStream
和ObjectOutputStream
类一样有两个构造方法 —— 一个为public
的单参数构造方法,一个为protected
的无参构造方法
同样地,当我们实例化ObjectInputStream
并传入new FileInputStream("panda.out")
参数后,调用的是ObjectInputStream
中的public
单参数构造方法,该方法内容如下:
和ObjectOutputStream
的构造方法一样——在该构造函数的开始,首先会调用verifySubclass
方法处理缓存信息,要求该类(或子类)进行验证——验证是否可以在不违反安全约束的情况下构造此实例。
然后和ObjectOutputStream
不同的是,在ObjectOutputStream
中我们初始化的对象是bout
、handles
、subs
以及enableOverride
,但是在ObjectInputStream
中,我们初始化的对象变成了bin
、handles
、vlist
以及enableOverride
。
/** filter stream for handling block data conversion */
private final BlockDataInputStream bin;
/** validation callback list */
private final ValidationList vlist;
/** wire handle -> obj/exception map */
private final HandleTable handles;
/** if true, invoke readObjectOverride() instead of readObject() */
private final boolean enableOverride;
03 漏洞基本原理
我们用下面这个例子来说明序列化漏洞的产生。
public class test{
public static void main(String args[]) throws Exception{
//定义myObj对象
MyObject myObj = new MyObject();
myObj.name = "hi";
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
MyObject objectFromDisk = (MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
class MyObject implements Serializable{
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
}
}
简单一句话概括就是:一个类在实现Serializable
后重写了 readObject
这个方法,在这里写上了一些危险的代码,就会导致反序列化漏洞的产生。
有的同学忽然惊呼:这不是和php反序列化一样吗?php是在__wakeup魔法方法里,而这里是readObject方法。
我想说的是:对 就是一样,很多同学被一些文章转发的CommonsCollections
这个利用链吓住了,其实正常情况下就是看readObject这个方法有没有危险方法或者利用点的。
04 ysoserial
说到反序列化的利用,就不得不提到ysoserial 链接地址:https://github.com/frohoff/ysoserial, 简单来说,就是利用自己选择的利用链,生成反序列化数据,通过这些数据发送给服务器,从而执行预定的命令。
java -jar ysoserial.jar CommonsCollections1 calc.exe | xxd
如上,ysoserial的利用链就是执行一个系统命令,把生成好的exp发送到对方机器上,这条命令就会执行。
05 总结
如何发现 Java 反序列化漏洞
-
从流量中发现序列化的痕迹,关键字: ac ed 00 05
,rO0AB
(见图1) -
Java RMI 的传输 100% 基于反序列化。 -
查看源码,看下哪些类实现了 Serializable
接口 -
观察反序列化的类中是否重写 readObject()
,是否有可利用的地方
本文始发于微信公众号(风炫安全):Java代码审计系列(六) 反序列化
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论