JAVA反序列化(1)

放暑假了,又来学习了。之前讲RMI反序列化,这篇正式开始JAVA反序列化的学习

参考文章:《JAVA安全漫谈》 JAVA反序列化安全入门

序列化与反序列化

序列化和反序列化是用来干嘛的

我们需要在网络上去传输数据,可能会用XML、YAML、JSON等数据交互格式。但是它们比较简单,对于比较复杂的数据难以表示清楚。比如想要表示一个对象,如果用JSON或者XML,定义起来就比较费劲,所以像Fastjson这类序列化库就是在JSON(XML)的基础上进行改造,通过特定语法传递对象,而像这样RMI就是直接通过JAVA内置的序列化方法,将对象转换成二进制进行传输。一旦有涉及到将对象转换成一个指定格式的数据 再从数据中还原出对象的这个过程,就有可能造成一系列安全问题

反序列化方法对比

之前我们也有说过PHP反序列化的东西

Java的反序列化和PHP的反序列化有点类似,都只能将一个对象中的属性按照某种特定的格式生成一段数据流,在反序列化的时候再按照这个格式将属性拿回来,再赋值给新的对象。

JAVA提供了writeObject来允许开发者在序列化六中插入自定义数据,再在反序列化的时候用readObject进行读取。PHP则提供了一个魔术方法__wakeup,在反序列化的时候触发。

1
Java设计readObject的思路和PHP的__wakeup不同点在于:readObject倾向于解决“反序列化时如何还原一个完整对象”这个问题,而PHP的__wakeup更倾向于解决“反序列化后如何初始化这个对象”的问题。

PHP

PHP的反序列化开发者是无法参与的,调用serialize之后数据就完成了,这就是一个完整的对象。序列化和反序列化都是春内部的内容,只有一些魔术方法在反序列化前后做一些操作,漏洞通常是通过反序列化为入口来控制还原出的对象的属性,从而控制一些危险函数的参数,来进行利用。

P牛这里给出了一个数据库连接的例子

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
<?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的作用是在反序列化之后,执行一些初始化操作。因为资源类型的对象(数据库连接)不会默认写入序列化数据,因此要在反序列化时调用__wakeup进行连接。

在php中大部分的反序列化漏洞都不是由反序列化造成的,只是通过反序列化可以控制对象的属性,而在代码的执行链路中又有比如eval()、exec()等危险函数,就可以构造数据来调用到这些危险函数进行危险操作

JAVA

JAVA的序列化操作很多时候需要开发者参与。大量的库都会实现readObject、writeObject方法。

JAVA在序列化时会调用writeObject,这个方法接收ObjectOutputStream类,开发者可以将任何内容写入这个Stream中,反序列化时,会调用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
35
36
37
38
39
40
41
package org.example;

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 message = (String) s.readObject();
System.out.println(message);
}

public static void main(String[] args) {
// 创建一个 Person 对象
Person person = new Person("John Doe", 25);

// 序列化 Person 对象
try {
FileOutputStream fileOut = new FileOutputStream("person.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(person);
out.close();
fileOut.close();
System.out.println("Person 对象已序列化到 person.ser 文件");
} catch (IOException e) {
e.printStackTrace();
}
}
}

执行完main会生成一个person.ser在这个项目的根目录下,然后用SerializationDumper工具来查看此时生成的序列化数据,这里我用的是v1.14版本的,JDK版本要稍微高一点我用JDK11都不行,最后用的JDK17才运行成功

1
java -jar .\SerializationDumper-v1.14.jar -r ./person.ser > person.txt
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 - 18 - 0x00 12
Value - org.example.Person - 0x6f72672e6578616d706c652e506572736f6e
serialVersionUID - 0x8b 3e 12 c3 8f b4 5e b5
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
org.example.Person
values
age
(int)25 - 0x00 00 00 19
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 8 - 0x00 08
Value - John Doe - 0x4a6f686e20446f65
objectAnnotation
TC_STRING - 0x74
newHandle 0x00 7e 00 04
Length - 16 - 0x00 10
Value - This is a object - 0x546869732069732061206f626a656374
TC_ENDBLOCKDATA - 0x78

就可以看到在objectAnnotation里面有我们调用writeobject写入的"This is a object"

Python

Python反序列化的过程实际上是在执行一个基于栈的虚拟机。如果可以控制python反序列化的过程就可以去添加一些指令比如一些函数的执行,或是一个完整的应用程序,构造payload完成远程RCE。