ShokaX

Java反序列化(2)-反序列化基础

发布于 字数统计 4.1k 字 阅读时长 14 分钟

Java反序列化(2)-反序列化基础

发布于 字数统计 4,058 阅读时长 21 分钟

简介

PS:跟着大佬学习的,大佬讲得是针不错。https://www.bilibili.com/video/BV16h411z7o9?p=1

Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存或文件中,实现跨平台通讯和持久化存储。ObjectOutputStream类的 writeObject() 方法可以实现序列化。反序列化则指把字节序列恢复为 Java 对象的过程,相应的,ObjectInputStream 类的 readObject() 方法用于反序列化。

使用场景

  • 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候。
  • 当你想用套接字在网络上传送对象的时候。
  • 当你想通过 RMI 传输对象的时候。

RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个 Java 虚拟机的对象调用运行在另一个 Java 虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

java序列化和反序列化的实现

JDK类库中序列化和反序列化API

JDK类库中存在序列化和反序列化(原生反序列化)的api,同时fastjson、jackson等等第三方组件也提供的反序列化的api。这里我们只讨论原生的反序列化。

java.io.ObjectOutputStream:表示对象输出流;它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中;

java.io.ObjectInputStream:表示对象输入流;它的readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回;

实现序列化的要求

序列化的目标是一个对象,而只有实现了Serializable(空接口)或Externalizable接口的类的对象才能被序列化,否则序列化会失败。
如图,我们这个Student对象实现了Serializable接口,后续实例化这个对象时,就能够对其进行序列化和反序列化了。

PS:Java的序列化会将一个类包含的引用中所有的成员变量保存下来(深度复制),所以里面的引用类型必须也要实现java.io.Serializable接口。

image-20220918151840325

序列化和反序列化实例

首先我们这里把序列化和反序列化的代码补完。
可以看到,我们定义了两个方法,
第一个Ser方法接受一个对象,然后把这个对象序列化存储到student.ser文件中。
第二个UnSer方法则是把指定文件的内容进行反序列化,还原对象,并返回。

image-20220918151859743

这里我们就针对student这个对象进行了序列化和反序列化。

image-20220918151910709

image-20220918151917526

readObject和writeObject方法(重点)

PS

!!序列化的目标是对象,而一个对象在实例化后只有成员变量会被存储。也就是说和php一样,方法是不会存储的。

我们前面调用的readObject和writeObject方法都是ObjectInputStream它们的方法。

但实际上针对不同的对象,我们可能会想要不同的序列化和反序列化策略。如果都用这个ObjectInputStream里面的方法

明显是做不到多样化策略的。所以我们可以在目标对象中重写这两个方法,实现我们想要的策略。

比如上面,我们在Student这个类中重写readObject这个方法,那么当这个对象被反序列化的时候,就会执行相应的策略。

image-20220918152012864

如图,当我们执行反序列化的时候,因为Student这个对象重写了readObject方法,就会调用这个方法进行对象的操作。

image-20220918152024348

反序列化漏洞

只要服务端反序列化数据,那么客户端传递进来的类的readObject中的代码就会自动执行,从而给予攻击者在服务器上运行代码的能力。

这句话怎么理解呢?很简单,以上面为例,当我们传入的对象是Student这个对象时,服务器发现本地存在Student这个类,然后又发现这个类重写了

readObject,因此就会执行Student这个类的readObject里的代码。

也就是说当目标服务器接受我们的一个对象,并且把这个对象给反序列化时,如果这个对象的类在目标服务器也存在。并且

重写了readObject,而恰好的是这个readObject中又存在危险命令执行函数的时候,就存在RCE了。但显然,这么多条件属实苛刻,现实中基本不可能存在这样的情况,

即使存在,也是开发人员写的测试类,这个类我们也很难得知。自然也无法利用了。

现实中我们的反序列化漏洞更多是以各种第三方组件来实现的,也就是说一些第三方组件中的类在反序列化的时候会存在RCE的情况。

这里我们暂时不需要想太多,等到后续我们分析一个链的时,大概就很清晰了。

反序列化漏洞可能存在的情况:

PS:入口类==我们可控的输入的类,比如在上面中,我们的入口类就是Student这个类。

  • 入口类的readObject直接调用危险方法。(前面的例子就是这样,但现实基本不存在)
  • 入口类参数中包含可控类,这个类有危险方法,入口类在readObject时触发这个类的危险方法。(分析URL DNS链就懂了)
  • 入口类参数中包含可控类,这个类又调用其他有危险方法的类,入口类在readObject时调用。(其实就是利用链的问题了,没啥好说的)

自建Servlet模拟反序列化漏洞的利用过程

Servlet和存在漏洞的类的搭建

这里为了更清晰反序列化漏洞的原理,我们自己搭建一个存在漏洞的Servlet,然后把恶意对象传给它触发漏洞。

image-20220919175256364

然后创建一个测试类,这个测试类是在Servlet上的,存在一个危险函数。

image-20220919175305474

Payload构造和EXP

很明显,上面Servlet接收请求体,然后把请求体给反序列化。而目标服务器上存在一个存在漏洞的类User,也就是说我们只要把User这个类放到请求体
传给Servlet,就能进行RCE了。

image-20220919175330907

image-20220919175335972

但此时执行payload发现报错。

image-20220919175354609

百度一哈,serialVersionUID参数主要是用来验证Java序列化版本一致性的。因此问题就是序列化的版本问题,这里解决方法就是在本地序列化恶意

对象时指定这个UID的值,让它和服务端一致就行。

image-20220919175403901

最后执行EXP,成功弹Calc

image-20220919175416255