CVE-2024-23114 Apache Camel: Camel-CassandraQL: Unsafe Deserialization from CassandraAggregationRepository

注意
本文最后更新于 2024-03-06,文中内容可能已过时。

CVE-2024-23114 Apache Camel: Camel-CassandraQL: Unsafe Deserialization from CassandraAggregationRepository

Apache Camel Security Advisory - CVE-2024-23114 - Apache Camel

Camel-CassandraQL AggregationRepository 容易受到不安全反序列化的影响。在特定条件下,可以反序列化恶意负载。

From 3.0.0 before 3.21.4, from 3.22.0 before 3.22.1, from 4.0.0 before 4.0.4, from 4.1.0 before 4.4.0.

3.21.4, 3.22.1, 4.0.4 and 4.4.0

通告中给出了 JIRA 的地址,直接查看其中的一个提交

image-20240305174708556

我们看到这个描述说“添加了一个字符串类型的 ObjectInputFilter”,也就是用过滤来解决这个反序列化的问题的。直接看到代码 CassandraCamelCodec.java,正如描述所说的。直接发现了一个原生反序列化的地方。

image-20240305175008911

搭建环境的时候,发现了一个简单的构建环境复现的方式。Kameleon提供了一个在线生成 standalone 页面,选择 CassandraQL 组件生成一个项目。

image-20240306105011776

解压之后使用 IDEA 打开,我们使用测试单元进行复现。我们模仿提交里面的两个测试类,添加以下两个类。

Employee.java

package org.acme.camel;

import java.io.IOException;
import java.io.Serializable;

public class Employee implements Serializable {

    String name;
    String surname;

    public Employee(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }

    public String getName() {
        return name;
    }

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

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        in.defaultReadObject();
        Runtime.getRuntime().exec("calc.exe");
    }
}

CassandraCamelCodecTest.java

package org.acme.camel;

import java.io.*;
import java.nio.ByteBuffer;

import org.apache.camel.processor.aggregate.cassandra.CassandraCamelCodec;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class CassandraCamelCodecTest extends CamelTestSupport {

    CassandraCamelCodec codec;

    @Override
    protected void startCamelContext() throws Exception {
        super.startCamelContext();
        codec = new CassandraCamelCodec();
    }

    @Test
    public void shouldFailWithRejected() throws IOException, ClassNotFoundException {
        Employee emp = new Employee("Mickey", "Mouse");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);

        oos.writeObject(emp);

        oos.flush();
        oos.close();

        InputStream is = new ByteArrayInputStream(baos.toByteArray());
        InvalidClassException thrown = Assertions.assertThrows(InvalidClassException.class, () -> {
            // fix version
//            codec.unmarshallExchange(context, ByteBuffer.wrap(is.readAllBytes()), "java.**;org.apache.camel.**;!*");
            // vuln version
            codec.unmarshallExchange(context, ByteBuffer.wrap(is.readAllBytes()));
        });
        System.out.println(thrown.getMessage());
        Assertions.assertEquals("filter status: REJECTED", thrown.getMessage());
    }
}

运行测试单元即可触发反序列化漏洞。

image-20240306110004454

修改 pom 文件的 camel-cassandraql 的版本(默认是没有的),修改到漏洞修复后的版本。

<dependency>
  <groupId>org.apache.camel</groupId>
  <artifactId>camel-cassandraql</artifactId>
  <version>4.4.0</version>
</dependency>

运行发现测试用例通过,意味着反序列化被拦截。

image-20240306110430529

看到代码修复的关键地方,加入了一个 Filter。

private Object deserialize(CamelContext camelContext, InputStream bytes, String deserializationFilter) throws IOException, ClassNotFoundException {
    ClassLoader classLoader = camelContext.getApplicationContextClassLoader();
    ObjectInputStream objectIn = new ClassLoadingAwareObjectInputStream(classLoader, bytes);
    objectIn.setObjectInputFilter(Config.createFilter(deserializationFilter)); // fix vuln
    Object object = objectIn.readObject();
    objectIn.close();
    return object;
}

后面才发现,这个是 JDK 自带的一个方法,而且这个方法是 JDK9 之后引入的,常用 JDK8 的脚本小子在这个时候孤陋寡闻了。而且这里只需要传入一个字符串类型的 pattern 即可设置完成 Filter。pattern 的解析具体在 java.io.ObjectInputFilter.Config.Global#Global

具体是文档在 ObjectInputFilter.Config (Java SE 9 & JDK 9 )

public static ObjectInputFilter createFilter(String pattern)

