Apache Camel 远程代码执行漏洞 | CVE-2026-40453复现&研究
0x0 背景介绍
Apache Camel是一个开源的集成框架,基于已知的企业集成模式实现。
受影响版本中,JmsHeaderFilterStrategy、ClassicJmsHeaderStrategy、SjmsHeaderFilterStrategy、CoAPHeaderFilterStrategy和GooglePubsubHeaderFilterStrategy这5个非HTTP HeaderFilterStrategy实现使用大小写敏感的String.startsWith进行header过滤,而Camel Exchange内部以大小写不敏感map存储header。攻击者可通过JMS等协议注入大小写变体的Camel内部header(如’CAmelExecCommandExecutable’),绕过过滤后被camel-exec、camel-file等下游组件以标准大小写处理,导致远程代码执行或任意文件写入。
修复版本中通过在这5个策略类的构造函数中添加setLowerCase(true),使header名称在过滤前先转换为小写,确保大小写变体均可被正确过滤。
0x1 环境搭建(Ubuntu24)
- 为了复现环境整的,而非标准生产环境
#!/bin/bash
# CVE-2026-40453 漏洞环境搭建脚本
# 若任何命令失败则立即退出
set -e
echo "[*] 阶段1/4:检查并安装基础依赖..."
if ! command -v docker &> /dev/null; then
echo "[+] Docker 未安装,正在尝试安装..."
# 这里仅演示安装docker.io,实际生产环境建议使用官方源
sudo apt update && sudo apt install -y docker.io
fi
sudo systemctl enable --now docker 2>/dev/null || true
# 检查 Docker Compose 插件(现代标准)
if ! docker compose version &> /dev/null; then
echo "[!] 警告:Docker Compose 插件未安装。请安装 docker-compose-plugin 或使用 'docker-compose' 命令。"
echo " 本脚本将尝试使用 'docker-compose' 命令(如果已安装)。"
fi
echo "[*] 阶段2/4:创建工作目录并生成项目文件..."
WORKDIR="$HOME/cve-2026-40453"
mkdir -p "$WORKDIR/camel-app/src/main/java/org/vuln" && cd "$WORKDIR"
echo "[+] 进入工作目录: $PWD"
# 阶段2.1:生成 docker-compose.yml
echo "[*] 正在生成 docker-compose.yml..."
cat > docker-compose.yml <<'EOF'
version: '3.8'
services:
activemq:
image: apache/activemq-classic:6.1.6
container_name: activemq
ports:
- "61616:61616" # JMS
- "8161:8161"
environment:
ACTIVEMQ_ADMIN_LOGIN: admin
ACTIVEMQ_ADMIN_PASSWORD: admin
camel-app:
build: ./camel-app
container_name: camel-app
depends_on:
- activemq
environment:
ACTIVEMQ_BROKER_URL: tcp://activemq:61616
# 简单用 sleep 防止 ActiveMQ 未完全就绪,生产可加 healthcheck
entrypoint: ["/bin/sh", "-c", "sleep 15 && java -jar /app/app.jar"]
EOF
# 阶段2.2:生成 Dockerfile
echo "[*] 正在生成 camel-app/Dockerfile..."
cat > camel-app/Dockerfile <<'EOF'
# 编译阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /src
COPY pom.xml .
COPY src ./src
RUN mvn package -DskipTests -q
# 运行阶段
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /src/target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
EOF
# 阶段2.3:生成 pom.xml
echo "[*] 正在生成 camel-app/pom.xml..."
cat > camel-app/pom.xml <<'EOF'
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>
<groupId>org.vuln</groupId>
<artifactId>camel-cve-2026-40453</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<camel.version>4.14.5</camel.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-spring-boot-bom</artifactId>
<version>${camel.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-jms-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-exec-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-client</artifactId>
<version>6.1.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
EOF
# 阶段2.4:生成 Java 源码
echo "[*] 正在生成 camel-app/src/main/java/org/vuln/Application.java..."
mkdir -p camel-app/src/main/java/org/vuln
cat > camel-app/src/main/java/org/vuln/Application.java <<'EOF'
package org.vuln;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.jms.JmsComponent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public JmsComponent jms(Environment env) {
String brokerUrl = env.getProperty("ACTIVEMQ_BROKER_URL", "tcp://localhost:61616");
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerUrl);
JmsComponent jms = new JmsComponent();
jms.setConnectionFactory(cf);
return jms;
}
@Bean
public RouteBuilder vulnerableRoutes() {
return new RouteBuilder() {
@Override
public void configure() {
// 场景 A:JMS -> exec (使用 vuln.exec 队列)
from("jms:queue:vuln.exec")
//.to("exec:bash");
.to("exec:whoami")
.log(">>> whoami output: ${body}");
// 场景 B:JMS -> file 任意文件写入 (队列 vuln.file)
from("jms:queue:vuln.file")
.to("file:/tmp/outbox?autoCreate=true");
// 场景 C:JMS -> exec (使用 incoming 队列,作为另一种演示)
from("jms:queue:incoming")
.to("exec:bash");
}
};
}
}
EOF
echo "[*] 阶段3/4:启动 Docker 容器环境..."
echo "[+] 正在拉取镜像并构建应用..."
# 停止并清理旧容器
docker compose down -v 2>/dev/null || true
# 启动服务
docker compose up -d
echo ""
echo "=============================================="
echo " CVE-2026-40453 漏洞环境部署完成!"
echo " - ActiveMQ 控制台: http://localhost:8161"
echo " 用户名: admin"
echo " 密码: admin"
echo " - Camel 应用容器已后台运行"
echo " - 漏洞触发点: 向 ActiveMQ 的 'incoming' 队列发送消息将触发 exec:bash"
echo ""
echo " - 查看日志: docker logs camel-app"
echo " - 进入容器: docker exec -it camel-app /bin/bash"
echo "=============================================="
0x2 漏洞复现
多说一句话,后续尽可能的将复现Pacp共享出来,便于大家做拦截验证
2.1-手动验证
- 一共是三个,但是本质相同

