Java反序列化基础篇-01-反序列化概念与利用
2022-5-19 22:5:56 Author: www.freebuf.com(查看原文) 阅读量:15 收藏

0x01 前言

写这篇文章,是想在 Java 反序列化基础的地方再多过几遍,毕竟万丈高楼平地起。

我是非常不建议新手们一上来就开始分析 CC 链,就开始看 shiro 的 POC 的,我觉得还是得先打好基础。

人人都会走弯路,但是要尽量少走弯路,这也是我写这篇文章由衷的目的,希望师傅们学习过程中可以少走弯路。

先学好基础,为后续的 Java 安全学习做好铺垫。

这里强推大家先去了解一下 Java 的反射是什么,力推 "Java 安全漫谈系列",师傅们可以加入 "Java 代码审计" 的知识星球。

0x02 序列化与反序列化

1. 什么是序列化与反序列化

之前也刷了 Port,对于序列化和反序列化还是清楚的,这里不厌其烦,再写一遍,也让自己再过一遍。

序列化:对象 -> 字符串
反序列化:字符串 -> 对象

2. 为什么我们需要序列化与反序列化

一开始学的时候还是不知道的。

序列化与反序列化的设计就是用来传输数据的。

当两个进程进行通信的时候,可以通过序列化反序列化来进行传输。

序列化的好处

(1) 能够实现数据的持久化,通过序列化可以把数据永久的保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。

(2) 利用序列化实现远程通信,在网络上传送对象的字节序列。

序列化与反序列化应用的场景

(1) 想把内存中的对象保存到一个文件中或者是数据库当中。
(2) 用套接字在网络上传输对象。
(3) 通过 RMI 传输对象的时候。

3. 几种创建的序列化和反序列化协议

XML&SOAP
JSON
Protobuf

当今 Java 原生当中的序列化与反序列化其实用的比较少吧,但是我们最开始讲起的话还是从原生开始讲起。

0x03 序列化与反序列化代码实现

先创建几个文件,这里要避免踩个坑

踩坑小记 —— IDEA 右键新建时没有 Java Class 选项

异常现象如图

image

解决

image

还有一种情况是你以数字命名文件夹了,比如 "001" 这种,是不行的

1. 代码展示,便于大家 Copy 省时间

  • 类文件:Person.java

package src;  // 修改成自己的 Package 路径
  
import java.io.Serializable;  
  
public class Person implements Serializable {  
  
    private String name;  
 private int age;  
  
 public Person(){  
  
    }  
    // 构造函数  
 public Person(String name, int age){  
        this.name = name;  
 this.age = age;  
 }  
  
    @Override  
 public String toString(){  
        return "Person{" +  
                "name='" + name + '\'' +  
                ", age=" + age +  
                '}';  
 }  
}
  • 序列化文件 SerializationTest.java

package src;  
  
  
import java.io.FileOutputStream;  
import java.io.IOException;  
import java.io.ObjectOutput;  
import java.io.ObjectOutputStream;  
  
public class SerializationTest {  
    public static void serialize(Object obj) throws IOException{  
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
 oos.writeObject(obj);  
 }  
  
    public static void main(String[] args) throws Exception{  
        Person person = new Person("aa",22);  
 		System.out.println(person);  
  		serialize(person);  
 }  
}
  • 反序列化文件 UnserializeTest.java

package src;  
  
import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.ObjectInputStream;  
  
public class UnserializeTest {  
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
 Object obj = ois.readObject();  
 return obj;  
 }  
  
    public static void main(String[] args) throws Exception{  
        Person person = (Person)unserialize("ser.bin");  
 System.out.println(person);  
 }  
}

2. 序列化与反序列化的代码讲解

基本实现

这里我们可以先跑一跑代码,看一看。

Run SerializationTest.java

image

Run UnserializationTest.java

image

前文我们说,序列化与反序列化的根本目的是数据的传输。

  • SerializationTest.java

这里我们将代码进行了封装,将序列化功能封装进了 serialize这个方法里面,在序列化当中,我们通过这个FileOutputStream输出流对象,将序列化的对象输出到ser.bin当中。再调用 oos 的writeObject方法,将对象进行序列化操作。

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
oos.writeObject(obj);
  • UnserializeTest.java

进行反序列化

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
Object obj = ois.readObject();

Serializable 接口

(1) 序列化类的属性没有实现 Serializable那么在序列化就会报错

只有实现 了Serializable或者 Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)

Serializable 接口是 Java 提供的序列化接口,它是一个空接口,所以其实我们不需要实现什么。

