Java序列化与反序列化漏洞

1. Java 序列化与反序列化

  • Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。
  • Java 反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。

序列化与反序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。主要应用在以下场景:

  • HTTP:多平台之间的通信,管理等
  • RMI:是 Java 的一组拥护开发分布式应用程序的 API,实现了不同操作系统之间程序的方法调用。值得注意的是,RMI 的传输 100% 基于反序列化,Java RMI 的默认端口是 1099 端口。
  • JMX:JMX 是一套标准的代理和服务,用户可以在任何 Java 应用程序中使用这些代理和服务实现管理,中间件软件 WebLogic 的管理页面就是基于 JMX 开发的,而 JBoss 则整个系统都基于 JMX 构架。 ​

2.序列化与反序列化基础

Java描述的是一个‘世界’,程序运行开始时,这个‘世界’也开始运作,但‘世界’中的对象不是一成不变的,它的属性会随着程序的运行而改变。
但很多情况下,我们需要保存某一刻某个对象的信息,来进行一些操作。比如利用反序列化将程序运行的对象状态以二进制形式储存与文件系统中,然后可以在另一个程序中对序列化后的对象状态数据进行反序列化恢复对象。可以有效地实现多平台之间的通信、对象持久化存储。

一个类的对象要想序列化成功,必须满足两个条件:

  • 1、该类必须实现 java.io.Serializable 接口。
  • 2、该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。
  • 如果你想知道一个 Java 标准类是否是可序列化的,可以通过查看该类的文档,查看该类有没有实现 java.io.Serializable接口。

下面书写一个简单的demo,为了节省文章篇幅,这里把序列化操作和反序列化操作弄得简单一些,并省去了传递过程,对象所属类:

public class Employee implements java.io.Serializable
{
   public String name;
   public String identify;
   public void mailCheck()
   {
      System.out.println("This is the "+this.identify+" of our company");
   }
}

将对象序列化为二进制文件:

//反序列化所需类在io包中
import java.io.*;

public class SerializeDemo
{
   public static void main(String [] args)
   {
      Employee e = new Employee();
      e.name = "员工甲";
      e.identify = "General staff";
      try
      {
        // 打开一个文件输入流
         FileOutputStream fileOut =
         new FileOutputStream("D:\\Task\\employee1.db");
         // 建立对象输入流
         ObjectOutputStream out = new ObjectOutputStream(fileOut);
         //输出反序列化对象
         out.writeObject(e);
         out.close();
         fileOut.close();
         System.out.printf("Serialized data is saved in D:\\Task\\employee1.db");
      }catch(IOException i)
      {
          i.printStackTrace();
      }
   }
}

一个Identity属性为Visitors的对象被储存进了employee1.db,而反序列化操作就是从二进制文件中提取对象:

import java.io.*;

public class SerializeDemo
{
   public static void main(String [] args)
   {
      Employee e = null;
      try
      {
        // 打开一个文件输入流
         FileInputStream fileIn = new FileInputStream("D:\\Task\\employee1.db");
        // 建立对象输入流
         ObjectInputStream in = new ObjectInputStream(fileIn);
        // 读取对象
         e = (Employee) in.readObject();
         in.close();
         fileIn.close();
      }catch(IOException i)
      {
         i.printStackTrace();
         return;
      }catch(ClassNotFoundException c)
      {
         System.out.println("Employee class not found");
         c.printStackTrace();
         return;
      }
      System.out.println("Deserialized Employee...");
      System.out.println("Name: " + e.name);
      System.out.println("This is the "+e.identify+" of our company");
    }
}

就这样,一个完整的序列化周期就完成了,其实实际应用中的序列化无非就是传输的方式和传输机制稍微复杂一点,和这个demo没有太大区别。

简单的反序列化漏洞demo

在Java反序列化中,会调用被反序列化的readObject方法,当readObject方法书写不当时就会引发漏洞。

PS:有时也会使用readUnshared()方法来读取对象,readUnshared()不允许后续的readObject和readUnshared调用引用这次调用反序列化得到的对象,而readObject读取的对象可以。

//反序列化所需类在io包中
import java.io.*;
public class test{
    public static void main(String args[]) throws Exception{

        UnsafeClass Unsafe = new UnsafeClass();
        Unsafe.name = "hacked by ph0rse";

        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        //writeObject()方法将Unsafe对象写入object文件
        os.writeObject(Unsafe);
        os.close();
        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("object");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        UnsafeClass objectFromDisk = (UnsafeClass)ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }
}

class UnsafeClass implements Serializable{
    public String name;
    //重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行命令
        Runtime.getRuntime().exec("calc.exe");
    }
}

程序运行逻辑为:

  • UnsafeClass类被序列化进object文件
  • 从object文件中恢复对象
  • 调用被恢复对象的readObject方法
  • 命令执行

3. 漏洞历史

  • 最为出名的大概应该是:15年的Apache Commons Collections 反序列化远程命令执行漏洞,其当初影响范围包括:WebSphere、JBoss、Jenkins、WebLogic 和 OpenNMSd等。
  • 2016年Spring RMI反序列化漏洞今年比较出名的:Jackson,FastJson
  • Java 十分受开发者喜爱的一点是其拥有完善的第三方类库,和满足各种需求的框架;但正因为很多第三方类库引用广泛,如果其中某些组件出现安全问题,那么受影响范围将极为广泛。

4. 漏洞成因

  • 暴露或间接暴露反序列化 API ,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码
  • 两个或多个看似安全的模块在同一运行环境下,共同产生的安全问题
  • 重写ObjectInputStream对象的resolveClass方法中的检测可被绕过。
  • 使用第三方的类进行黑名单控制。虽然Java的语言严谨性要比PHP强的多,但在大型应用中想要采用黑名单机制禁用掉所有危险的对象几乎是不可能的。因此,如果在审计过程中发现了采用黑名单进行过滤的代码,多半存在一两个‘漏网之鱼’可以利用。并且采取黑名单方式仅仅可能保证此刻的安全,若在后期添加了新的功能,就可能引入了新的漏洞利用方式。所以仅靠黑名单是无法保证序列化过程的安全的。

