Skip to content

Java安全之反序列化漏洞

前言:一直都听说Java存在反序列化漏洞,但由于个人对安全这块内容涉猎不深,所以完全不大了解反序列化漏洞到底是什么,特此写一篇文章记录学习一下

序列化与反序列化

序列化技术是什么

  • 序列化就是把对象转换成字节流,便于保存在内存、文件、数据库中
  • 反序列化即逆过程,把字节流还原成对象。

Untitled

序列化技术是一项非常方便的技术,主要有如下几个用途:

  • 永久性保存对象,保存对象的字节序列到本地文件或者数据库中
  • 通过序列化以字节流的形式使对象在网络中进行传递和接收
  • 通过序列化在进程间传递对象

简单来说,java中序列化与反序列化就是 Java 对象与数据之间的相互转化。


📌 为什么Java要有序列化机制?

实质上,序列化机制并不只局限于 Java 语言,序列化的本质是内存对象到数据流的一种转换,我们知道内存中的东西不具备持久性,但有些场景却需要将对象持久化保存或传输。例如缓存系统中存储了用户的 Session,如果缓存系统直接下线,带系统重启后用户就需要重新登陆,为了使缓存系统内存中的 Session 对象一直有效,就需要有一种机制将对象从内存中保存入磁盘,并且待系统重启后还能将 Session 对象恢复到内存中,这个过程就是对象序列化与反序列化的过程,从而避免了用户会话的有效性受系统故障的影响

此外,在 Java 工程中,序列化还广泛应用于 JMX,RMI,网络传输(协议包对象)等场景,可以说序列化机制赋予了内存对象持久化的机会,就像虚拟机镜像(VMware Take a snapshot),也可以将序列化机制看作是内存对象的一种镜像机制。


在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以通过 ObjectInputStream 与 ObejctOutputStream 序列化,如下我们模拟了 Session 对象持久化存储与从磁盘加载的过程:

  • 示例代码

    java
    public class SerializationTest {
    
        public static void main(String[] args) throws Exception {
            long expired = (long) 7 * 24 * 60 * 60 * 1000;
            Session testSession = new Session("ABCDEFG", expired);
    
            // 1.模拟Session持久化到磁盘文件
            FileOutputStream outputStream = new FileOutputStream("session.bin");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
            objectOutputStream.writeObject(testSession);
            objectOutputStream.close();
    
            // 2.模拟从磁盘加载Session对象
            FileInputStream inputStream = new FileInputStream("session.bin");
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            Session read = (Session) objectInputStream.readObject();
            objectInputStream.close();
    
            System.out.println(read.toString());
        }
    
    }
    
    class Session implements Serializable {
        private String sessionId;
        private long expired;
    
        public Session(String sessionId, long expired) {
            this.sessionId = sessionId;
            this.expired = expired;
        }
    
        @Override
        public String toString() {
            return "Session{" +
                    "sessionId='" + sessionId + '\'' +
                    ", expired=" + expired +
                    '}';
        }
    }

反序列化漏洞成因

2015 年年底,由公共依赖库 Apache Common Collections 引起的 Java 任意命令执行漏洞的严重安全问题,使得 Java 反序列化漏洞逐渐进入了安全研究人员的视野(在此之前存在,但并未被重视),而任意命令执行的成因正是前文反序列化操作的 ObejctInputStream 类的 readObject 方法触发的。

Java 序列化机制虽然有默认序列化机制,但也支持用户自定义的序列化与反序列化策略。例如对象的一些成员变量没必要序列化保存或传输,就可以不序列化,或者也可以对一些敏感字段进行处理等自定义对象序列化的行为,而自定义序列化规则的方式就是重写 writeObejct 与 readObject。当对象重写了 writeObejct 或 readObject方法时,Java 序列化与反序列化就会调用用户自定义的逻辑了

Untitled

我们对 Session 对象重写了序列化处理函数,会发现实际走的writeObejct 与 readObject 是我们自己重写的方法

java
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField readFields = inputStream.readFields();
        String sessionId = GeneralUtils.objectToString( readFields.get("sessionId", ""));
        try {
            sessionId = new String(sessionId.getBytes());
            System.out.println("self serialization" + this.getClass());
        } catch (Exception e) {
            sessionId = e.getMessage();
        }
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        ObjectOutputStream.PutField putField = out.putFields();
        putField.put("sessionId", sessionId.getBytes());
        System.out.println("self unserialization" + this.getClass());
        out.writeFields();
    }

Untitled

如果反序列化过程中提供了命令执行的机会,那么任意命令执行漏洞就产生了

如下图提供了执行命令的机会,传入的sessionId如果是“calc.exe”,就可以做到唤起计算器

Untitled

梳理

梳理一下上例中漏洞存在的条件与利用思路

条件

首先 Session 对象重写了反序列化函数 readObject,并且 readObject 方法存在执行命令的机会。

漏洞利用

正常的反序列化流程会重新生成一个正常 Session 对象,而恶意的序列化数据抓住了反序列化的漏洞执行命令的机会,精心构造了序列化对象,使得数据流反序列化的过程中恶意命令得以执行。

