Java安全[反序列化(1)]

终于开始反序列化开篇,之前再RMI的攻击和流程中发现,大多数的数据发送和接收都是反序列化数据。

那么,为什么反序列化常常会带来安全隐患?

一门成熟的语言,如果需要在网络上传递信息,通常会用到一些格式化数据,

比如:

  • JSON
  • XML

但这两个数据格式都有一个共 同的问题:不支持复杂的数据类型。 大多数处理方法中,JSON和XML支持的数据类型就是基本数据类型,整型、浮点型、字符串、布尔 等,如果开发者希望在传输数据的时候直接传输一个对象,那么就不得不想办法扩展基础的 JSON(XML)语法。

比如,Jackson和Fastjson这类序列化库,在JSON(XML)的基础上进行改造,通过特定的语法来传递对象;亦或者如RMI,直接使用Java等语言内置的序列化方法,将一个对象转换成一串二进制数据进行 传输。

不管是Jackson、Fastjson还是编程语言内置的序列化方法,一旦涉及到序列化与反序列化数据,就可 能会涉及到安全问题。但首先要理解的是,“反序列化漏洞”是对一类漏洞的泛指,而不是专指某种反序 列化方法导致的漏洞,比如Jackson反序列化漏洞和Java readObject造成的反序列化漏洞就是完全不同 的两种漏洞。

我们先来说说Java内置的序列化方法readObject,和其有关的漏洞

Java安全[反序列化(1)]

反序列化方法的对比

说到反序列化,第一时间想到的就是php反序列化和python反序列化。

其中Java反序列化与php反序列化有类似之处,他们都只能将一个对象中的属性按照某种特定的格式生成一段数据流,反序列化时再按照序列化的格式顺序,将属性拿回来重新赋值给新的对象。

但是两者区别在于,Java反序列化更加深入,它提供了更加高级、灵活的方法wirteObject

允许开发者在序列化流中插入一些自定义数据,进而在反序列化的时候能够使用 readObject 进行读取。

而php中有个魔术方法叫做__wakeup,在反序列化的时候进行触发。虽然Java的readObject也是在反序列化的时候触发的,但是两者处理的问题还是有所不同的。

  • readObject倾向于解决反序列化时,如何将序列化对象进行还原为一个完整的对象
  • __wakeup更倾向于解决反序列化后,如何初始化这个对象

下面仔细分析这两者的差异。

php反序列化

php在对数据进行序列化的过程开发者是无法介入的,在调用serialize函数后,序列化数据就已经完成,最后直接得到一个完整的对象,如果还想在序列化数据流中新增某一个内容,只能将其保存在一个属性中,所以php的序列化、反序列化是一个纯内部的过程,而其__sleep__wakeup魔术方法的目的就是序列化或者反序列化的前后执行一些操作。

一个非常典型的PHP序列化例子,就是含有资源类型的PHP类,如数据库连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class Connection
{
protected $link;
private $dsn, $username, $password;
public function __construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}
private function connect()
{
$this->link = new PDO($this->dsn, $this->username, $this->password);
}
}

PHP中,资源类型的对象默认是不会写入序列化数据中的。那么上述Connection类的 $link 属性在序 列化后就是null,反序列化时拿到的也是null。 那么,如果我想要反序列化时拿到的 $link 就是一个数据库连接,我就需要编写 __wakeup 方法:

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
<?php
class Connection
{
protected $link;
private $dsn, $username, $password;
public function __construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}
private function connect()
{
$this->link = new PDO($this->dsn, $this->username, $this->password);
}
public function __sleep()
{
return array('dsn', 'username', 'password');
}
public function __wakeup()
{
$this->connect();
}
}

可见,这里 __wakeup 的工作就是在反序列化拿到Connection对象后,执行 connect() 函数,连接数 据库。

__wakeup 的作用在反序列化后,执行一些初始化操作。但其实我们很少利用序列化数据传递资源类型 的对象,而其他类型的对象,在反序列化的时候就已经赋予其值了。

所以你会发现,PHP的反序列化漏洞,很少是由 __wakeup 这个方法触发的,通常触发在析构函数 __destruct 里。其实大部分PHP反序列化漏洞,都并不是由反序列化导致的,只是通过反序列化可以 控制对象的属性,进而在后续的代码中进行危险操作。

Java反序列化

上篇文章最后提及classAnnotations,这里讲到ObjectAnnotations知识。

Java在序列化时一个对象,将会调用这个对象writeobject方法,参数类型是ObjectOutputStream,开发者可以将任何内容写入这个stream中;

反序列化时,会调用readobject,开发者也可以从读取出前面的写入的内容,再进行处理。

举个例子,展示writeobjectreadobject的作用,

