java安全(五)java反序列化

admin 2022年4月23日02:37:49评论37 views字数 10046阅读33分29秒阅读模式

目录:

1. 序列化
2.反序列化
3.反序列化方法的对比
4.php反序列化
5.java反序列化
6.objectinputstream、objectoutputstream
7.java.io.serializable
8.java.io.externalizable.java:
9.自定义序列化(writeobject)和反序列化(reado


 

1. 序列化







在调用RMI时,发现接收发送数据都是反序列化数据.

例如JSON和XML等语言,在网络上传递信息,都会用到一些格式化数据,大多数处理方法中,JSON和XML支持的数据类型就是基本数据类型,整型、浮点型、字符串、布尔等,如果开发者希望在传输数据的时候直接传输一个对象,那么就不得不想办法扩展基础的JSON(XML)语法。比如,Jackson和Fastjson这类序列化库,在JSON(XML)的基础上进行改造,通过特定的语法来传递对象.

RMI使用java等语言内置的序列化方法,将一个对象转化成一串二进制数据进行传输

 2.反序列化



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

在Java中实现对象反序列化非常简单,实现**java.io.Serializable(内部序列化)java.io.Externalizable(外部序列化)接口**即可被序列化,其中java.io.Externalizable接口只是实现了java.io.Serializable接口。

反序列化类对象时有如下**限制:**
**1.被反序列化的类必须存在。
2. serialVersionUID值必须一致。**


除此之外,反序列化类对象是不会调用该类构造方法的,因为在反序列化创建类实例时使用了sun.reflect.ReflectionFactory.newConstructorForSerialization创建了一个反序列化专用的Constructor(反射构造方法对象),使用这个特殊的Constructor可以绕过构造方法创建类实例(前面章节讲sun.misc.Unsafe 的时候我们提到了使用allocateInstance方法也可以实现绕过构造方法创建类实例)。

 3.反序列化方法的对比



在接触Java反序列化之前,相比大家多少都了解过其他语言的反序列化漏洞,其中极为经典的要数PHP
和Python。
那么,Java的反序列化,究竟和PHP、Python的反序列化有什么异同?
Java的反序列化和PHP的反序列化其实有点类似,他们都只能将一个对象中的属性按照某种特定的格式
生成一段数据流,在反序列化的时候再按照这个格式将属性拿回来,再赋值给新的对象。
但Java相对PHP序列化更深入的地方在于,其提供了更加高级、灵活地方法 writeObject ,允许开发者
在序列化流中插入一些自定义数据,进而在反序列化的时候能够使用 readObject 进行读取。
当然,PHP中也提供了一个魔术方法叫 __wakeup ,在反序列化的时候进行触发。很多人会认为Java的 readObject 和PHP的 __wakeup 类似,但其实不全对,虽然都是在反序列化的时候触发,但他们解决
的问题稍微有些差异。
Java设计 **readObject** 的思路和PHP的 **__wakeup** 不同点在于:
 **readObject 倾向于解决“反序列化时如
何还原一个完整对象”这个问题,而PHP的 __wakeup 更倾向于解决“反序列化后如何初始化这个对象”的
问题。**

4.PHP反序列化




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


一个非常典型的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 就是一个数据库连接,我就需要编写 __wakeup 方法:


<?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反序列化漏洞,都并不是由反序列化导致的,只是通过反序列化可以
控制对象的属性,进而在后续的代码中进行危险操作。


5.Java反序列化




在Java中实现对象反序列化非常简单,实现**java.io.Serializable(内部序列化)java.io.Externalizable(外部序列化)接口**即可被序列化,其中java.io.Externalizable接口只是实现了java.io.Serializable接口。

反序列化类对象时有如下**限制:**
**1.被反序列化的类必须存在。
2. serialVersionUID值必须一致。**


除此之外,反序列化类对象是不会调用该类构造方法的,因为在反序列化创建类实例时使用了sun.reflect.ReflectionFactory.newConstructorForSerialization创建了一个反序列化专用的Constructor(反射构造方法对象),使用这个特殊的Constructor可以绕过构造方法创建类实例(前面章节讲sun.misc.Unsafe 的时候我们提到了使用allocateInstance方法也可以实现绕过构造方法创建类实例)。


使用反序列化方式创建类实例代码片段:


package com.anbai.sec.serializes;
import sun.reflect.ReflectionFactory;
import java.lang.reflect.Constructor;
/** * 使用反序列化方式在不调用类构造方法的情况下创建类实例 * https://www.iteye.com/topic/850027 */public class ReflectionFactoryTest {
public static void main(String[] args) { try { // 获取sun.reflect.ReflectionFactory对象 ReflectionFactory factory = ReflectionFactory.getReflectionFactory();
// 使用反序列化方式获取DeserializationTest类的构造方法 Constructor constructor = factory.newConstructorForSerialization( DeserializationTest.class, Object.class.getConstructor());
// 实例化DeserializationTest对象 System.out.println(constructor.newInstance()); } catch (Exception e) { e.printStackTrace(); } }
}


输出

java安全(五)java反序列化


6.ObjectInputStream、ObjectOutputStream


java.io.ObjectOutputStream类最核心的方法是writeObject方法,即序列化类对象。

java.io.ObjectInputStream类最核心的功能是readObject方法,即反序列化类对象。

所以,只需借助ObjectInputStream和ObjectOutputStream类我们就可以实现类的序列化和反序列化功能了。

7.java.io.Serializable




java.io.Serializable是一个空的接口,我们不需要实现java.io.Serializable的任何方法,代码如下:

```

public interface Serializable {}

```


您可能会好奇我们实现一个空接口有什么意义?其实实现java.io.Serializable接口仅仅只用于标识这个类可序列化。实现了java.io.Serializable接口的类原则上都需要生产一个serialVersionUID常量,反序列化时如果双方的serialVersionUID不一致会导致InvalidClassException 异常。如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID值。

DeserializationTest.java测试代码如下:


package com.anbai.sec.serializes;
import java.io.*;import java.util.Arrays;
/** * Creator: yz * Date: 2019/12/15 */public class DeserializationTest implements Serializable {


private String username;
private String email;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public static void main(String[] args) { ByteArrayOutputStream baos = new ByteArrayOutputStream();
try { // 创建DeserializationTest类,并类设置属性值 DeserializationTest t = new DeserializationTest(); t.setUsername("yz"); t.setEmail("[email protected]");
// 创建Java对象序列化输出流对象 ObjectOutputStream out = new ObjectOutputStream(baos);
// 序列化DeserializationTest类 out.writeObject(t); out.flush(); out.close();
// 打印DeserializationTest类序列化以后的字节数组,我们可以将其存储到文件中或者通过Socket发送到远程服务地址 System.out.println("DeserializationTest类序列化后的字节数组:" + Arrays.toString(baos.toByteArray()));
// 利用DeserializationTest类生成的二进制数组创建二进制输入流对象用于反序列化操作 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
// 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象 ObjectInputStream in = new ObjectInputStream(bais);
// 反序列化输入流数据为DeserializationTest对象 DeserializationTest test = (DeserializationTest) in.readObject(); System.out.println("用户名:" + test.getUsername() + ",邮箱:" + test.getEmail());
// 关闭ObjectInputStream输入流 in.close(); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } }
}


输出:

java安全(五)java反序列化

核心逻辑其实就是使用ObjectOutputStream类的writeObject方法序列化DeserializationTest类,使用ObjectInputStream类的readObject方法反序列化DeserializationTest类而已。

**简化后的代码片段如下:**

// 序列化DeserializationTest类ObjectOutputStream out = new ObjectOutputStream(baos);out.writeObject(t);
// 反序列化输入流数据为DeserializationTest对象ObjectInputStream in = new ObjectInputStream(bais);DeserializationTest test = (DeserializationTest) in.readObject();


ObjectOutputStream序列化类对象的主要流程是首先判断序列化的类是否重写了**writeObject方法**,如果重写了就调用序列化对象自身的**writeObject方法**序列化,序列化时会先写入类名信息,其次是写入成员变量信息(通过反射获取所有不包含被**transient修饰**的变量和值)。

8.java.io.Externalizable.java:




public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}


**ExternalizableTest.java测试代码如下:**

package com.anbai.sec.serializes;
import java.io.*;import java.util.Arrays;
/** * Creator: yz * Date: 2019/12/15 */package com.anbai.sec.serializes;
import java.io.*;import java.util.Arrays;
/** * Creator: yz * Date: 2019/12/15 */public class ExternalizableTest implements java.io.Externalizable {
private String username;
private String email;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
@Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(username); out.writeObject(email); }
@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { this.username = (String) in.readObject(); this.email = (String) in.readObject(); }
public static void main(String[] args) { ByteArrayOutputStream baos = new ByteArrayOutputStream();
try { // 创建ExternalizableTest类,并类设置属性值 ExternalizableTest t = new ExternalizableTest(); t.setUsername("yz"); t.setEmail("[email protected]");
ObjectOutputStream out = new ObjectOutputStream(baos); out.writeObject(t); out.flush(); out.close();
// 打印ExternalizableTest类序列化以后的字节数组,我们可以将其存储到文件中或者通过Socket发送到远程服务地址 System.out.println("ExternalizableTest类序列化后的字节数组:" + Arrays.toString(baos.toByteArray())); System.out.println("ExternalizableTest类反序列化后的字符串:" + new String(baos.toByteArray()));
// 利用DeserializationTest类生成的二进制数组创建二进制输入流对象用于反序列化操作 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
// 通过反序列化输入流创建Java对象输入流(ObjectInputStream)对象 ObjectInputStream in = new ObjectInputStream(bais);
// 反序列化输入流数据为ExternalizableTest对象 ExternalizableTest test = (ExternalizableTest) in.readObject(); System.out.println("用户名:" + test.getUsername() + ",邮箱:" + test.getEmail());
// 关闭ObjectInputStream输入流 in.close(); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } }
}

输出:

java安全(五)java反序列化

两者之间没有多大差别

9.自定义序列化(writeObject)和反序列化(readObject)




实现了java.io.Serializable接口的类,还可以定义如下方法(反序列化魔术方法),这些方法将会在类序列化或反序列化过程中调用:

 1. private void writeObject(ObjectOutputStream oos),自定义序列化。
 2. private void readObject(ObjectInputStream ois),自定义反序列化。
 3. private void readObjectNoData()。
 4. protected Object writeReplace(),写入时替换对象。
 5. protected Object readResolve()。

具体的方法名定义在java.io.ObjectStreamClass#ObjectStreamClass(java.lang.Class<?>),其中方法有详细的声明。

**序列化时可自定义的方法示例代码:**

public class DeserializationTest implements Serializable {
/** * 自定义反序列化类对象 * * @param ois 反序列化输入流对象 * @throws IOException IO异常 * @throws ClassNotFoundException 类未找到异常 */ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { System.out.println("readObject...");
// 调用ObjectInputStream默认反序列化方法 ois.defaultReadObject();
// 省去调用自定义反序列化逻辑... }
/** * 自定义序列化类对象 * * @param oos 序列化输出流对象 * @throws IOException IO异常 */ private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject();
System.out.println("writeObject..."); // 省去调用自定义序列化逻辑... }
private void readObjectNoData() { System.out.println("readObjectNoData..."); }
/** * 写入时替换对象 * * @return 替换后的对象 */ protected Object writeReplace() { System.out.println("writeReplace....");
return null; }
protected Object readResolve() { System.out.println("readResolve....");
return null; }
}

当我们对DeserializationTest类进行序列化操作时,会自动调用(反射调用)该类的writeObject(ObjectOutputStream oos)方法,对其进行反序列化操作时也会自动调用该类的readObject(ObjectInputStream)方法,也就是说我们可以通过在待序列化或反序列化的类中定义readObject和writeObject方法,来实现自定义的序列化和反序列化操作,当然前提是,被序列化的类必须有此方法,并且方法的修饰符必须是private。


原文始发于微信公众号(b1gpig信息安全):java安全(五)java反序列化

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月23日02:37:49
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   java安全(五)java反序列化http://cn-sec.com/archives/934098.html

发表评论

匿名网友 填写信息