Returns an ObjectInputFilter from a string of patterns. 从模式字符串返回一个 ObjectInputFilter。

Patterns are separated by “;” (semicolon). Whitespace is significant and is considered part of the pattern. If a pattern includes an equals assignment, “=” it sets a limit. If a limit appears more than once the last value is used. 模式由“;”分隔(分号)。空白很重要,被认为是模式的一部分。如果模式包含 equals 赋值,“ = ”它会设置一个限制。如果限制出现多次,则使用最后一个值。

  • maxdepth=value - the maximum depth of a graph maxdepth= value - 图表的最大深度
  • maxrefs=value - the maximum number of internal references maxrefs= value - 内部引用的最大数量
  • maxbytes=value - the maximum number of bytes in the input stream maxbytes= value - 输入流中的最大字节数
  • maxarray=value - the maximum array length allowed maxarray= value - 允许的最大数组长度

Other patterns match or reject class or package name as returned from Class.getName() and if an optional module name is present class.getModule().getName(). Note that for arrays the element type is used in the pattern, not the array type. 其他模式匹配或拒绝从 Class.getName() 返回的类或包名称,并且如果存在可选模块名称 class.getModule().getName() 。请注意,对于数组,模式中使用的是元素类型,而不是数组类型。

  • If the pattern starts with “!”, the class is rejected if the remaining pattern is matched; otherwise the class is allowed if the pattern matches. 如果模式以“!”开头,则如果剩余模式匹配,则该类将被拒绝;否则,如果模式匹配,则允许该类。
  • If the pattern contains “/”, the non-empty prefix up to the “/” is the module name; if the module name matches the module name of the class then the remaining pattern is matched with the class name. If there is no “/”, the module name is not compared. 如果模式包含“/”,则“/”之前的非空前缀是模块名称;如果模块名称与类的模块名称匹配,则其余模式与类名称匹配。如果没有“/”,则不比较模块名称。
  • If the pattern ends with “.” it matches any class in the package and all subpackages. 如果模式以“.”结尾,则它匹配包和所有子包中的任何类。
  • If the pattern ends with “.” it matches any class in the package. 如果模式以“.”结尾,则它与包中的任何类匹配。
  • If the pattern ends with “”, it matches any class with the pattern as a prefix. 如果模式以“”结尾,则它匹配以该模式作为前缀的任何类。
  • If the pattern is equal to the class name, it matches. 如果模式等于类名,则匹配。
  • Otherwise, the pattern is not matched. 否则,模式不匹配。

The resulting filter performs the limit checks and then tries to match the class, if any. If any of the limits are exceeded, the filter returns Status.REJECTED. If the class is an array type, the class to be matched is the element type. Arrays of any number of dimensions are treated the same as the element type. For example, a pattern of “!example.Foo”, rejects creation of any instance or array of example.Foo. The first pattern that matches, working from left to right, determines the Status.ALLOWED or Status.REJECTED result. If the limits are not exceeded and no pattern matches the class, the result is Status.UNDECIDED. 生成的过滤器执行限制检查,然后尝试匹配该类(如果有)。如果超出任何限制,过滤器将返回 Status.REJECTED 。如果类是数组类型,则匹配的类是元素类型。任意维数的数组都被视为与元素类型相同。例如,“ !example.Foo ”模式拒绝创建 example.Foo 的任何实例或数组。第一个匹配的模式(从左到右)确定 Status.ALLOWEDStatus.REJECTED 结果。如果未超出限制并且没有模式与该类匹配,则结果为 Status.UNDECIDED

  • Parameters: 参数:

    pattern - the pattern string to parse; not null pattern - 要解析的模式字符串;不为空

  • Returns: 返回:

    a filter to check a class being deserialized; null if no patterns 用于检查正在反序列化的类的过滤器; null 如果没有模式

  • Throws: 投掷:

    IllegalArgumentException - if the pattern string is illegal or malformed and cannot be parsed. In particular, if any of the following is true: IllegalArgumentException - 如果模式字符串非法或格式错误且无法解析。特别是,如果满足以下任一条件:if a limit is missing the name or the name is not one of “maxdepth”, “maxrefs”, “maxbytes”, or “maxarray” 如果缺少名称限制或名称不是“maxdepth”、“maxrefs”、“maxbytes”或“maxarray”之一if the value of the limit can not be parsed by Long.parseLong or is negative 如果限制的值无法被 Long.parseLong 解析或者为负数if the pattern contains “/” and the module name is missing or the remaining pattern is empty 如果模式包含“/”并且模块名称丢失或剩余模式为空if the package is missing for “.” and “.**” 如果包中缺少“.”和“.**”