本示例只是为了以最直观的方式演示反序列漏洞产生原因,就直接提供了一个 HelloWorld 级别的漏洞示例.

实际上, Java Apache-CommonsCollections 造成的序列化漏洞与 Spring 框架的反序列化漏洞(spring-tx.jar)的成因与原理都与上例相似,只是漏洞利用的构成比较复杂而已。

例子:Java Apache-CommonsCollections RCE 漏洞解析

  • 复制了一份网上的例子,加深理解

    该漏洞曝光于 2015 年年底,被誉为当年“最被低估了的漏洞”,利用思路一经爆出,各大 Java web 厂商纷纷躺枪,受此影响的 Web 服务器有:WebLogic、WebSphere、JBoss、Jenkins、OpenNMS等。

    介于该漏洞曝光距今已经有两年之久,并且网上也有对此分析的很透彻的文章,本文就来讲一下这个可以通过精心构造触发命令执行的漏洞的要点。该漏洞的主要问题就出现在 org.apache.commons.collections.Transformer 接口上,在 Apache-CommonsCollections 包中,有一个 InvokerTransformer 类实现了 Transformer 接口,并且 InvokerTransformer 这个类也很恰巧,InvokerTransformer 的 transform 方法提供了一个可以通过 Java 反射机制,调用任意 Java 方法的机会:

    Untitled

    相信很多漏洞利用者对 invoke 非常敏感,意味着提供了方法调用的机会,再结合方法名与参数都能通过 InvokerTransformer 构造函数来控制,因此该类一定是漏洞挖掘者反复徘徊的地方。那该怎么利用?

    InvokerTransformer 结合 ChainedTransformer 就能构造一个 Java 类加载函数调用链,POC 的作者是如下构造的:

    Untitled

    这样构造的原因在于 ChainedTransformer 的 transform 函数会依次调用数组中 InvokerTransformer 的 transform 函数,构成一个函数调用链,下图是调试状态下,ChainedTransformer 的 transform 方法的变量状态:

    Untitled

    可以看到,通过 ChainedTransformer.transform 的构造相当于反射调用了 Runtime.getRuntime().exec(),触发了命令的执行。

    OK,那么问题来了,谁去触发调用 ChainedTransformer.transform 函数呢?

    漏洞利用者找到了包中 TransformedMap.checkSetValue() 方法:

    Untitled

    这样一来,POC 就可以通过构造一个 TransformedMap 对象,然后再想办法触发 checkSetValue 函数即可,而 TransformedMap 在 Apache-CommonsCollections 这个集合库中可以通过 TransformedMap.decorate 修饰器方法来修饰一个 Map 对象,而 Map 对象在反序列化是只需调用 MapEntiry.setValue 就能触发 checkSetValue 函数,进而进一步触发第一步构造的 ChainedTransformer.transform方法,从而实现漏洞利用的目的。

    以上就是 Apache-CommonsCollections RCE 漏洞的利用思路,该漏洞的实质是 Apache-CommonsCollections 为利用者提供了一系列可以构造行命令机会。

    上面的漏洞触发条件并不是在反序列化函数 readObject 中实现的,怎么能在反序列化中触发 POC 的执行?

    好问题!思路是这样的,可以找到一个这样的对象:

    1.该类自定义重写了 readObejct 反序列化函数。

    2.readObecjt 方法中调用了 Map 的 checkSetValue 函数,并且该 Map 对象还可以通过构造函数构造。

    在 JDK 中找到了一个满足条件的的对象sun.reflect.annotation.AnnotationInvocationHandler。看一下的 AnnotationInvocationHandler 的反序列化函数满不满足触发要求:

    Untitled

    因为 setValue 函数会调用 checkSetValue

    Untitled

    从而在 sun.reflect.annotation.AnnotationInvocationHandler 对象反序列化时,就满足了 ChainedTransformerTransformerMap构造的 POC 触发的条件,最后我们给出完整的构造 Apache CommonCollections 反序列化漏洞利用POC的代码(来自网络):

    Untitled

    Untitled

如何防范

  • 开发者要有安全意识,应该清楚项目使用到的组件是否有漏洞存在,虽然说 Apache-CommonsCollections RCE 漏洞曝光将近两年了,但使用存在漏洞的 3.2.2 之前版本的 web 服务框架依然存在,Apache James 3.0.0 版本的 CVE-2017-12628 漏洞就是还在使用 commons-collections-3.2.1.jar 造成的。
  • 可以禁用 JVM 执行外部命令(Runtime.exec),因为 Runtime.exec 对于大多数 Java 正常应用来说是不会用到的,但是确是黑客控制Web服务后运行命令的重要方法,因此该手段是 Java Web 防护常用的且有效的手段,如果从攻击者角度看这种防护效果,那就是攻击工具 webshell 只能文件相关操作,无法执行命令。可以通过扩展 SecurityManager 来禁用 Runtime.exec,当触发运行时还可加入报警逻辑,启动应急响应

最后更新于: