终于开始反序列化开篇,之前再RMI的攻击和流程中发现,大多数的数据发送和接收都是反序列化数据。
那么,为什么反序列化常常会带来安全隐患?
一门成熟的语言,如果需要在网络上传递信息,通常会用到一些格式化数据,
比如:
但这两个数据格式都有一个共 同的问题:不支持复杂的数据类型。 大多数处理方法中,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
,开发者也可以从读取出前面的写入的内容,再进行处理。
举个例子,展示writeobject
和readobject
的作用,
先写个Person
类,重写writeobject
和readobject
,
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(); s.writeObject("This is a object"); }
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); 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(); } }
|
运行结果,
最后用工具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
类的name
和age
的后面。 在反序列化时,读取了这个字符串,并将其输出。
这个特性就让Java
的开发变得非常灵活。比如后面将会讲到的HashMap
,其就是将Map
中的所有键、 值都存储在 objectAnnotation
中,而并不是某个具体属性里。
重写writeObject
和readObject
对于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"); }
|
但是如果将这个加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"); }
|
在理解学习时,又发现,并不是需要实例化传参,才给writeObject,
可以看到我重新定义一个Example类,然后直接给Person类中的元素赋值,
然后执行结果为我们直接定义的值,
这是因为Person p = new Person();
并非只是实例化构造方法,而是整体类,包括其中的元素和方法。
所以当writeObject
方法打印age
时,就直接读取我们定义好的age
,也就是直接读取public int age = Example.age;
而非一定需要构造函数传参。
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); }
|
可以看到,age = age + 100
后打印的结果是100
,也就是在readObject
中age = 0
,也可以推出在writeObject
中age=20
,从下面的测试代码可以看出来。
调试断点看看,可以看到readObject
并没有调用Person
构造函数中的值,而是读取变量定义时候的值,所以age
和name
都默认为初始,一个为0
,一个为null
。
这也是因为readObject
并不需要参数,没有和wirteObject
一样读取实例化对象p。
所以readObject
就只能获取变量默认值,而wirteObject
可以读取变量的定义值。
于是当尝试修改默认值,看看能否修改readObject的值,
但是发现readObject
中的值还是默认值0,这是因为在调用s.defaultReadObject()
反序列化之前,readObject
获取的变量的值都只是默认值。
因为在反序列化之后,变量值又会为反序列化出来的值,这样就没必要再次读取当前变量的值浪费资源。
但是如何使得readObject
中的变量的值不再是默认值呢
虽然readObject
不会读取变量的定义值,也就是不会读取这个变量的赋值操作,但是如果这个变量并不是当前类中的,而是从父类继承而来的,那子类最后获取的就是一个自带值的变量,而子类也无法访问父类的赋值过程,就算readObject
不进行赋值操作,但是最后获得变量却是自带值的,某种意义上来说相当于在Person类中修改了age
变量的初始值。
如下,name
和age
的值从Example类中继承,
执行结果,readObject
中age
也为1000
,因为其能访问的变量age
是来自父类,自带值1000
,无法不进行赋值,这里相当于在Person
类中age
的默认值为1000
。
将父类修改回john
和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
| 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 {
s.defaultWriteObject(); s.writeObject("This is a object"); } private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
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
中没有了name
和age
的值,这是因这两个变量是来自父类的,但是父类没有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
|
如果想要将父类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入手,带大家深入理解反序列化漏洞的美妙。