先写个Person类,重写writeobjectreadobject

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

import java.io.*;

public class Person implements java.io.Serializable {
public String name;
public int age;

Person(String name, int age) {
this.name = name;
this.age = age;
}

private void writeObject(java.io.ObjectOutputStream s) throws
IOException {
s.defaultWriteObject(); //调用默认写对象,把Person类中name和age序列化写入字节流中
s.writeObject("This is a object"); //额外写入字符串,加到序列化对象后面,写入字节流中
}

private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject(); //调用默认读对象,反序列化Person类中name和age
String str = (String) s.readObject(); //读取写入字符串
System.out.println(str);
}

public static void main(String[] args) throws Exception {
Person p = new Person("John", 20);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("per.ser"));
oos.writeObject(p);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("per.ser"));
ois.readObject();
}
}

运行结果,

image-20231104085645456

最后用工具SerializationDumper得到反序列化内容,

java -jar .\SerializationDumper.jar -r .\test\per.ser

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
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 12 - 0x00 0c
Value - Ser_1.Person - 0x5365725f312e506572736f6e
serialVersionUID - 0xb5 13 8a f8 13 b1 5c a7
newHandle 0x00 7e 00 00
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - name - 0x6e616d65
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 02
classdata
Ser_1.Person
values
age
(int)20 - 0x00 00 00 14
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 4 - 0x00 04
Value - John - 0x4a6f686e
objectAnnotation
TC_STRING - 0x74
newHandle 0x00 7e 00 04
Length - 16 - 0x00 10
Value - This is a object - 0x546869732069732061206f626a656374
TC_ENDBLOCKDATA - 0x78

可以看到,我们写入的字符串 This is a object 被放在 objectAnnotation 的位置,并且位于Person类的nameage的后面。 在反序列化时,读取了这个字符串,并将其输出。

这个特性就让Java的开发变得非常灵活。比如后面将会讲到的HashMap,其就是将Map中的所有键、 值都存储在 objectAnnotation 中,而并不是某个具体属性里。

重写writeObjectreadObject

对于Java序列化和反序列化的过程,可以更加详细的分析,

首先writeObject是对对象进行序列化,而readObject是对对象进行反序列化

序列化数据后无法通过赋值修改,

而序列化之前以及反序列化之后,可以进行赋值修改数据

就拿上面的代码进行展示,

writeObject

在调用默认写对象进行序列化前,将age加上100,然后用工具看看反序列后数据,

1
2
3
4
5
6
private void writeObject(java.io.ObjectOutputStream s) throws
IOException {
age = age + 100;
s.defaultWriteObject();
s.writeObject("This is a object");
}

image-20231104100930112

但是如果将这个加100写到序列化后进行,就无法实现,无法覆盖序列化数据中age的值,

1
2
3
4
5
6
private void writeObject(java.io.ObjectOutputStream s) throws
IOException {
s.defaultWriteObject();
age = age + 100;
s.writeObject("This is a object");
}

image-20231104101020500

在理解学习时,又发现,并不是需要实例化传参,才给writeObject,

可以看到我重新定义一个Example类,然后直接给Person类中的元素赋值,

image-20240103165433126

然后执行结果为我们直接定义的值,

image-20240103165843165

这是因为Person p = new Person();并非只是实例化构造方法,而是整体类,包括其中的元素和方法。

所以当writeObject方法打印age时,就直接读取我们定义好的age,也就是直接读取public int age = Example.age;

而非一定需要构造函数传参。

image-20240103165815668

readObject

但是反序列化就有点意思了,如下代码

1
2
3
4
5
6
7
8
9
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
age = age + 100;
System.out.println(age);
s.defaultReadObject();
System.out.println(age);
String str = (String) s.readObject();
System.out.println(str);
}

image-20231104103730076

可以看到,age = age + 100后打印的结果是100,也就是在readObjectage = 0,也可以推出在writeObjectage=20,从下面的测试代码可以看出来。

image-20231216211543444

调试断点看看,可以看到readObject并没有调用Person构造函数中的值,而是读取变量定义时候的值,所以agename都默认为初始,一个为0,一个为null

image-20240103161643826

这也是因为readObject并不需要参数,没有和wirteObject一样读取实例化对象p。

所以readObject就只能获取变量默认值,而wirteObject可以读取变量的定义值。

image-20240103161934137

于是当尝试修改默认值,看看能否修改readObject的值,

但是发现readObject中的值还是默认值0,这是因为在调用s.defaultReadObject()反序列化之前,readObject获取的变量的值都只是默认值。

因为在反序列化之后,变量值又会为反序列化出来的值,这样就没必要再次读取当前变量的值浪费资源。

image-20240103172414854