public interface Serializable {
}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致如下结果。

image

(2) 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
(3)一个实现 Serializable接口的子类也是可以被序列化的。
(4) 静态成员变量是不能被序列化

序列化是针对对象属性的,而静态成员变量是属于类的。

(5) transient 标识的对象成员变量不参与序列化

这里我们可以动手实操一下,将 Person.java中的name加上transient的类型标识。加完之后再跑我们的序列化与反序列化的两个程序,修改过程与运行结果如图所示。

image


上述说的还是关于序列化本身的一些特性,接下来我们讲一讲序列化的安全问题是如何产生的。

0x04 为什么会产生序列化的安全问题

1. 引子

  • 序列化与反序列化当中有两个 "特别特别特别特别特别"重要的方法 ————writeObjectreadObject

这两个方法可以经过开发者重写,一般序列化的重写都是由于下面这种场景诞生的。

举个例子,MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的,所以这里就要自定义序列化过程啦,具体的做法是重写以下两个 private 方法:

private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException

只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,基于攻击者在服务器上运行代码的能力。

所以从根本上来说,Java 反序列化的漏洞的与readObject有关。

2. 可能存在安全漏洞的形式

(1) 入口类的readObject直接调用危险方法

  • 这种情况呢,在实际开发场景中并不是特别常见,我们还是跟着代码来走一遍,写一段弹计算器的代码,文件 ———— "Person.Java"

image

先运行序列化程序 ———— "SerializationTest.java",再运行反序列化程序 ———— "UnserializeTest.java"

这时候就会弹出计算器,也就是calc.exe,是不是帅的飞起哈哈。

这是黑客最理想的情况,但是这种情况几乎不会出现。

(2) 入口参数中包含可控类,该类有危险方法,readObject时调用

(3) 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用

(4) 构造函数/静态代码块等类加载时隐式执行

3. 产生漏洞的攻击路线

首先的攻击前提:继承 Serializable

入口类:source (重写 readObject 调用常见的函数;参数类型宽泛,比如可以传入一个类作为参数;最好 jdk 自带)

找到入口类之后要找调用链 gadget chain 相同名称、相同类型

执行类 sink (RCE SSRF 写文件等等)比如exec这种函数

这里看不懂先不要紧,后续看到 URLDNS 利用链的复现就会悟了哈哈,我当时这里也看不懂,后面悟了。

以 HashMap 为例说明一下,仅仅只是说明如何找到入门类

  • 我这里逐个给大家过一遍,以HashMap为例进行说明。

首先,攻击前提,那必然是要继承了Serializable这个接口,要不然谈何序列化与反序列化对吧。

HashMap 确实继承了Serializable这个接口。

image

入口类这里比较难懂,还是以HashMap为例吧,这些步骤是要自己动手实操一下的,不然体验感很差。

打开 "Structure",找到重写的readObject,往下分析。

image

我们看到第 1416 行与 1418 行中,Key 与 Value 的值执行了readObject的操作,再将 Key 和 Value 两个变量扔进hash这个方法里,我们再跟进(ctrl+鼠标左键即可) hash 当中。

image

  • 若传入的参数 key 不为空,则h = key.hashCode(),于是乎,继续跟进hashCode当中。

hashCode 位置处于 Object 类当中,满足我们 调用常见的函数这一条件。

实战 ———— URLDNS

出发点:URLDNS 在 Java 复杂的反序列化漏洞当中足够简单;URLDNS 就是 ysoserial 中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。
因为其参数不是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次 DNS 请求。

虽然这个“利⽤链”实际上是不能“利⽤”的,但因为其如下的优点,⾮常适合我们在检测反序列化漏洞时使⽤。

使⽤ Java 内置的类构造,对第三⽅库没有依赖。

在⽬标没有回显的时候,能够通过 DNS 请求得知是否存在反序列化漏洞 URL 类,调用openConnection方法,到此处的时候,其实openConnection不是常见函数,就已经难以利用了。

我们先去到 ysoserial 的项目当中,去看看它是如何构造 URLDNS 链的。

ysoserial/URLDNS.java at master · frohoff/ysoserial (github.com)

image

ysoserial 对 URLDNS 的利用链看着无比简单,就这么几行代码。

Gadget Chain:
	HashMap.readObject()
		HashMap.putVal()
			HashMap.hash()
				URL.hashCode()

再来开始自己复现一遍 URLDNS 的利用链。

初步复现

