Java反序列化漏洞原理

一、背景

2015年11月6日FoxGlove Security安全团队的@breenmachine 发布了一篇长博客,阐述了利用Java反序列化和Apache Commons Collections这一基础类库实现远程命令执行的真实案例,各大Java Web Server纷纷躺枪,这个漏洞横扫WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版,这也是反序列化漏洞第一次引起我们的关注。

二、认识Java序列化与反序列化

2.1 定义:

序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。
这两个过程结合起来,可以轻松地存储和传输数据,这就是序列化的意义所在。

我们可以把对象序列化为不同的格式,比如说,Json序列化、XML序列化、二进制序列化、SOAP序列化等,以上这些不同的格式也都是为了适应具体的业务需求。


Java反序列化漏洞原理_第1张图片

2.2 用途:

  1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2. 在网络上传送对象的字节序列。

2.3 应用场景:

1. 一般来说,服务器启动后,就不会再关闭了,但是如果逼不得已需要重启,而用户会话还在进行相应的操作,这时就需要使用序列化将session信息保存起来放在硬盘,服务器重启后,又重新加载。这样就保证了用户信息不会丢失,实现永久化保存。

  1. 在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便减轻内存压力或便于长期保存。

比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

例子: 淘宝每年都会有定时抢购的活动,很多用户会提前登录等待,长时间不进行操作,一致保存在内存中,而到达指定时刻,几十万用户并发访问,就可能会有几十万个session,内存可能吃不消。这时就需要进行对象的活化、钝化,让其在闲置的时候离开内存,将信息保存至硬盘,等要用的时候,就重新加载进内存。

三、Java中的序列化和反序列化API实现

3.1 实现方法

  • Java.io.ObjectOutputStream
  • java.io.ObjectInputStream

序列化:  ObjectOutputStream类 --> writeObject()

注:该方法对参数指定的obj对象进行序列化,把字节序列写到一个目标输出流中
按Java的标准约定是给文件一个.ser扩展名

反序列化: ObjectInputStream类 --> readObject()

注:该方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

3.2 简单测试代码

import java.io.*;

/*
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
*/

public class Java_Test{

    public static void main(String args[]) throws Exception {
        String obj = "ls ";

        // 将序列化对象写入文件object.txt中
        FileOutputStream fos = new FileOutputStream("aa.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(obj);
        os.close();

        // 从文件object.txt中读取数据
        FileInputStream fis = new FileInputStream("aa.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);

        // 通过反序列化恢复对象obj
        String obj2 = (String)ois.readObject();
        System.out.println(obj2);
        ois.close();
    }

}

我们可以看到,先通过输入流创建一个文件,再调用ObjectOutputStream类的 writeObject方法把序列化的数据写入该文件;然后调用ObjectInputStream类的readObject方法反序列化数据并打印数据内容。

实现Serializable和Externalizable接口的类的对象才能被序列化。

Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。
  
对象序列化包括如下步骤:
  1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
  2) 通过对象输出流的writeObject()方法写对象。

对象反序列化的步骤如下:
  1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
  2) 通过对象输入流的readObject()方法读取对象。

3.3 代码示例

我们创建一个Person接口,然后写两个方法:

    序列化方法:   创建一个Person实例,调用函数为其三个成员变量赋值,通过writeObject方法把该对象序列化,写入Person.txt文件中
    反序列化方法:调用readObject方法,返回一个经过饭序列化处理的对象

  在测试主类里面,我们先序列化Person实例对象,然后又反序列化该对象,最后调用函数获取各个成员变量的值。
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.MessageFormat;
import java.io.Serializable;

class Person implements Serializable {

    /**
     * 序列化ID
     */
    private static final long serialVersionUID = -5809782578272943999L;
    
    
    private int age;
    private String name;
    private String sex;
    
    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public String getSex() {
        return sex;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}

/**
 * 

ClassName: SerializeAndDeserialize

*

Description: 测试对象的序列化和反序列

*/ public class SerializeDeserialize_readObject { public static void main(String[] args) throws Exception { SerializePerson();//序列化Person对象 Person p = DeserializePerson();//反序列Perons对象 System.out.println(MessageFormat.format("name={0},age={1},sex={2}", p.getName(), p.getAge(), p.getSex())); } /** * MethodName: SerializePerson * Description: 序列化Person对象 */ private static void SerializePerson() throws FileNotFoundException, IOException { Person person = new Person(); person.setName("ssooking"); person.setAge(20); person.setSex("男"); // ObjectOutputStream 对象输出流,将Person对象存储到Person.txt文件中,完成对Person对象的序列化操作 ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream( new File("Person.txt"))); oo.writeObject(person); System.out.println("Person对象序列化成功!"); oo.close(); } /** * MethodName: DeserializePerson * Description: 反序列Perons对象 */ private static Person DeserializePerson() throws Exception, IOException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Person.txt"))); /* FileInputStream fis = new FileInputStream("Person.txt"); ObjectInputStream ois = new ObjectInputStream(fis); */ Person person = (Person) ois.readObject(); System.out.println("Person对象反序列化成功!"); return person; } }

3.4 漏洞原理

我们既然已经知道了序列化与反序列化的过程,那么如果反序列化的时候,这些即将被反序列化的数据是我们特殊构造的呢!
如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。

四、漏洞挖掘

4.1 漏洞触发场景

在java编写的web应用与web服务器间java通常会发送大量的序列化对象例如以下场景:

  • HTTP请求中的参数,cookies以及Parameters。
  • RMI协议,被广泛使用的RMI协议完全基于序列化
  • JMX 同样用于处理序列化对象
  • 自定义协议 用来接收与发送原始的java对象

4.2 挖掘方法

(1)确定反序列化输入点
首先应找出readObject方法调用,在找到之后进行下一步的注入操作。
一般可以通过以下方法进行查找:

  • 源码审计:寻找可以利用的“靶点”,即确定调用反序列化函数readObject的调用地点。
  • 对该应用进行网络行为抓包,寻找序列化数据,如wireshark,tcpdump等

(2)再考察应用的Class Path中是否包含Apache Commons Collections库
(3)生成反序列化的payload
(4)提交我们的payload数据

  • 本文参考资料:https://www.cnblogs.com/ssooking/p/5875215.html

你可能感兴趣的:(Java反序列化漏洞原理)