2.1.1 场景 A:JMS -> camel-exec 命令执行
这一类场景最适合做“最短利用链”验证。先搭一个最容易暴露问题的跨边界链路: 外部生产者只能往消息队列里塞消息头,而路由内部再把消息交给 exec:。
建议环境
1. 中间件:
ActiveMQ Classic 或任意支持 JMS 属性的 Broker。
2. 路由形态:
• 消费端: from("jms:queue:vuln.exec")
•危险下游: to("exec:whoami") 或 Windows 下 to("exec:cmd.exe")
3. 依赖模块:
•camel-jms
•camel-exec
•对应 JMS 驱动
最小化示例路由
from("jms:queue:vuln.exec")
.to("exec:whoami");
如果希望从 Web 侧,可以再加一个 HTTP 入口,把请求头原样转发进 JMS:
345678
rest("/demo")
.post("/exec")
.to("direct:bridge");
from("direct:bridge")
.to("jms:queue:vuln.exec");。
Windows 下可替换为:
cAmelExecCommandExecutable=cmd.exe
cAmelExecCommandArgs=/c whoami
2.1.2 场景 B:JMS -> camel-file 任意文件写入
很多系统不会直接接 exec:,却很常见地把 MQ 中的消息落到文件系统。此时攻击者未必需要拿 RCE,只要能劫持输出文件名或路径,就已经越过了原本的安全边界。
建议环境
1. 路由形态:
•消费端: from("jms:queue:vuln.file")
•危险下游: to("file:./outbox?autoCreate=true")
2.依赖模块:
•camel-jms
•camel-file
最小化示例路由
from("jms:queue:vuln.file")
.to("file:./outbox?autoCreate=true");
2.2-复现流量特征 (PCAP)
本次复现PCAP:
https://github.com/Kai-One001/PCAP-For-Cybersecurity.rule/blob/main/2026/CVE-2026-40453-Apache-Camel.pcap
- 命令接口

- 文件写入

- 命令接口