5. 漏洞基本原理

实现序列化与反序列化

public class test{
    public static void main(String args[])throws Exception{
          //定义obj对象
        String obj="hello world!";
          //创建一个包含对象进行反序列化信息的”object”数据文件
        FileOutputStream fos=new FileOutputStream("object");
        ObjectOutputStream os=new ObjectOutputStream(fos);
          //writeObject()方法将obj对象写入object文件
        os.writeObject(obj);
        os.close();
          //从文件中反序列化obj对象
        FileInputStream fis=new FileInputStream("object");
        ObjectInputStream ois=new ObjectInputStream(fis);
          //恢复对象
        String obj2=(String)ois.readObject();
        System.out.print(obj2);
        ois.close();
    }
}

上面代码将 String 对象 obj1 序列化后写入文件 object 文件中,后又从该文件反序列化得到该对象。

public class test{
    public static void main(String args[]) throws Exception{
        //定义myObj对象
        MyObject myObj = new MyObject();
        myObj.name = "hi";
        //创建一个包含对象进行反序列化信息的”object”数据文件
        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        //writeObject()方法将myObj对象写入object文件
        os.writeObject(myObj);
        os.close();
        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("object");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        MyObject objectFromDisk = (MyObject)ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }
}

class MyObject implements Serializable{
    public String name;
    //重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行打开计算器程序命令
        Runtime.getRuntime().exec("open /Applications/Calculator.app/");
    }
}

这次我们自己写了一个 class 来进行对象的序列与反序列化。我们看到,MyObject 类有一个公有属性 name ,myObj 实例化后将 myObj.name 赋值为了 “hi” ,然后序列化写入文件 object,然后读取 object 反序列化时:

我们注意到 MyObject 类实现了Serializable接口,并且重写了readObject()函数。这里需要注意:只有实现了Serializable接口的类的对象才可以被序列化,Serializable 接口是启用其序列化功能的接口,实现 java.io.Serializable 接口的类才是可序列化的,没有实现此接口的类将不能使它们的任一状态被序列化或逆序列化。这里的 readObject() 执行了Runtime.getRuntime().exec("open /Applications/Calculator.app/"),而 readObject() 方法的作用正是从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回,readObject() 是可以重写的,可以定制反序列化的一些行为。

网鼎杯-青龙组~filejava

考点:Path Traversal、Arbitrary File Read、java class Decompile、Blind XXE

能上传,传完之后能下载:

看这个url,考虑有目录穿越可以下载任意文件,测试一下:

读取WEB-XML

/file_in_java/DownloadServlet?filename=../../../../../../../../../../../usr/local/tomcat/webapps/file_in_java/WEB-INF/web.xml

得到:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
  <display-name>file_in_java</display-name>
  <welcome-file-list>
    <welcome-file>upload.jsp</welcome-file>
  </welcome-file-list>
  <servlet>
    <description></description>
    <display-name>UploadServlet</display-name>
    <servlet-name>UploadServlet</servlet-name>
    <servlet-class>cn.abc.servlet.UploadServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>UploadServlet</servlet-name>
    <url-pattern>/UploadServlet</url-pattern>
  </servlet-mapping>
  <servlet>
    <description></description>
    <display-name>ListFileServlet</display-name>
    <servlet-name>ListFileServlet</servlet-name>
    <servlet-class>cn.abc.servlet.ListFileServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>ListFileServlet</servlet-name>
    <url-pattern>/ListFileServlet</url-pattern>
  </servlet-mapping>
  <servlet>
    <description></description>
    <display-name>DownloadServlet</display-name>
    <servlet-name>DownloadServlet</servlet-name>
    <servlet-class>cn.abc.servlet.DownloadServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>DownloadServlet</servlet-name>
    <url-pattern>/DownloadServlet</url-pattern>
  </servlet-mapping>
</web-app>

之后根据xml中的<servlet-class>把对应class都下载下来,然后反编译(我用的JD-GUI)得到源码:

(如果哪位大佬有mac上好用的java反编译软件麻烦留言告诉一下)

源码比较长就不贴了,主要是在UploadServlet.java中有如下代码:

if (filename.startsWith("excel-") &amp;&amp; "xlsx".equals(fileExtName)) {
  
  try {
    Workbook wb1 = WorkbookFactory.create(in);
    Sheet sheet = wb1.getSheetAt(0);
    System.out.println(sheet.getFirstRowNum());
  } catch (InvalidFormatException e) {
    System.err.println("poi-ooxml-3.10 has something wrong");
    e.printStackTrace();
  } 
}

这就比较明显了,考虑是Excel的xxe。先在[Content-Types].xml中引用外部dtd实体,然后再给压缩回去,上传,flag就打回来了

有的人就要问了,既然知道flag在/flag,为啥不能直接用下载器目录穿越然后读取?是因为DownloadServlet中有过滤:

String fileName = request.getParameter("filename");
fileName = new String(fileName.getBytes("ISO8859-1"), "UTF-8");
System.out.println("filename=" + fileName);
if (fileName != null &amp;&amp; fileName.toLowerCase().contains("flag")) {
  request.setAttribute("message", "");
  request.getRequestDispatcher("/message.jsp").forward((ServletRequest)request, (ServletResponse)response);
  
  return;
} 

后面的漏洞复现等有时间在搞。。PHP的反序列化和Java反序列化貌似不同。

上一篇
下一篇