URL 是由 HashMap 的put方法产生的,所以我们先跟进put方法当中。put方法之后又是调用了hash方法;hash方法则是调用了hashcode这一函数。

image

还记得前文最看不懂的地方吗?我当时说这一块不懂不要紧,看到 URLDNS 的复现就懂了。

我们看到这个hashCode函数的变量名是 key;那这个 key 是啥啊?
噢 ~ 原来 key 是hash这一方法传进的参数!那我们前面写的 key 不就是这个东东吗 ~!

hashmap.put(new URL("DNS生成的 URL,用dnslog就可以"),1);

// 传进去两个参数,key = 前面那串网址,value = 1

所以这里,我们跟进 URL,去看看 URL 跟进一堆之后的hashCode方法是如何实现的。

跟进 URL,我们肯定是要去寻找 URL 调用的函数的函数(的函数,应该还有好几个的函数,就不写出来了,不然大家就晕了)的hashCode方法。

在左边 Structure 直接寻找hashCode方法,URL 中的hashCodehandler这一对象所调用,handler又是URLStreamHandler的抽象类。我们再去找URLStreamHandlerhashCode方法。

image

终于找到了,这个用于 URLDNS 的方法 ————getHostAddress

image

再跟进getHostAddress

image

这⾥InetAddress.getByName(host)的作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询。到这⾥就不必要再跟了。

所以,⾄此,整个 URLDNS 的Gadget其实清晰⼜简单:

  1. HashMap->readObject()

  2. HashMap->hash()

  3. URL->hashCode()

  4. URLStreamHandler->hashCode()

  5. URLStreamHandler->getHostAddress()

  6. InetAddress->getByName()

半路杀出个程咬金
  • 我们的复现步骤:

SerializationTest.java文件下添加如下代码

HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();   
hashmap.put(new URL("DNS生成的 URL,用dnslog就可以"),1);

serialize(hashmap);

image

我们先把它序列化,按照道理来说,这个过程应该什么都不会发生对吧。

image

很奇怪,为什么却能收到 URLDNS 的请求????
那我们的视线很容易就被干扰了呀,无法判断到底是因为反序列化的 URLDNS ,还是因为序列化的过程中的 URLDNS。

把程咬金给办了!

还是从原理角度分析,我们回到 URL 这个对象,回到hashCode这里。

image

我们发现,当hashCode的值不等于 -1 的时候,函数就会直接return hashCode而不执行hashCode = handler.hashCode(this);。而一开始定义 HashMap 类的时候hashCode的值为 -1,便是发起了请求。

所以我们在没有反序列化的情况下面,就收到了 DNS 请求,这是不正确的。

那要如何才能把 “半路杀出的程咬金” 给办了呢?我们大致有这样一个思路。

image

有关反射的知识又是一个很庞大的体系了,我们下篇文章再讲 ~
这里我先把 Poc 挂出来。

URLDNS 反序列化利用链的 POC

根据我们的思路,将 Main 函数进行修改,我这里直接全部挂出来了,不然师傅们容易看错。

SerializationTest.java

public static void main(String[] args) throws Exception{  
 Person person = new Person("aa",22);  
 HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();  
 // 这里不要发起请求  
 URL url = new URL("http://bl00nzimnnujskz418kboqxt9kfb30.oastify.com"); 
 Class c = url.getClass();  
 Field hashcodefile = c.getDeclaredField("hashCode");  
 hashcodefile.setAccessible(true);  
 hashcodefile.set(url,1234);  
 hashmap.put(url,1);  
 // 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性  
 hashcodefile.set(url,-1);  
 serialize(hashmap);  
}

反序列化的文件无需更改

接着我们运行序列化文件,是收不到 DNS 请求的,而当我们运行反序列化的文件时候,可以收到请求,这就代表着我们的 URLDNS 链构造成功了。

image

0x05 小结与后续展望

其实看了很多的反序列化教程,自己写这篇文章也是为了大家能够少走弯路吧,我们分析下来,一个最简单的 URLDNS 对于刚入门的师傅们来说也是比较难以理解与分析的,我本人也是学了好几天才啃下来的。

相对于一开始就接触 CC 链,或者是其他 Tomcat,shiro 漏洞复现的就更甚了,不懂原理只当一个脚本小子对个人的提升意义并不是很大。

后续的一些展望

师傅们若是对 Java 安全感兴趣的话可以关注我 ~后续还会继续更新相关自己的学习笔记的。


文章来源: https://www.freebuf.com/articles/web/333697.html
如有侵权请联系:admin#unsafe.sh