0x3 漏洞原理分析
3.0-[架构定位] 先把受影响模块放同一条消息链
只有先把每个模块在链条中的职责摆清楚,后面看代码时才不会迷失在大量组件实现里。
3.1-[核心入口]先看 HeaderFilterStrategy
原本根据漏洞信息时,第一反应应该是去翻camel-exec,这次先不,这次是先追问一个更基础的问题: 攻击者明明只能控制“外部消息头”,这些头为什么有机会在路由内部被当成CamelExecCommandExecutable这样的框架级内部头使用?
先找到外部 Header 进入 Camel 体系时的第一道边界。顺着JMS consumer的逻辑往下看,定位到 JmsBinding.extractHeadersFromJms()。这个函数是关键,因为它一边枚举JMS属性,一边决定哪些头该被拦下,哪些头可以进入Camel消息对象。
// components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsBinding.java
whilewhile (names.hasMoreElements()) {
String name = names.nextElement().toString();
Object value = JmsMessageHelper.getProperty(jmsMessage, name);
if (headerFilterStrategy != null
&& headerFilterStrategy.applyFilterToExternalHeaders(name, value, exchange)) {
continue;
}
String key = jmsKeyFormatStrategy.decodeKey(name);
map.put(key, value);
}
-
预期安全边界,本来应该是“外部消息头先过过滤器,Camel内部保留头不能进入业务 Exchange”。
-
可真正实现里,过滤是否命中完全取决于
HeaderFilterStrategy,而一旦没有命中,头就会被map.put(key, value)塞进Camel的消息头图。 -
换句话说,这里就是“最后一道失守前的边界闸门”。
接着往上追过滤器的具体实现,可以看到 JMS 默认使用的是JmsHeaderFilterStrategy:
// components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsHeaderFilterStrategy.java
public JmsHeaderFilterStrategy(boolean includeAllJMSXProperties) {
setOutFilterStartsWith(DefaultHeaderFilterStrategy.CAMEL_FILTER_STARTS_WITH);
setInFilterStartsWith(DefaultHeaderFilterStrategy.CAMEL_FILTER_STARTS_WITH);
if (!includeAllJMSXProperties) {
initialize();
}
}
- 这里传入的
CAMEL_FILTER_STARTS_WITH实际上就是框架试图保护内部头空间的规则: 凡是以Camel/camel开头的头,都应该被视为内部头,不让外部带进来。
3.2-[逻辑缺陷] 预期要“拦 Camel 头”,实际却只拦了两种大小写
顺着JmsHeaderFilterStrategy往下追,停在DefaultHeaderFilterStrategy.doFiltering()。看这里的原因很简单: 入口策略把规则交给了公共基类,真正决定是否放行
// core/camel-support/src/main/java/org/apache/camel/support/DefaultHeaderFilterStrategy.java
private boolean doFiltering(Direction direction, String headerName, Object headerValue, Exchange exchange) {
// ...
if (startsWith != null) {
if (tryHeaderMatch(headerName, startsWith)) {
return filterOnMatch;
}
if (lowerCase) {
lower = headerName.toLowerCase();
if (tryHeaderMatch(lower, startsWith)) {
return filterOnMatch;
}
}
}
// ...
}
private boolean tryHeaderMatch(String headerName, String[] startsWith) {
for (String s : startsWith) {
boolean match = headerName.startsWith(s);
if (match) {
return true;
}
}
return false;
}
-
startsWith这一支并没有做统一规范化后再比较,而是直接把原始
headerName拿来和"Camel"、"camel"做前缀匹配。 -
它真正能拦住的,是完全以
Camel开头或者完全以camel开头的两类头。
这就是设计预期与实现现实之间差异:
•预期边界:
任何 Camel 内部头,不管攻击者怎么折腾大小写,只要语义上还是那个内部头,就不该从外部进入。
•实际实现:
过滤器只认识 "Camel" 和 "camel" 两种字面前缀,像 cAmelExecCommandExecutable、CAMelFileName 这类混合大小写变体会被当成“普通外部头”放行。
更值得注意的是,同一份实现里,Set型过滤反而考虑了equalsIgnoreCase
// core/camel-support/src/main/java/org/apache/camel/support/DefaultHeaderFilterStrategy.java
private boolean evalFilterMatch(String headerName, String lower, Set<String> filter) {
if (isCaseInsensitive()) {
for (String filterString : filter) {
if (filterString.equalsIgnoreCase(headerName)) {
return true;
}
}
} else if (lowerCase) {
// ...
}
return false;
}
3.3-[攻击链路] 为什么绕过入口后,下游还能按规范头名精确命中
如果分析停在上一步,其实还差最后一块拼图: 就算攻击者用 cAmelExecCommandExecutable 绕过了 JMS 过滤器,下游组件读取的却是标准名字 CamelExecCommandExecutable。这两个字符串并不完全相同,为什么还能连上?
答案藏在 Camel 默认消息实现里。之所以回头去看DefaultMessage,是因为所有组件最终读的都是exchange.getIn().getHeader(...)。只要这里对头名查找是大小写不敏感的,前面的绕过和后面的命中就会自然拼接起来。
// core/camel-support/src/main/java/org/apache/camel/support/DefaultMessage.java
/**
* This implementation uses a {@link org.apache.camel.util.CaseInsensitiveMap} storing the headers.
* This allows us to be able to lookup headers using case insensitive keys.
*/
public Object getHeader(String name) {
if (headers == null) {
headers = createHeaders();
}
if (!headers.isEmpty()) {
return headers.get(name);
} else {
return null;
}
}
再往底层看,CaseInsensitiveMap直接使用了String.CASE_INSENSITIVE_ORDER:
// core/camel-util/src/main/java/org/apache/camel/util/CaseInsensitiveMap.java
public class CaseInsensitiveMap extends TreeMap<String, Object> {
public CaseInsensitiveMap() {
super(String.CASE_INSENSITIVE_ORDER);
}
public CaseInsensitiveMap(Map<? extends String, ?> map) {
super(String.CASE_INSENSITIVE_ORDER);
putAll(map);
}
}
-
入口过滤是在大小写敏感地看头名,消息对象内部却在大小写不敏感地存取头名。
-
攻击者只需要在入口边界把头名改成一个大小写变体,让它躲过过滤;
-
一旦进入
CaseInsensitiveMap,这个变体就会与规范头名合流。 -
下游组件完全不需要知道攻击者用了什么花样大小写,它按正常代码读取标准头名,就能把恶意值取出来。
3.4-[爆发点一] 为什么确信这条链能打到 camel-exec
确认了“能进来”和“能命中”之后,下一步就是找真正的危险操作函数。对camel-exec 来说,这个函数非常明确: DefaultExecBinding.readInput() 负责从消息头取出命令相关参数,随后 ExecProducer.process() 直接执行。
public ExecCommand readInput(Exchange exchange, ExecEndpoint endpoint) {
Object args = exchange.getIn().removeHeader(EXEC_COMMAND_ARGS);
String cmd = getAndRemoveHeader(exchange.getIn(), EXEC_COMMAND_EXECUTABLE, endpoint.getExecutable(), String.class);
String dir = getAndRemoveHeader(exchange.getIn(), EXEC_COMMAND_WORKING_DIR, endpoint.getWorkingDir(), String.class);
long timeout = getAndRemoveHeader(exchange.getIn(), EXEC_COMMAND_TIMEOUT, endpoint.getTimeout(), Long.class);
String exitValuesString
= getAndRemoveHeader(exchange.getIn(), EXEC_COMMAND_EXIT_VALUES, endpoint.getExitValues(), String.class);
String outFilePath = getAndRemoveHeader(exchange.getIn(), EXEC_COMMAND_OUT_FILE, endpoint.getOutFile(), String.class);
// ...
return new ExecCommand(
cmd, argsList, dir, timeout, exitValues, input, outFile, useStderrOnEmptyStdout, commandLogLevel);
}
public void process(Exchange exchange) throws Exception {
ExecCommand execCommand = getBinding().readInput(exchange, endpoint);
ExecCommandExecutor executor = endpoint.getCommandExecutor();
// ...
ExecResult result = executor.execute(execCommand);
}
-
如果只看框架设计
CamelExecCommandExecutable、CamelExecCommandArgs这类头本来应该是路由内部受控参数,用于在可信处理器之间传递执行上下文,而不应该由外部消息生产者直接注入。 -
但是现在,攻击者只要先利用前面的大小写缺陷把这些头带进Exchange,再借助
CaseInsensitiveMap让它们被标准名命中ExecProducer就会心安理得地执行攻击者指定的命令。 -
在这条链上,最后一道失守的防线其实就是
ExecProducer.process()之前的DefaultExecBinding.readInput()。因为到了这里,框架已经不再区分“这个头是可信内部逻辑写入的,还是外部消息伪装带进来的”。一旦读到了值,它就会把它们当作合法执行参数组装成ExecCommand。
3.5-[爆发点二] 同样的逻辑为什么还能劫持 camel-file
接着继续找camel-file,是因为它代表另一类非常常见的风险: 不一定要执行命令,只要能影响文件路径,就能形成越权写入、投放后门文件、污染后续处理结果等更隐蔽的攻击面。
首先看框架对这些头的定义:
// components/camel-file/src/main/java/org/apache/camel/component/file/FileConstants.java
@Metadata(description = "(producer) Specifies the name of the file to write ...")
public static final String FILE_NAME = Exchange.FILE_NAME;
xxxxxx
@Metadata(label = "producer", description = "Is used for overruling `CamelFileName` header ...")
public static final String OVERRULE_FILE_NAME = Exchange.OVERRULE_FILE_NAME;
然后看真正消费这些头的位置:
// components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileProducer.java
protected void doProcess(Exchange exchange) throws Exception {
final String existing = exchange.getIn().getHeader(FileConstants.FILE_NAME, String.class);
String target = createFileName(exchange);
// ...
exchange.getIn().removeHeader(Exchange.OVERRULE_FILE_NAME);
exchange.getIn().setHeader(FileConstants.FILE_NAME, existing);
}
// 2
public String createFileName(Exchange exchange) {
Object overrule = exchange.getIn().getHeader(FileConstants.OVERRULE_FILE_NAME);
final Object value = getOverrule(exchange, overrule);
// ...
}
private static Object getOverrule(Exchange exchange, Object overrule) {
if (overrule != null) {
// ...
} else {
value = exchange.getIn().getHeader(FileConstants.FILE_NAME);
}
return value;
}
// 3
public void writeFile(Exchange exchange, String fileName) throws GenericFileOperationFailedException {
if (endpoint.isAutoCreate()) {
String name = FileUtil.normalizePath(fileName);
File file = new File(name);
String directory = file.getParent();
if (directory != null) {
operations.buildDirectory(directory, absolute);
}
}
boolean success = operations.storeFile(fileName, exchange, -1);
}
这里的问题与camel-exec 是同构的。
-
CamelFileName / CamelOverruleFileName应该是路由内部用来控制输出文件名的保留头; -
实际中呢,只要攻击者能通过大小写变体把它们偷运进Exchange,下游
GenericFileProducer就会把它们作为正常的输出路径参数使用。 -
尤其当端点开启
autoCreate=true时,攻击者不仅能控制文件名,往往还可以顺手控制目录层级。
如果目标业务把 MQ 消息落盘到某个被其他系统消费的目录,那么这就不只是“任意写一个文件”那么简单了。它可能进一步变成:
1. 向任务目录投放伪造作业文件,诱导后续处理链执行;
2. 覆盖关键业务输出,制造数据污染;
3. 在某些部署结构下,向 Web 可访问目录写入可执行脚本或恶意内容。
3.6-[攻击链路] 从注入点到爆发点的完整闭环
现在整条链已经可以完整闭环了。我们重新串起来,会发现这个漏洞并不是单点编码失误,而是三个“各自看起来合理”的设计在边界上发生了危险叠加:
1. JMS/SJMS 希望用 HeaderFilterStrategy 阻止外部消息伪装内部头。
2. Camel 核心为了易用性,默认把消息头放进 CaseInsensitiveMap。
3. camel-exec、camel-file 等下游组件按规范内部头名读取参数。
完整调用链如下:
// 外部 HTTP/JMS 生产者
-> 注入大小写变体头(如 cAmelExecCommandExecutable / cAmelFileName)
-> JMS Consumer / SJMS Consumer
-> JmsBinding.extractHeadersFromJms()
-> HeaderFilterStrategy.applyFilterToExternalHeaders()
-> DefaultHeaderFilterStrategy.doFiltering() 前缀匹配遗漏混合大小写
-> map.put(key, value) 写入 Camel Message
-> DefaultMessage / CaseInsensitiveMap 以不区分大小写方式存储与检索
-> 下游组件按规范头名 getHeader()/removeHeader()
-> 爆发点 A: DefaultExecBinding.readInput() -> ExecProducer.process() -> 命令执行
-> 爆发点 B: GenericFileProducer.createFileName()/writeFile() -> 文件写入
0x4 修复建议
1、升级最新版本:将组件升级安全版本
https://camel.apache.org/security/CVE-2026-40453.html
2、临时防护措施:
-
限制访问:严格限制谁可以向JMS/消息队列生产消息;对桥接MQ的HTTP入口增加鉴权,不允许匿名或低信任租户直接投递
-
防火墙 / WAF:可拦截包含异常Camel内部头特征的请求头,例如 :
(?i)^camel(exec|file); -
危险组件隔离:避免把jms:或其他外部可写消息源直接连接到exec:、file:、bean:等受Header强驱动的端点,中间增加显式映射层,只复制白名单字段。
-
审计与检测:重点检索日志、Broker 属性和流量中是否出现非常规大小写的Camel 内部头,如cAmelExecCommandExecutable、CAMelFileName、cAmelOverruleFileName。这类特征在正常业务里几乎没有正当理由出现。
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。
/** 怎么样怎么样?专门找今天发,因为假期最后一天啦,同志们要注意时间管理呀**/

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)