java反序列化前置

JAVA反序列化前置

我们在调试RMI时会发现,其发送、接收的数据都是反序列化数据

其他不同语言都拥有此类方法(发序列化),且多少都拥有相关的漏洞

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

编程语言如果需要在网络上传递信息,通常会用到一些格式化数据:

  • JSON
  • XML

JSON和XML是通用数据交互格式,通常用于不同语言、不同环境下数据的交互,比如前端的JavaScript通过JSON和后端服务通信、微信服务器通过XML和公众号服务器通信。但这两个数据格式都有一个共同的问题:不支持复杂的数据类型。

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

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

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

接下来我们先学习readObject造成的反序列化漏洞

反序列化方法的对比

在此之前我们已经学习了php 和 python的反序列化 那么他们和java反序列化有什么差别呢

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

但Java相对PHP序列化更深入的地方在于,其提供了更加高级、灵活地方法 writeObject ,允许开发者在序列化流中插入一些自定义数据,进而在反序列化的时候能够使用 readObject 进行读取。

当然,PHP中也提供了一个魔术方法叫 __wakeup ,在反序列化的时候进行触发。看似Java的readObject 和PHP的 __wakeup 类似,但其实不全对,虽然都是在反序列化的时候触发,但他们解决的问题稍微有些差异。

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

php反序列化

这个没什么好讲的

PHP的序列化是开发者不能参与的,开发者调用 serialize 函数后,序列化的数据就已经完成了,你得到的是一个完整的对象,你并不能在序列化数据流里新增某一个内容,你如果想插入新的内容,只有将其保存在一个属性中。也就是说PHP的序列化、反序列化是一个纯内部的过程,而其 __sleep__wakeup 魔术方法的目的就是在序列化、反序列化的前后执行一些操作。

具体的代码我就不写了 大家应该都懂

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

java反序列化

Java反序列化的操作,很多是需要开发者深入参与的,所以你会发现大量的库会实现 readObjectwriteObject 方法,这和PHP中 __wakeup__sleep 很少使用是存在鲜明对比的。

classAnnotations

在序列化Java类的时候用到了一个类,叫 ObjectOutputStream 。这个类内部有一个方法 annotateClass ,ObjectOutputStream 的子类有需要向序列化后的数据里放任何内容,都可以重写这个方法,写入你自己想要写入的数据。然后反序列化时,就可以读取到这个信息并使用。

所以,我们在分析序列化数据时看到的 classAnnotations ,实际上就是 annotateClass 方法写入的内容

objectAnnotation

Java在序列化时一个对象,将会调用这个对象中的 writeObject 方法,参数类型是ObjectOutputStream ,开发者可以将任何内容写入这个stream中;反序列化时,会调用 readObject ,开发者也可以从中读取出前面写入的内容,并进行处理。

例:

package org.vulhub.Ser;  

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Person implements Serializable {
public String name;
public int age;

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


private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeObject("This is an object");
}


private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
String message = (String) s.readObject();
System.out.println(message);
}
}

这里在执行完默认的 s.defaultWriteObject() 后,我向stream里写入了一个字符串 This is a object 。我们用上一章讲的工具SerializationDumper查看此时生成的序列化数据:

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 21 - 0x00 15
Value - org.vulhub.Ser.Person -
0x6f72672e76756c6875622e5365722e506572736f6e
serialVersionUID - 0xf1 ad b2 c3 a9 83 d4 5c
newHandle 0x00 7e 00 00
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
fieldCount - 3 - 0x00 03
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
2:
Object - L - 0x4c
fieldName
Length - 8 - 0x00 08
Value - password - 0x70617373776f7264
className1
TC_REFERENCE - 0x71
Handle - 8257537 - 0x00 7e 00 01
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 02
classdata
org.vulhub.Ser.Person
values
age
(int)22 - 0x00 00 00 16
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 3 - 0x00 03
Value - Bob - 0x426f62
password
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 04
Length - 6 - 0x00 06
Value - secret - 0x736563726574
objectAnnotation
TC_STRING - 0x74
newHandle 0x00 7e 00 05
Length - 16 - 0x00 10
Value - This is a object - 0x546869732069732061206f626a656374
TC_ENDBLOCKDATA - 0x78

可见,我们写入的字符串 This is a object 被放在 objectAnnotation 的位置。 在反序列化时,我读取了这个字符串,并将其输出:

image-20240523152213089

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

python反序列化

Python反序列化和Java、PHP有个显著的区别,就是Python的反序列化过程实际上是在执行一个基于栈的虚拟机。我们可以向栈上增、删对象,也可以执行一些指令,比如函数的执行等,甚至可以用这个虚拟机执行一个完整的应用程序。

所以,Python的反序列化可以立即导致任意函数、命令执行漏洞,与需要gadget的PHP和Java相比更加危险

pickle反序列化就不多说了

python>java>php

总结

总结一下,从危害上来看,Python的反序列化危害是最大的;从应用广度上来看,Java的反序列化是最常被用到的;从反序列化的原理上来看,PHP和Java是类似又不尽相同的。