但是如何使得readObject中的变量的值不再是默认值呢

虽然readObject不会读取变量的定义值,也就是不会读取这个变量的赋值操作,但是如果这个变量并不是当前类中的,而是从父类继承而来的,那子类最后获取的就是一个自带值的变量,而子类也无法访问父类的赋值过程,就算readObject不进行赋值操作,但是最后获得变量却是自带值的,某种意义上来说相当于在Person类中修改了age变量的初始值。

如下,nameage的值从Example类中继承,

image-20240103174707394

执行结果,readObjectage也为1000,因为其能访问的变量age是来自父类,自带值1000,无法不进行赋值,这里相当于在Person类中age的默认值为1000

image-20240103174759861

将父类修改回john20,再反序列化看看结构有没有什么变化。

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

import java.io.*;

class Example {
public String name = "John";
public int age = 20;
}

public class Person extends Example implements java.io.Serializable {
public Person() {
}
private void writeObject(java.io.ObjectOutputStream s) throws
IOException {
// System.out.println("age in writeObject is "+age);
s.defaultWriteObject();
s.writeObject("This is a object");
}
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// System.out.println("age in readObject is "+age);
s.defaultReadObject();
String str = (String) s.readObject();
System.out.println(str);
}

public static void main(String[] args) throws Exception {
Person p = new Person();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("per.ser"));
oos.writeObject(p);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("per.ser"));
ois.readObject();
}
}

SerializationDumper得出的结构,发现classdata中没有了nameage的值,这是因这两个变量是来自父类的,但是父类没有Serializable接口,所以无法进行序列化。

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
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 12 - 0x00 0c
Value - Ser_1.Person - 0x5365725f312e506572736f6e
serialVersionUID - 0x50 4f bf 8b 9e 3f bd e4
newHandle 0x00 7e 00 00
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 01
classdata
Ser_1.Person
values
objectAnnotation
TC_STRING - 0x74
newHandle 0x00 7e 00 02
Length - 16 - 0x00 10
Value - This is a object - 0x546869732069732061206f626a656374
TC_ENDBLOCKDATA - 0x78

image-20240103175852659

如果想要将父类Example也实例化,给父类也加上Serializable接口就行,

1
2
3
4
class Example implements java.io.Serializable{
public String name = "John";
public int age = 20;
}

就可以得到结构,

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
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 12 - 0x00 0c
Value - Ser_1.Person - 0x5365725f312e506572736f6e
serialVersionUID - 0x50 4f bf 8b 9e 3f bd e4
newHandle 0x00 7e 00 00
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_CLASSDESC - 0x72
className
Length - 13 - 0x00 0d
Value - Ser_1.Example - 0x5365725f312e4578616d706c65
serialVersionUID - 0xfe a1 35 71 e0 5e 93 ad
newHandle 0x00 7e 00 01
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
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
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 03
classdata
Ser_1.Example
values
age
(int)20 - 0x00 00 00 14
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 04
Length - 4 - 0x00 04
Value - John - 0x4a6f686e
Ser_1.Person
values
objectAnnotation
TC_STRING - 0x74
newHandle 0x00 7e 00 05
Length - 16 - 0x00 10
Value - This is a object - 0x546869732069732061206f626a656374
TC_ENDBLOCKDATA - 0x78

相比没给父类加上序列化接口,在superClassDesc中多了以下内容,发现它对这个父类进行一定的描述,名字长度,变量字段以及值。

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
className
Length - 13 - 0x00 0d
Value - Ser_1.Example - 0x5365725f312e4578616d706c65
serialVersionUID - 0xfe a1 35 71 e0 5e 93 ad
newHandle 0x00 7e 00 01
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
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
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 03
classdata
Ser_1.Example
values
age
(int)20 - 0x00 00 00 14
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 04
Length - 4 - 0x00 04
Value - John - 0x4a6f686e

Python反序列化

Python反序列化和Java、PHP有个显著的区别,就是Python的反序列化过程实际上是在执行一个基于栈的虚拟机。我们可以向栈上增、删对象,也可以执行一些指令,比如函数的执行等,甚至可以用这个虚拟机执行一个完整的应用程序。
所以,Python的反序列化可以立即导致任意函数、命令执行漏洞,与需要gadget的PHP和Java相比更加危险。
有关于Python反序列化的一些有趣的操作,

可以参考p神的另一篇文章《Code-Breaking中的两个Python沙箱》
总结一下,从危害上来看,Python的反序列化危害是最大的;从应用广度上来看,Java的反序列化是最常被用到的;从反序列化的原理上来看,PHP和Java是类似又不尽相同的。后文我将从一个非常简单的Gadget,即URLDNS入手,带大家深入理解反序列化漏洞的美妙。