序列化与反序列化
引 言
反序列漏洞在安全领域可谓红极一时, 不同语言都拥有此类方法, 且多少都拥有相关的漏洞
首先一门成熟的语言, 如果需要在网络上传递信息, 通常会用到一些格式化数据, 比如 : JSON、XML, 这满足了不同情况下前端和后端的通信需求, 但是这两个数据格式都有一个通用的问题就是不支持复杂的数据类型
基于上面的问题, 也产生了一些特殊的工具, 比如 jackson 和 Fastjson 这类序列化库, 在 JSON(XML) 的基础上进行改造, 通过特定的语法来传递对象, 又或者 RMI 直接使用 Java 等语言内置的序列化方法, 将一个对象转换成一串二进制数据进行传输.
不管是 Jackson 、Fastjson 还是编程语言内置的序列化方法, 一旦涉及到序列化与反序列化数据, 就可能会涉及到安全问题, 但是首先需要明确的是, "反序列化漏洞" 是指对一类漏洞的泛指, 而不是专指某种反序列化方法导致的漏洞
反序列化方法的对比
我主要研究 Java 和 Python, 因此对于 PHP 只是理解即可
首先, Java 的反序列化和 PHP 的反序列化其实有点类似, 他们都是只能将一个对象中的属性按照某种特定的格式生成一段数据流, 在反序列化的时候再按照这个格式将属性赋值给新的对象.
但 Java 相对 PHP 序列化更深入的地方在于, 其提供了更加高级、灵活的方法 writeObject()
, 这允许开发者在序列化流中插入一些自定义数据 , 进而在反序列化的时候可以使用 readObject()
读取使用.
当然 PHP 也提供了一个魔术方法叫 __wakeup()
, 会在反序列化的时候进行触发.
首先两者触发时间一致, 都是在反序列化的时候触发, 但是二者处理数据的方式有差异, Java 的 readObject()
的倾向于解决反序列化时如何还原一个完整对象, 而 PHP 的 __weakup()
更倾向于解决反序列化后如何初始化这个对象
PHP 反序列化
首先来看一段标准的 PHP 反序列化后的数据
- Code
- 反序列化后的数据
- Banana
<?php
class Ctf{
public $flag;
public $name='cxk';
public $age='10';
}
class Flag{
public $flag;
}
$ctfer=new Ctf();
$flag=new Flag();
//实例化一个对象
$ctfer->flag=$flag;
$ctfer->name='Sch0lar';
$ctfer->age='18';
echo serialize($ctfer);
?>
O:3:"Ctf":3:{s:4:"flag";O:4:"Flag":1:{s:4:"flag";N;}s:4:"name";s:7:"Sch0lar";s:3:"age";s:2:"18";}
- O代表对象,因为我们序列化的是一个对象;序列化数组的话则用A来表示
- 3代表类的名字长三个字符
- Ctf 是类名
- 3代表这个类里有三个属性(三个变量)
- s代表字符串 4代表属性名的长度 name
- s:7:"Sch0lar"; 字符串,属性长度,属性值
This is a banana 🍌
从上面可以看到, 在 PHP 序列化后的数据中, 只是描述对象的值是什么
我们知道 PHP 的序列化操作开发者是无法参与的, 通过调用 serialize()
就会产生序列化数据, 我们就可以得到一个完整的对象, 并不能在序列化数据流中新增某一个内容, 如果我们想插入一些内容, 只能将其保存在对象的属性中, 也就是说 PHP 的序列化、反序列操作都是一个纯内部操作, 而 __sleep()
__weakup()
这类方法的目的是在序列化、反序列化操作前后执行一些操作
经典的 PHP 序列化操作例子, 就是含有资源类型的 PHP 类, 如: 数据库连接:
<?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
是一个数据库连接, 就需要编写 __weakup()
<?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();
}
?>
这里的 __weakup()
的作用是在反序列化之后拿到 Connection 对象, 执行 connect()
函数连接数据库
__weakup()
作用是在反序列化之后执行一些操作, 但其实我们很少利用序列化数据传递资源类型的对象, 而其他类型的随心, 在反序列化的时候就已经赋值了.所以我们可以发现, PHP 的反序列化漏洞, 很少是由
__weakup()
触发的, 通常触发在析构函数__destruct()
, 其实大部分PHP反序列化漏洞,都并不是由反序列化导致的,只是通过反序列化可以 控制对象的属性,进而在后续的代码中进行危险操作。
Java 反序列化
Java 反序列化的操作, 很多是需要开发者进行参与的, 所以我们可以发现大量的库会实现 readObject()
和 writeObject()
ava 在序列化一个对象时, 会调用对象中的 writeObject(ObjectOutputStream)
开发者可以将任何内容写入这个 stream 中, 在反序列化时, 会通过调用 readObject()
开发者也可以借此读取处前面写入的数据进行处理
- JAvA
- 序列数据分析
package com.jtz;
import java.io.*;
public class Person implements Serializable {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeObject("This is a object");
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
String message = (String) s.readObject();
System.out.println(message);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person jtz = new Person("JTZ", 18);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(jtz);
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
objectInputStream.readObject();
}
}
在上述代码中, 在执行完默认的 s.defaultWriteObject()
之后, 我们向 stream 中写入了一个字符串 This is a object
, 我们使用 SerializationDumper 分析对应的序列化数据
┌──(root㉿kali)-[~/Java]
└─# java -jar SerializationDumper-v1.13.jar "ACED00057372000E636F6D2E6A747A2E506572736F6E720927926FBC69D20300024900036167654C00046E616D657400124C6A6176612F6C616E672F537472696E673B7870000000127400034A545A740010546869732069732061206F626A65637478"
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 14 - 0x00 0e
Value - com.jtz.Person - 0x636f6d2e6a747a2e506572736f6e
serialVersionUID - 0x72 09 27 92 6f bc 69 d2
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
com.jtz.Person
values
age
(int)18 - 0x00 00 00 12
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 3 - 0x00 03
Value - JTZ - 0x4a545a
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
的位置, 并且我们在反序列的时候也读取了这个字符串
这个特性让 Java 的开发变得非常的灵活, 值得一提的是 HashMap , 其就是将 Map 中的所有键值存储在 objectAnnotation
中的, 而不是某个具体的属性
Python 反序列化
Python 的反序列化和 Java 、PHP有个明显的区别, 就是 Python 的反序列过程实际上是在执行一个基于栈的虚拟机, 我们可以向栈上增、删对象, 也可以执行一些指令, 比如函数的执行, 甚至可以利用这个虚拟机执行一个完整的应用程序, 所以 Python 的反序列化可以立即导致任意函数、命令执行漏洞, 相比于 PHP 和 Java 更加危险.